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