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