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