]> git.saurik.com Git - wxWidgets.git/blame - wxPython/wx/lib/timectrl.py
removed second parameter from GetFirstChild calls
[wxWidgets.git] / wxPython / wx / lib / timectrl.py
CommitLineData
d14a1e28
RD
1#----------------------------------------------------------------------------
2# Name: wxTimeCtrl.py
3# Author: Will Sadkin
4# Created: 09/19/2002
5# Copyright: (c) 2002 by Will Sadkin, 2002
6# RCS-ID: $Id$
7# License: wxWindows license
8#----------------------------------------------------------------------------
9# NOTE:
10# This was written way it is because of the lack of masked edit controls
11# in wxWindows/wxPython. I would also have preferred to derive this
12# control from a wxSpinCtrl rather than wxTextCtrl, but the wxTextCtrl
13# component of that control is inaccessible through the interface exposed in
14# wxPython.
15#
16# wxTimeCtrl does not use validators, because it does careful manipulation
17# of the cursor in the text window on each keystroke, and validation is
18# cursor-position specific, so the control intercepts the key codes before the
19# validator would fire.
20#
21# wxTimeCtrl now also supports .SetValue() with either strings or wxDateTime
22# values, as well as range limits, with the option of either enforcing them
23# or simply coloring the text of the control if the limits are exceeded.
24#
25# Note: this class now makes heavy use of wxDateTime for parsing and
26# regularization, but it always does so with ephemeral instances of
27# wxDateTime, as the C++/Python validity of these instances seems to not
28# persist. Because "today" can be a day for which an hour can "not exist"
29# or be counted twice (1 day each per year, for DST adjustments), the date
30# portion of all wxDateTimes used/returned have their date portion set to
31# Jan 1, 1970 (the "epoch.")
32#
1fded56b 33
d14a1e28
RD
34"""<html><body>
35<P>
36<B>wxTimeCtrl</B> provides a multi-cell control that allows manipulation of a time
37value. It supports 12 or 24 hour format, and you can use wxDateTime or mxDateTime
38to get/set values from the control.
39<P>
40Left/right/tab keys to switch cells within a wxTimeCtrl, and the up/down arrows act
41like a spin control. wxTimeCtrl also allows for an actual spin button to be attached
42to the control, so that it acts like the up/down arrow keys.
43<P>
44The <B>!</B> or <B>c</B> key sets the value of the control to the current time.
45<P>
46Here's the API for wxTimeCtrl:
47<DL><PRE>
48 <B>wxTimeCtrl</B>(
49 parent, id = -1,
50 <B>value</B> = '12:00:00 AM',
51 pos = wxDefaultPosition,
52 size = wxDefaultSize,
53 <B>style</B> = wxTE_PROCESS_TAB,
54 <B>validator</B> = wxDefaultValidator,
55 name = "time",
56 <B>fmt24hr</B> = False,
57 <B>spinButton</B> = None,
58 <B>min</B> = None,
59 <B>max</B> = None,
60 <B>limited</B> = None,
61 <B>oob_color</B> = "Yellow"
62)
63</PRE>
64<UL>
65 <DT><B>value</B>
66 <DD>If no initial value is set, the default will be midnight; if an illegal string
67 is specified, a ValueError will result. (You can always later set the initial time
68 with SetValue() after instantiation of the control.)
69 <DL><B>size</B>
70 <DD>The size of the control will be automatically adjusted for 12/24 hour format
71 if wxDefaultSize is specified.
72 <DT><B>style</B>
73 <DD>By default, wxTimeCtrl will process TAB events, by allowing tab to the
74 different cells within the control.
75 <DT><B>validator</B>
76 <DD>By default, wxTimeCtrl just uses the default (empty) validator, as all
77 of its validation for entry control is handled internally. However, a validator
78 can be supplied to provide data transfer capability to the control.
79 <BR>
80 <DT><B>fmt24hr</B>
81 <DD>If True, control will display time in 24 hour time format; if False, it will
82 use 12 hour AM/PM format. SetValue() will adjust values accordingly for the
83 control, based on the format specified.
84 <BR>
85 <DT><B>spinButton</B>
86 <DD>If specified, this button's events will be bound to the behavior of the
87 wxTimeCtrl, working like up/down cursor key events. (See BindSpinButton.)
88 <BR>
89 <DT><B>min</B>
90 <DD>Defines the lower bound for "valid" selections in the control.
91 By default, wxTimeCtrl doesn't have bounds. You must set both upper and lower
92 bounds to make the control pay attention to them, (as only one bound makes no sense
93 with times.) "Valid" times will fall between the min and max "pie wedge" of the
94 clock.
95 <DT><B>max</B>
96 <DD>Defines the upper bound for "valid" selections in the control.
97 "Valid" times will fall between the min and max "pie wedge" of the
98 clock. (This can be a "big piece", ie. <b>min = 11pm, max= 10pm</b>
99 means <I>all but the hour from 10:00pm to 11pm are valid times.</I>)
100 <DT><B>limited</B>
101 <DD>If True, the control will not permit entry of values that fall outside the
102 set bounds.
103 <BR>
104 <DT><B>oob_color</B>
105 <DD>Sets the background color used to indicate out-of-bounds values for the control
106 when the control is not limited. This is set to "Yellow" by default.
107 </DL>
108</UL>
109<BR>
110<BR>
111<BR>
112<DT><B>EVT_TIMEUPDATE(win, id, func)</B>
113<DD>func is fired whenever the value of the control changes.
114<BR>
115<BR>
116<DT><B>SetValue(time_string | wxDateTime | wxTimeSpan | mx.DateTime | mx.DateTimeDelta)</B>
117<DD>Sets the value of the control to a particular time, given a valid
118value; raises ValueError on invalid value.
119<EM>NOTE:</EM> This will only allow mx.DateTime or mx.DateTimeDelta if mx.DateTime
120was successfully imported by the class module.
121<BR>
122<DT><B>GetValue(as_wxDateTime = False, as_mxDateTime = False, as_wxTimeSpan=False, as mxDateTimeDelta=False)</B>
123<DD>Retrieves the value of the time from the control. By default this is
124returned as a string, unless one of the other arguments is set; args are
125searched in the order listed; only one value will be returned.
126<BR>
127<DT><B>GetWxDateTime(value=None)</B>
128<DD>When called without arguments, retrieves the value of the control, and applies
129it to the wxDateTimeFromHMS() constructor, and returns the resulting value.
130The date portion will always be set to Jan 1, 1970. This form is the same
131as GetValue(as_wxDateTime=True). GetWxDateTime can also be called with any of the
132other valid time formats settable with SetValue, to regularize it to a single
133wxDateTime form. The function will raise ValueError on an unconvertable argument.
134<BR>
135<DT><B>GetMxDateTime()</B>
136<DD>Retrieves the value of the control and applies it to the DateTime.Time()
137constructor,and returns the resulting value. (The date portion will always be
138set to Jan 1, 1970.) (Same as GetValue(as_wxDateTime=True); provided for backward
139compatibility with previous release.)
140<BR>
141<BR>
142<DT><B>BindSpinButton(wxSpinBtton)</B>
143<DD>Binds an externally created spin button to the control, so that up/down spin
144events change the active cell or selection in the control (in addition to the
145up/down cursor keys.) (This is primarily to allow you to create a "standard"
146interface to time controls, as seen in Windows.)
147<BR>
148<BR>
149<DT><B>SetMin(min=None)</B>
150<DD>Sets the expected minimum value, or lower bound, of the control.
151(The lower bound will only be enforced if the control is
152configured to limit its values to the set bounds.)
153If a value of <I>None</I> is provided, then the control will have
154explicit lower bound. If the value specified is greater than
155the current lower bound, then the function returns False and the
156lower bound will not change from its current setting. On success,
157the function returns True. Even if set, if there is no corresponding
158upper bound, the control will behave as if it is unbounded.
159<DT><DD>If successful and the current value is outside the
160new bounds, if the control is limited the value will be
161automatically adjusted to the nearest bound; if not limited,
162the background of the control will be colored with the current
163out-of-bounds color.
164<BR>
165<DT><B>GetMin(as_string=False)</B>
166<DD>Gets the current lower bound value for the control, returning
167None, if not set, or a wxDateTime, unless the as_string parameter
168is set to True, at which point it will return the string
169representation of the lower bound.
170<BR>
171<BR>
172<DT><B>SetMax(max=None)</B>
173<DD>Sets the expected maximum value, or upper bound, of the control.
174(The upper bound will only be enforced if the control is
175configured to limit its values to the set bounds.)
176If a value of <I>None</I> is provided, then the control will
177have no explicit upper bound. If the value specified is less
178than the current lower bound, then the function returns False and
179the maximum will not change from its current setting. On success,
180the function returns True. Even if set, if there is no corresponding
181lower bound, the control will behave as if it is unbounded.
182<DT><DD>If successful and the current value is outside the
183new bounds, if the control is limited the value will be
184automatically adjusted to the nearest bound; if not limited,
185the background of the control will be colored with the current
186out-of-bounds color.
187<BR>
188<DT><B>GetMax(as_string = False)</B>
189<DD>Gets the current upper bound value for the control, returning
190None, if not set, or a wxDateTime, unless the as_string parameter
191is set to True, at which point it will return the string
192representation of the lower bound.
1fded56b 193
d14a1e28
RD
194<BR>
195<BR>
196<DT><B>SetBounds(min=None,max=None)</B>
197<DD>This function is a convenience function for setting the min and max
198values at the same time. The function only applies the maximum bound
199if setting the minimum bound is successful, and returns True
200only if both operations succeed. <B><I>Note: leaving out an argument
201will remove the corresponding bound, and result in the behavior of
202an unbounded control.</I></B>
203<BR>
204<DT><B>GetBounds(as_string = False)</B>
205<DD>This function returns a two-tuple (min,max), indicating the
206current bounds of the control. Each value can be None if
207that bound is not set. The values will otherwise be wxDateTimes
208unless the as_string argument is set to True, at which point they
209will be returned as string representations of the bounds.
210<BR>
211<BR>
212<DT><B>IsInBounds(value=None)</B>
213<DD>Returns <I>True</I> if no value is specified and the current value
214of the control falls within the current bounds. This function can also
215be called with a value to see if that value would fall within the current
216bounds of the given control. It will raise ValueError if the value
217specified is not a wxDateTime, mxDateTime (if available) or parsable string.
218<BR>
219<BR>
220<DT><B>IsValid(value)</B>
221<DD>Returns <I>True</I>if specified value is a legal time value and
222falls within the current bounds of the given control.
223<BR>
224<BR>
225<DT><B>SetLimited(bool)</B>
226<DD>If called with a value of True, this function will cause the control
227to limit the value to fall within the bounds currently specified.
228(Provided both bounds have been set.)
229If the control's value currently exceeds the bounds, it will then
230be set to the nearest bound.
231If called with a value of False, this function will disable value
232limiting, but coloring of out-of-bounds values will still take
233place if bounds have been set for the control.
234<DT><B>IsLimited()</B>
235<DD>Returns <I>True</I> if the control is currently limiting the
236value to fall within the current bounds.
237<BR>
238</DL>
239</body></html>
240"""
241
242import string, copy
243from wxPython.wx import *
244from wxPython.tools.dbg import Logger
245from wxPython.lib.maskededit import wxMaskedTextCtrl, Field
246
247dbg = Logger()
248dbg(enable=0)
249
250try:
251 from mx import DateTime
252 accept_mx = True
253except ImportError:
254 accept_mx = False
255
256# This class of event fires whenever the value of the time changes in the control:
257wxEVT_TIMEVAL_UPDATED = wxNewId()
258def EVT_TIMEUPDATE(win, id, func):
259 """Used to trap events indicating that the current time has been changed."""
260 win.Connect(id, -1, wxEVT_TIMEVAL_UPDATED, func)
261
262class TimeUpdatedEvent(wxPyCommandEvent):
263 def __init__(self, id, value ='12:00:00 AM'):
264 wxPyCommandEvent.__init__(self, wxEVT_TIMEVAL_UPDATED, id)
265 self.value = value
266 def GetValue(self):
267 """Retrieve the value of the time control at the time this event was generated"""
268 return self.value
269
270
271class wxTimeCtrl(wxMaskedTextCtrl):
272
273 valid_ctrl_params = {
274 'display_seconds' : True, # by default, shows seconds
275 'min': None, # by default, no bounds set
276 'max': None,
277 'limited': False, # by default, no limiting even if bounds set
278 'useFixedWidthFont': True, # by default, use a fixed-width font
279 'oob_color': "Yellow" # by default, the default wxMaskedTextCtrl "invalid" color
280 }
281
282 def __init__ (
283 self, parent, id=-1, value = '12:00:00 AM',
284 pos = wxDefaultPosition, size = wxDefaultSize,
285 fmt24hr=False,
286 spinButton = None,
287 style = wxTE_PROCESS_TAB,
288 validator = wxDefaultValidator,
289 name = "time",
290 **kwargs ):
291
292 # set defaults for control:
293 dbg('setting defaults:')
294 for key, param_value in wxTimeCtrl.valid_ctrl_params.items():
295 # This is done this way to make setattr behave consistently with
296 # "private attribute" name mangling
297 setattr(self, "_wxTimeCtrl__" + key, copy.copy(param_value))
298
299 # create locals from current defaults, so we can override if
300 # specified in kwargs, and handle uniformly:
301 min = self.__min
302 max = self.__max
303 limited = self.__limited
304 self.__posCurrent = 0
305
306
307 # (handle positional args (from original release) differently from rest of kwargs:)
308 self.__fmt24hr = fmt24hr
309
310 maskededit_kwargs = {}
311
312 # assign keyword args as appropriate:
313 for key, param_value in kwargs.items():
314 if key not in wxTimeCtrl.valid_ctrl_params.keys():
315 raise AttributeError('invalid keyword argument "%s"' % key)
316
317 if key == "display_seconds":
318 self.__display_seconds = param_value
319
320 elif key == "min": min = param_value
321 elif key == "max": max = param_value
322 elif key == "limited": limited = param_value
323
324 elif key == "useFixedWidthFont":
325 maskededit_kwargs[key] = param_value
326 elif key == "oob_color":
327 maskededit_kwargs['invalidBackgroundColor'] = param_value
328
329 if self.__fmt24hr:
330 if self.__display_seconds: maskededit_kwargs['autoformat'] = 'MILTIMEHHMMSS'
331 else: maskededit_kwargs['autoformat'] = 'MILTIMEHHMM'
332
333 # Set hour field to zero-pad, right-insert, require explicit field change,
334 # select entire field on entry, and require a resultant valid entry
335 # to allow character entry:
336 hourfield = Field(formatcodes='0r<SV', validRegex='0\d|1\d|2[0123]', validRequired=True)
337 else:
338 if self.__display_seconds: maskededit_kwargs['autoformat'] = 'TIMEHHMMSS'
339 else: maskededit_kwargs['autoformat'] = 'TIMEHHMM'
340
341 # Set hour field to allow spaces (at start), right-insert,
342 # require explicit field change, select entire field on entry,
343 # and require a resultant valid entry to allow character entry:
344 hourfield = Field(formatcodes='_0<rSV', validRegex='0[1-9]| [1-9]|1[012]', validRequired=True)
345 ampmfield = Field(formatcodes='S', emptyInvalid = True, validRequired = True)
346
347 # Field 1 is always a zero-padded right-insert minute field,
348 # similarly configured as above:
349 minutefield = Field(formatcodes='0r<SV', validRegex='[0-5]\d', validRequired=True)
350
351 fields = [ hourfield, minutefield ]
352 if self.__display_seconds:
353 fields.append(copy.copy(minutefield)) # second field has same constraints as field 1
354
355 if not self.__fmt24hr:
356 fields.append(ampmfield)
357
358 # set fields argument:
359 maskededit_kwargs['fields'] = fields
360
361 # This allows range validation if set
362 maskededit_kwargs['validFunc'] = self.IsInBounds
363
364 # This allows range limits to affect insertion into control or not
365 # dynamically without affecting individual field constraint validation
366 maskededit_kwargs['retainFieldValidation'] = True
367
368 # allow control over font selection:
369 maskededit_kwargs['useFixedWidthFont'] = self.__useFixedWidthFont
370
371 # allow for explicit size specification:
372 if size != wxDefaultSize:
373 # override (and remove) "autofit" autoformat code in standard time formats:
374 maskededit_kwargs['formatcodes'] = 'T!'
375
376 # Now we can initialize the base control:
377 wxMaskedTextCtrl.__init__(
378 self, parent, id=id,
379 pos=pos, size=size,
380 style = style,
381 validator = validator,
382 name = name,
383 setupEventHandling = False,
384 **maskededit_kwargs)
385
386
387 # This makes ':' act like tab (after we fix each ':' key event to remove "shift")
388 self._SetKeyHandler(':', self._OnChangeField)
389
390
391 # This makes the up/down keys act like spin button controls:
392 self._SetKeycodeHandler(WXK_UP, self.__OnSpinUp)
393 self._SetKeycodeHandler(WXK_DOWN, self.__OnSpinDown)
394
395
396 # This allows ! and c/C to set the control to the current time:
397 self._SetKeyHandler('!', self.__OnSetToNow)
398 self._SetKeyHandler('c', self.__OnSetToNow)
399 self._SetKeyHandler('C', self.__OnSetToNow)
400
401
402 # Set up event handling ourselves, so we can insert special
403 # processing on the ":' key to remove the "shift" attribute
404 # *before* the default handlers have been installed, so
405 # that : takes you forward, not back, and so we can issue
406 # EVT_TIMEUPDATE events on changes:
407
408 EVT_SET_FOCUS( self, self._OnFocus ) ## defeat automatic full selection
409 EVT_KILL_FOCUS( self, self._OnKillFocus ) ## run internal validator
410 EVT_LEFT_UP(self, self.__LimitSelection) ## limit selections to single field
411 EVT_LEFT_DCLICK(self, self._OnDoubleClick ) ## select field under cursor on dclick
412 EVT_KEY_DOWN( self, self._OnKeyDown ) ## capture control events not normally seen, eg ctrl-tab.
413 EVT_CHAR( self, self.__OnChar ) ## remove "shift" attribute from colon key event,
414 ## then call wxMaskedTextCtrl._OnChar with
415 ## the possibly modified event.
416 EVT_TEXT( self, self.GetId(), self.__OnTextChange ) ## color control appropriately and EVT_TIMEUPDATE events
417
418
419 # Validate initial value and set if appropriate
420 try:
421 self.SetBounds(min, max)
422 self.SetLimited(limited)
423 self.SetValue(value)
424 except:
425 self.SetValue('12:00:00 AM')
426
427 if spinButton:
428 self.BindSpinButton(spinButton) # bind spin button up/down events to this control
429
430
431
432 def BindSpinButton(self, sb):
433 """
434 This function binds an externally created spin button to the control, so that
435 up/down events from the button automatically change the control.
436 """
437 dbg('wxTimeCtrl::BindSpinButton')
438 self.__spinButton = sb
439 if self.__spinButton:
440 # bind event handlers to spin ctrl
441 EVT_SPIN_UP(self.__spinButton, self.__spinButton.GetId(), self.__OnSpinUp)
442 EVT_SPIN_DOWN(self.__spinButton, self.__spinButton.GetId(), self.__OnSpinDown)
443
444
445 def __repr__(self):
446 return "<wxTimeCtrl: %s>" % self.GetValue()
447
448
449 def SetValue(self, value):
450 """
451 Validating SetValue function for time values:
452 This function will do dynamic type checking on the value argument,
453 and convert wxDateTime, mxDateTime, or 12/24 format time string
454 into the appropriate format string for the control.
455 """
456 dbg('wxTimeCtrl::SetValue(%s)' % repr(value), indent=1)
457 try:
458 strtime = self._toGUI(self.__validateValue(value))
459 except:
460 dbg('validation failed', indent=0)
461 raise
462
463 dbg('strtime:', strtime)
464 self._SetValue(strtime)
465 dbg(indent=0)
466
467 def GetValue(self,
468 as_wxDateTime = False,
469 as_mxDateTime = False,
470 as_wxTimeSpan = False,
471 as_mxDateTimeDelta = False):
472
473
474 if as_wxDateTime or as_mxDateTime or as_wxTimeSpan or as_mxDateTimeDelta:
475 value = self.GetWxDateTime()
476 if as_wxDateTime:
477 pass
478 elif as_mxDateTime:
479 value = DateTime.DateTime(1970, 1, 1, value.GetHour(), value.GetMinute(), value.GetSecond())
480 elif as_wxTimeSpan:
481 value = wxTimeSpan(value.GetHour(), value.GetMinute(), value.GetSecond())
482 elif as_mxDateTimeDelta:
483 value = DateTime.DateTimeDelta(0, value.GetHour(), value.GetMinute(), value.GetSecond())
484 else:
485 value = wxMaskedTextCtrl.GetValue(self)
486 return value
487
488
489 def SetWxDateTime(self, wxdt):
490 """
491 Because SetValue can take a wxDateTime, this is now just an alias.
492 """
493 self.SetValue(wxdt)
494
495
496 def GetWxDateTime(self, value=None):
497 """
498 This function is the conversion engine for wxTimeCtrl; it takes
499 one of the following types:
500 time string
501 wxDateTime
502 wxTimeSpan
503 mxDateTime
504 mxDateTimeDelta
505 and converts it to a wxDateTime that always has Jan 1, 1970 as its date
506 portion, so that range comparisons around values can work using
507 wxDateTime's built-in comparison function. If a value is not
508 provided to convert, the string value of the control will be used.
509 If the value is not one of the accepted types, a ValueError will be
510 raised.
511 """
512 global accept_mx
513 dbg(suspend=1)
514 dbg('wxTimeCtrl::GetWxDateTime(%s)' % repr(value), indent=1)
515 if value is None:
516 dbg('getting control value')
517 value = self.GetValue()
518 dbg('value = "%s"' % value)
519
520 if type(value) == types.UnicodeType:
521 value = str(value) # convert to regular string
522
523 valid = True # assume true
524 if type(value) == types.StringType:
525
526 # Construct constant wxDateTime, then try to parse the string:
527 wxdt = wxDateTimeFromDMY(1, 0, 1970)
528 dbg('attempting conversion')
529 value = value.strip() # (parser doesn't like leading spaces)
530 checkTime = wxdt.ParseTime(value)
531 valid = checkTime == len(value) # entire string parsed?
532 dbg('checkTime == len(value)?', valid)
533
534 if not valid:
535 dbg(indent=0, suspend=0)
536 raise ValueError('cannot convert string "%s" to valid time' % value)
537
538 else:
539 if isinstance(value, wxDateTime):
540 hour, minute, second = value.GetHour(), value.GetMinute(), value.GetSecond()
541 elif isinstance(value, wxTimeSpan):
542 totalseconds = value.GetSeconds()
543 hour = totalseconds / 3600
544 minute = totalseconds / 60 - (hour * 60)
545 second = totalseconds - ((hour * 3600) + (minute * 60))
546
547 elif accept_mx and isinstance(value, DateTime.DateTimeType):
548 hour, minute, second = value.hour, value.minute, value.second
549 elif accept_mx and isinstance(value, DateTime.DateTimeDeltaType):
550 hour, minute, second = value.hour, value.minute, value.second
551 else:
552 # Not a valid function argument
553 if accept_mx:
554 error = 'GetWxDateTime requires wxDateTime, mxDateTime or parsable time string, passed %s'% repr(value)
555 else:
556 error = 'GetWxDateTime requires wxDateTime or parsable time string, passed %s'% repr(value)
557 dbg(indent=0, suspend=0)
558 raise ValueError(error)
559
560 wxdt = wxDateTimeFromDMY(1, 0, 1970)
561 wxdt.SetHour(hour)
562 wxdt.SetMinute(minute)
563 wxdt.SetSecond(second)
564
565 dbg('wxdt:', wxdt, indent=0, suspend=0)
566 return wxdt
567
568
569 def SetMxDateTime(self, mxdt):
570 """
571 Because SetValue can take an mxDateTime, (if DateTime is importable),
572 this is now just an alias.
573 """
574 self.SetValue(value)
575
576
577 def GetMxDateTime(self, value=None):
578 if value is None:
579 t = self.GetValue(as_mxDateTime=True)
580 else:
581 # Convert string 1st to wxDateTime, then use components, since
582 # mx' DateTime.Parser.TimeFromString() doesn't handle AM/PM:
583 wxdt = self.GetWxDateTime(value)
584 hour, minute, second = wxdt.GetHour(), wxdt.GetMinute(), wxdt.GetSecond()
585 t = DateTime.DateTime(1970,1,1) + DateTimeDelta(0, hour, minute, second)
586 return t
587
588
589 def SetMin(self, min=None):
590 """
591 Sets the minimum value of the control. If a value of None
592 is provided, then the control will have no explicit minimum value.
593 If the value specified is greater than the current maximum value,
594 then the function returns 0 and the minimum will not change from
595 its current setting. On success, the function returns 1.
596
597 If successful and the current value is lower than the new lower
598 bound, if the control is limited, the value will be automatically
599 adjusted to the new minimum value; if not limited, the value in the
600 control will be colored as invalid.
601 """
602 dbg('wxTimeCtrl::SetMin(%s)'% repr(min), indent=1)
603 if min is not None:
604 try:
605 min = self.GetWxDateTime(min)
606 self.__min = self._toGUI(min)
607 except:
608 dbg('exception occurred', indent=0)
609 return False
610 else:
611 self.__min = min
612
613 if self.IsLimited() and not self.IsInBounds():
614 self.SetLimited(self.__limited) # force limited value:
615 else:
616 self._CheckValid()
617 ret = True
618 dbg('ret:', ret, indent=0)
619 return ret
620
621
622 def GetMin(self, as_string = False):
623 """
624 Gets the minimum value of the control.
625 If None, it will return None. Otherwise it will return
626 the current minimum bound on the control, as a wxDateTime
627 by default, or as a string if as_string argument is True.
628 """
629 dbg(suspend=1)
630 dbg('wxTimeCtrl::GetMin, as_string?', as_string, indent=1)
631 if self.__min is None:
632 dbg('(min == None)')
633 ret = self.__min
634 elif as_string:
635 ret = self.__min
636 dbg('ret:', ret)
637 else:
638 try:
639 ret = self.GetWxDateTime(self.__min)
640 except:
641 dbg(suspend=0)
642 dbg('exception occurred', indent=0)
643 dbg('ret:', repr(ret))
644 dbg(indent=0, suspend=0)
645 return ret
646
647
648 def SetMax(self, max=None):
649 """
650 Sets the maximum value of the control. If a value of None
651 is provided, then the control will have no explicit maximum value.
652 If the value specified is less than the current minimum value, then
653 the function returns False and the maximum will not change from its
654 current setting. On success, the function returns True.
655
656 If successful and the current value is greater than the new upper
657 bound, if the control is limited the value will be automatically
658 adjusted to this maximum value; if not limited, the value in the
659 control will be colored as invalid.
660 """
661 dbg('wxTimeCtrl::SetMax(%s)' % repr(max), indent=1)
662 if max is not None:
663 try:
664 max = self.GetWxDateTime(max)
665 self.__max = self._toGUI(max)
666 except:
667 dbg('exception occurred', indent=0)
668 return False
669 else:
670 self.__max = max
671 dbg('max:', repr(self.__max))
672 if self.IsLimited() and not self.IsInBounds():
673 self.SetLimited(self.__limited) # force limited value:
674 else:
675 self._CheckValid()
676 ret = True
677 dbg('ret:', ret, indent=0)
678 return ret
679
680
681 def GetMax(self, as_string = False):
682 """
683 Gets the minimum value of the control.
684 If None, it will return None. Otherwise it will return
685 the current minimum bound on the control, as a wxDateTime
686 by default, or as a string if as_string argument is True.
687 """
688 dbg(suspend=1)
689 dbg('wxTimeCtrl::GetMin, as_string?', as_string, indent=1)
690 if self.__max is None:
691 dbg('(max == None)')
692 ret = self.__max
693 elif as_string:
694 ret = self.__max
695 dbg('ret:', ret)
696 else:
697 try:
698 ret = self.GetWxDateTime(self.__max)
699 except:
700 dbg(suspend=0)
701 dbg('exception occurred', indent=0)
702 raise
703 dbg('ret:', repr(ret))
704 dbg(indent=0, suspend=0)
705 return ret
706
707
708 def SetBounds(self, min=None, max=None):
709 """
710 This function is a convenience function for setting the min and max
711 values at the same time. The function only applies the maximum bound
712 if setting the minimum bound is successful, and returns True
713 only if both operations succeed.
714 NOTE: leaving out an argument will remove the corresponding bound.
715 """
716 ret = self.SetMin(min)
717 return ret and self.SetMax(max)
718
719
720 def GetBounds(self, as_string = False):
721 """
722 This function returns a two-tuple (min,max), indicating the
723 current bounds of the control. Each value can be None if
724 that bound is not set.
725 """
726 return (self.GetMin(as_string), self.GetMax(as_string))
727
728
729 def SetLimited(self, limited):
730 """
731 If called with a value of True, this function will cause the control
732 to limit the value to fall within the bounds currently specified.
733 If the control's value currently exceeds the bounds, it will then
734 be limited accordingly.
735
736 If called with a value of 0, this function will disable value
737 limiting, but coloring of out-of-bounds values will still take
738 place if bounds have been set for the control.
739 """
740 dbg('wxTimeCtrl::SetLimited(%d)' % limited, indent=1)
741 self.__limited = limited
742
743 if not limited:
744 self.SetMaskParameters(validRequired = False)
745 self._CheckValid()
746 dbg(indent=0)
747 return
748
749 dbg('requiring valid value')
750 self.SetMaskParameters(validRequired = True)
751
752 min = self.GetMin()
753 max = self.GetMax()
754 if min is None or max is None:
755 dbg('both bounds not set; no further action taken')
756 return # can't limit without 2 bounds
757
758 elif not self.IsInBounds():
759 # set value to the nearest bound:
760 try:
761 value = self.GetWxDateTime()
762 except:
763 dbg('exception occurred', indent=0)
764 raise
765
766 if min <= max: # valid range doesn't span midnight
767 dbg('min <= max')
768 # which makes the "nearest bound" computation trickier...
769
770 # determine how long the "invalid" pie wedge is, and cut
771 # this interval in half for comparison purposes:
772
773 # Note: relies on min and max and value date portions
774 # always being the same.
775 interval = (min + wxTimeSpan(24, 0, 0, 0)) - max
776
777 half_interval = wxTimeSpan(
778 0, # hours
779 0, # minutes
780 interval.GetSeconds() / 2, # seconds
781 0) # msec
782
783 if value < min: # min is on next day, so use value on
784 # "next day" for "nearest" interval calculation:
785 cmp_value = value + wxTimeSpan(24, 0, 0, 0)
786 else: # "before midnight; ok
787 cmp_value = value
788
789 if (cmp_value - max) > half_interval:
790 dbg('forcing value to min (%s)' % min.FormatTime())
791 self.SetValue(min)
792 else:
793 dbg('forcing value to max (%s)' % max.FormatTime())
794 self.SetValue(max)
795 else:
796 dbg('max < min')
797 # therefore max < value < min guaranteed to be true,
798 # so "nearest bound" calculation is much easier:
799 if (value - max) >= (min - value):
800 # current value closer to min; pick that edge of pie wedge
801 dbg('forcing value to min (%s)' % min.FormatTime())
802 self.SetValue(min)
803 else:
804 dbg('forcing value to max (%s)' % max.FormatTime())
805 self.SetValue(max)
806
807 dbg(indent=0)
808
809
810
811 def IsLimited(self):
812 """
813 Returns True if the control is currently limiting the
814 value to fall within any current bounds. Note: can
815 be set even if there are no current bounds.
816 """
817 return self.__limited
818
819
820 def IsInBounds(self, value=None):
821 """
822 Returns True if no value is specified and the current value
823 of the control falls within the current bounds. As the clock
824 is a "circle", both minimum and maximum bounds must be set for
825 a value to ever be considered "out of bounds". This function can
826 also be called with a value to see if that value would fall within
827 the current bounds of the given control.
828 """
829 if value is not None:
830 try:
831 value = self.GetWxDateTime(value) # try to regularize passed value
832 except ValueError:
833 dbg('ValueError getting wxDateTime for %s' % repr(value), indent=0)
834 raise
835
836 dbg('wxTimeCtrl::IsInBounds(%s)' % repr(value), indent=1)
837 if self.__min is None or self.__max is None:
838 dbg(indent=0)
839 return True
840
841 elif value is None:
842 try:
843 value = self.GetWxDateTime()
844 except:
845 dbg('exception occurred', indent=0)
846
847 dbg('value:', value.FormatTime())
848
849 # Get wxDateTime representations of bounds:
850 min = self.GetMin()
851 max = self.GetMax()
852
853 midnight = wxDateTimeFromDMY(1, 0, 1970)
854 if min <= max: # they don't span midnight
855 ret = min <= value <= max
856
857 else:
858 # have to break into 2 tests; to be in bounds
859 # either "min" <= value (<= midnight of *next day*)
860 # or midnight <= value <= "max"
861 ret = min <= value or (midnight <= value <= max)
862 dbg('in bounds?', ret, indent=0)
863 return ret
864
865
866 def IsValid( self, value ):
867 """
868 Can be used to determine if a given value would be a legal and
869 in-bounds value for the control.
870 """
871 try:
872 self.__validateValue(value)
873 return True
874 except ValueError:
875 return False
876
877
878#-------------------------------------------------------------------------------------------------------------
879# these are private functions and overrides:
880
881
882 def __OnTextChange(self, event=None):
883 dbg('wxTimeCtrl::OnTextChange', indent=1)
884
885 # Allow wxMaskedtext base control to color as appropriate,
886 # and Skip the EVT_TEXT event (if appropriate.)
887 ##! WS: For some inexplicable reason, every wxTextCtrl.SetValue()
888 ## call is generating two (2) EVT_TEXT events. (!)
889 ## The the only mechanism I can find to mask this problem is to
890 ## keep track of last value seen, and declare a valid EVT_TEXT
891 ## event iff the value has actually changed. The masked edit
892 ## OnTextChange routine does this, and returns True on a valid event,
893 ## False otherwise.
894 if not wxMaskedTextCtrl._OnTextChange(self, event):
895 return
896
897 dbg('firing TimeUpdatedEvent...')
898 evt = TimeUpdatedEvent(self.GetId(), self.GetValue())
899 evt.SetEventObject(self)
900 self.GetEventHandler().ProcessEvent(evt)
901 dbg(indent=0)
902
903
904 def SetInsertionPoint(self, pos):
905 """
906 Records the specified position and associated cell before calling base class' function.
907 This is necessary to handle the optional spin button, because the insertion
908 point is lost when the focus shifts to the spin button.
909 """
910 dbg('wxTimeCtrl::SetInsertionPoint', pos, indent=1)
911 wxMaskedTextCtrl.SetInsertionPoint(self, pos) # (causes EVT_TEXT event to fire)
912 self.__posCurrent = self.GetInsertionPoint()
913 dbg(indent=0)
914
915
916 def SetSelection(self, sel_start, sel_to):
917 dbg('wxTimeCtrl::SetSelection', sel_start, sel_to, indent=1)
918
919 # Adjust selection range to legal extent if not already
920 if sel_start < 0:
921 sel_start = 0
922
923 if self.__posCurrent != sel_start: # force selection and insertion point to match
924 self.SetInsertionPoint(sel_start)
925 cell_start, cell_end = self._FindField(sel_start)._extent
926 if not cell_start <= sel_to <= cell_end:
927 sel_to = cell_end
928
929 self.__bSelection = sel_start != sel_to
930 wxMaskedTextCtrl.SetSelection(self, sel_start, sel_to)
931 dbg(indent=0)
932
933
934 def __OnSpin(self, key):
935 """
936 This is the function that gets called in response to up/down arrow or
937 bound spin button events.
938 """
939 self.__IncrementValue(key, self.__posCurrent) # changes the value
940
941 # Ensure adjusted control regains focus and has adjusted portion
942 # selected:
943 self.SetFocus()
944 start, end = self._FindField(self.__posCurrent)._extent
945 self.SetInsertionPoint(start)
946 self.SetSelection(start, end)
947 dbg('current position:', self.__posCurrent)
948
949
950 def __OnSpinUp(self, event):
951 """
952 Event handler for any bound spin button on EVT_SPIN_UP;
953 causes control to behave as if up arrow was pressed.
954 """
955 dbg('wxTimeCtrl::OnSpinUp', indent=1)
956 self.__OnSpin(WXK_UP)
957 keep_processing = False
958 dbg(indent=0)
959 return keep_processing
960
961
962 def __OnSpinDown(self, event):
963 """
964 Event handler for any bound spin button on EVT_SPIN_DOWN;
965 causes control to behave as if down arrow was pressed.
966 """
967 dbg('wxTimeCtrl::OnSpinDown', indent=1)
968 self.__OnSpin(WXK_DOWN)
969 keep_processing = False
970 dbg(indent=0)
971 return keep_processing
972
973
974 def __OnChar(self, event):
975 """
976 Handler to explicitly look for ':' keyevents, and if found,
977 clear the m_shiftDown field, so it will behave as forward tab.
978 It then calls the base control's _OnChar routine with the modified
979 event instance.
980 """
981 dbg('wxTimeCtrl::OnChar', indent=1)
982 keycode = event.GetKeyCode()
983 dbg('keycode:', keycode)
984 if keycode == ord(':'):
985 dbg('colon seen! removing shift attribute')
986 event.m_shiftDown = False
987 wxMaskedTextCtrl._OnChar(self, event ) ## handle each keypress
988 dbg(indent=0)
989
990
991 def __OnSetToNow(self, event):
992 """
993 This is the key handler for '!' and 'c'; this allows the user to
994 quickly set the value of the control to the current time.
995 """
996 self.SetValue(wxDateTime_Now().FormatTime())
997 keep_processing = False
998 return keep_processing
999
1000
1001 def __LimitSelection(self, event):
1002 """
1003 Event handler for motion events; this handler
1004 changes limits the selection to the new cell boundaries.
1005 """
1006 dbg('wxTimeCtrl::LimitSelection', indent=1)
1007 pos = self.GetInsertionPoint()
1008 self.__posCurrent = pos
1009 sel_start, sel_to = self.GetSelection()
1010 selection = sel_start != sel_to
1011 if selection:
1012 # only allow selection to end of current cell:
1013 start, end = self._FindField(sel_start)._extent
1014 if sel_to < pos: sel_to = start
1015 elif sel_to > pos: sel_to = end
1016
1017 dbg('new pos =', self.__posCurrent, 'select to ', sel_to)
1018 self.SetInsertionPoint(self.__posCurrent)
1019 self.SetSelection(self.__posCurrent, sel_to)
1020 if event: event.Skip()
1021 dbg(indent=0)
1022
1023
1024 def __IncrementValue(self, key, pos):
1025 dbg('wxTimeCtrl::IncrementValue', key, pos, indent=1)
1026 text = self.GetValue()
1027 field = self._FindField(pos)
1028 dbg('field: ', field._index)
1029 start, end = field._extent
1030 slice = text[start:end]
1031 if key == WXK_UP: increment = 1
1032 else: increment = -1
1033
1034 if slice in ('A', 'P'):
1035 if slice == 'A': newslice = 'P'
1036 elif slice == 'P': newslice = 'A'
1037 newvalue = text[:start] + newslice + text[end:]
1038
1039 elif field._index == 0:
1040 # adjusting this field is trickier, as its value can affect the
1041 # am/pm setting. So, we use wxDateTime to generate a new value for us:
1042 # (Use a fixed date not subject to DST variations:)
1043 converter = wxDateTimeFromDMY(1, 0, 1970)
1044 dbg('text: "%s"' % text)
1045 converter.ParseTime(text.strip())
1046 currenthour = converter.GetHour()
1047 dbg('current hour:', currenthour)
1048 newhour = (currenthour + increment) % 24
1049 dbg('newhour:', newhour)
1050 converter.SetHour(newhour)
1051 dbg('converter.GetHour():', converter.GetHour())
1052 newvalue = converter # take advantage of auto-conversion for am/pm in .SetValue()
1053
1054 else: # minute or second field; handled the same way:
1055 newslice = "%02d" % ((int(slice) + increment) % 60)
1056 newvalue = text[:start] + newslice + text[end:]
1057
1058 try:
1059 self.SetValue(newvalue)
1060
1061 except ValueError: # must not be in bounds:
1062 if not wxValidator_IsSilent():
1063 wxBell()
1064 dbg(indent=0)
1065
1066
1067 def _toGUI( self, wxdt ):
1068 """
1069 This function takes a wxdt as an unambiguous representation of a time, and
1070 converts it to a string appropriate for the format of the control.
1071 """
1072 if self.__fmt24hr:
1073 if self.__display_seconds: strval = wxdt.Format('%H:%M:%S')
1074 else: strval = wxdt.Format('%H:%M')
1075 else:
1076 if self.__display_seconds: strval = wxdt.Format('%I:%M:%S %p')
1077 else: strval = wxdt.Format('%I:%M %p')
1078
1079 return strval
1080
1081
1082 def __validateValue( self, value ):
1083 """
1084 This function converts the value to a wxDateTime if not already one,
1085 does bounds checking and raises ValueError if argument is
1086 not a valid value for the control as currently specified.
1087 It is used by both the SetValue() and the IsValid() methods.
1088 """
1089 dbg('wxTimeCtrl::__validateValue(%s)' % repr(value), indent=1)
1090 if not value:
1091 dbg(indent=0)
1092 raise ValueError('%s not a valid time value' % repr(value))
1093
1094 valid = True # assume true
1095 try:
1096 value = self.GetWxDateTime(value) # regularize form; can generate ValueError if problem doing so
1097 except:
1098 dbg('exception occurred', indent=0)
1099 raise
1100
1101 if self.IsLimited() and not self.IsInBounds(value):
1102 dbg(indent=0)
1103 raise ValueError (
1104 'value %s is not within the bounds of the control' % str(value) )
1105 dbg(indent=0)
1106 return value
1107
1108#----------------------------------------------------------------------------
1109# Test jig for wxTimeCtrl:
1110
1111if __name__ == '__main__':
1112 import traceback
1113
1114 class TestPanel(wxPanel):
1115 def __init__(self, parent, id,
1116 pos = wxPyDefaultPosition, size = wxPyDefaultSize,
1117 fmt24hr = 0, test_mx = 0,
1118 style = wxTAB_TRAVERSAL ):
1119
1120 wxPanel.__init__(self, parent, id, pos, size, style)
1121
1122 self.test_mx = test_mx
1123
1124 self.tc = wxTimeCtrl(self, 10, fmt24hr = fmt24hr)
1125 sb = wxSpinButton( self, 20, wxDefaultPosition, wxSize(-1,20), 0 )
1126 self.tc.BindSpinButton(sb)
1127
1128 sizer = wxBoxSizer( wxHORIZONTAL )
1129 sizer.AddWindow( self.tc, 0, wxALIGN_CENTRE|wxLEFT|wxTOP|wxBOTTOM, 5 )
1130 sizer.AddWindow( sb, 0, wxALIGN_CENTRE|wxRIGHT|wxTOP|wxBOTTOM, 5 )
1131
1132 self.SetAutoLayout( True )
1133 self.SetSizer( sizer )
1134 sizer.Fit( self )
1135 sizer.SetSizeHints( self )
1136
1137 EVT_TIMEUPDATE(self, self.tc.GetId(), self.OnTimeChange)
1138
1139 def OnTimeChange(self, event):
1140 dbg('OnTimeChange: value = ', event.GetValue())
1141 wxdt = self.tc.GetWxDateTime()
1142 dbg('wxdt =', wxdt.GetHour(), wxdt.GetMinute(), wxdt.GetSecond())
1143 if self.test_mx:
1144 mxdt = self.tc.GetMxDateTime()
1145 dbg('mxdt =', mxdt.hour, mxdt.minute, mxdt.second)
1146
1147
1148 class MyApp(wxApp):
1149 def OnInit(self):
1150 import sys
1151 fmt24hr = '24' in sys.argv
1152 test_mx = 'mx' in sys.argv
1153 try:
1154 frame = wxFrame(NULL, -1, "wxTimeCtrl Test", wxPoint(20,20), wxSize(100,100) )
1155 panel = TestPanel(frame, -1, wxPoint(-1,-1), fmt24hr=fmt24hr, test_mx = test_mx)
1156 frame.Show(True)
1157 except:
1158 traceback.print_exc()
1159 return False
1160 return True
1161
1162 try:
1163 app = MyApp(0)
1164 app.MainLoop()
1165 except:
1166 traceback.print_exc()