]> git.saurik.com Git - wxWidgets.git/blame - wxPython/wx/lib/masked/timectrl.py
Undeprecated AddWindow, AddSizer, AddSpacer and etc.
[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)
368 if wxdt.Format('%p') != 'AM':
369 TimeCtrl.valid_ctrl_params['format'] = '24HHMMSS'
370 self.__fmt24hr = True
371 fmt24hr = True # force/change default positional argument
372 # (will countermand explicit set to False too.)
373
d4b73b1b 374 for key, param_value in TimeCtrl.valid_ctrl_params.items():
d14a1e28
RD
375 # This is done this way to make setattr behave consistently with
376 # "private attribute" name mangling
d4b73b1b 377 setattr(self, "_TimeCtrl__" + key, copy.copy(param_value))
d14a1e28
RD
378
379 # create locals from current defaults, so we can override if
380 # specified in kwargs, and handle uniformly:
381 min = self.__min
382 max = self.__max
383 limited = self.__limited
384 self.__posCurrent = 0
fffd96b7
RD
385 # handle deprecated keword argument name:
386 if kwargs.has_key('display_seconds'):
387 kwargs['displaySeconds'] = kwargs['display_seconds']
388 del kwargs['display_seconds']
389 if not kwargs.has_key('displaySeconds'):
390 kwargs['displaySeconds'] = True
391
392 # (handle positional arg (from original release) differently from rest of kwargs:)
fffd96b7
RD
393 if not kwargs.has_key('format'):
394 if fmt24hr:
395 if kwargs.has_key('displaySeconds') and kwargs['displaySeconds']:
396 kwargs['format'] = '24HHMMSS'
397 del kwargs['displaySeconds']
398 else:
399 kwargs['format'] = '24HHMM'
400 else:
401 if kwargs.has_key('displaySeconds') and kwargs['displaySeconds']:
402 kwargs['format'] = 'HHMMSS'
403 del kwargs['displaySeconds']
404 else:
405 kwargs['format'] = 'HHMM'
d14a1e28 406
fffd96b7
RD
407 if not kwargs.has_key('useFixedWidthFont'):
408 # allow control over font selection:
409 kwargs['useFixedWidthFont'] = self.__useFixedWidthFont
d14a1e28 410
fffd96b7 411 maskededit_kwargs = self.SetParameters(**kwargs)
d14a1e28 412
fffd96b7
RD
413 # allow for explicit size specification:
414 if size != wx.DefaultSize:
415 # override (and remove) "autofit" autoformat code in standard time formats:
416 maskededit_kwargs['formatcodes'] = 'T!'
d14a1e28
RD
417
418 # This allows range validation if set
419 maskededit_kwargs['validFunc'] = self.IsInBounds
420
421 # This allows range limits to affect insertion into control or not
422 # dynamically without affecting individual field constraint validation
423 maskededit_kwargs['retainFieldValidation'] = True
424
d14a1e28 425 # Now we can initialize the base control:
fffd96b7 426 BaseMaskedTextCtrl.__init__(
d14a1e28
RD
427 self, parent, id=id,
428 pos=pos, size=size,
429 style = style,
430 validator = validator,
431 name = name,
432 setupEventHandling = False,
433 **maskededit_kwargs)
434
435
436 # This makes ':' act like tab (after we fix each ':' key event to remove "shift")
437 self._SetKeyHandler(':', self._OnChangeField)
438
439
440 # This makes the up/down keys act like spin button controls:
b881fc78
RD
441 self._SetKeycodeHandler(wx.WXK_UP, self.__OnSpinUp)
442 self._SetKeycodeHandler(wx.WXK_DOWN, self.__OnSpinDown)
d14a1e28
RD
443
444
445 # This allows ! and c/C to set the control to the current time:
446 self._SetKeyHandler('!', self.__OnSetToNow)
447 self._SetKeyHandler('c', self.__OnSetToNow)
448 self._SetKeyHandler('C', self.__OnSetToNow)
449
450
451 # Set up event handling ourselves, so we can insert special
452 # processing on the ":' key to remove the "shift" attribute
453 # *before* the default handlers have been installed, so
454 # that : takes you forward, not back, and so we can issue
455 # EVT_TIMEUPDATE events on changes:
456
b881fc78
RD
457 self.Bind(wx.EVT_SET_FOCUS, self._OnFocus ) ## defeat automatic full selection
458 self.Bind(wx.EVT_KILL_FOCUS, self._OnKillFocus ) ## run internal validator
459 self.Bind(wx.EVT_LEFT_UP, self.__LimitSelection) ## limit selections to single field
460 self.Bind(wx.EVT_LEFT_DCLICK, self._OnDoubleClick ) ## select field under cursor on dclick
461 self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown ) ## capture control events not normally seen, eg ctrl-tab.
462 self.Bind(wx.EVT_CHAR, self.__OnChar ) ## remove "shift" attribute from colon key event,
fffd96b7 463 ## then call BaseMaskedTextCtrl._OnChar with
b881fc78
RD
464 ## the possibly modified event.
465 self.Bind(wx.EVT_TEXT, self.__OnTextChange, self ) ## color control appropriately and EVT_TIMEUPDATE events
d14a1e28
RD
466
467
468 # Validate initial value and set if appropriate
469 try:
470 self.SetBounds(min, max)
471 self.SetLimited(limited)
472 self.SetValue(value)
473 except:
339983ff 474 self.SetValue('00:00:00')
d14a1e28
RD
475
476 if spinButton:
477 self.BindSpinButton(spinButton) # bind spin button up/down events to this control
478
479
fffd96b7 480 def SetParameters(self, **kwargs):
f54a36bb
RD
481 """
482 Function providing access to the parameters governing TimeCtrl display and bounds.
483 """
c878ceea 484## dbg('TimeCtrl::SetParameters(%s)' % repr(kwargs), indent=1)
fffd96b7
RD
485 maskededit_kwargs = {}
486 reset_format = False
487
488 if kwargs.has_key('display_seconds'):
489 kwargs['displaySeconds'] = kwargs['display_seconds']
490 del kwargs['display_seconds']
491 if kwargs.has_key('format') and kwargs.has_key('displaySeconds'):
492 del kwargs['displaySeconds'] # always apply format if specified
493
494 # assign keyword args as appropriate:
495 for key, param_value in kwargs.items():
496 if key not in TimeCtrl.valid_ctrl_params.keys():
497 raise AttributeError('invalid keyword argument "%s"' % key)
498
499 if key == 'format':
339983ff
RD
500 wxdt = wx.DateTimeFromDMY(1, 0, 1970)
501 if wxdt.Format('%p') != 'AM':
502 require24hr = True
503 else:
504 require24hr = False
505
fffd96b7
RD
506 # handle both local or generic 'maskededit' autoformat codes:
507 if param_value == 'HHMMSS' or param_value == 'TIMEHHMMSS':
508 self.__displaySeconds = True
509 self.__fmt24hr = False
510 elif param_value == 'HHMM' or param_value == 'TIMEHHMM':
511 self.__displaySeconds = False
512 self.__fmt24hr = False
513 elif param_value == '24HHMMSS' or param_value == '24HRTIMEHHMMSS':
514 self.__displaySeconds = True
515 self.__fmt24hr = True
516 elif param_value == '24HHMM' or param_value == '24HRTIMEHHMM':
517 self.__displaySeconds = False
518 self.__fmt24hr = True
519 else:
520 raise AttributeError('"%s" is not a valid format' % param_value)
339983ff
RD
521
522 if require24hr and not self.__fmt24hr:
523 raise AttributeError('"%s" is an unsupported time format for the current locale' % param_value)
524
fffd96b7
RD
525 reset_format = True
526
527 elif key in ("displaySeconds", "display_seconds") and not kwargs.has_key('format'):
528 self.__displaySeconds = param_value
529 reset_format = True
530
531 elif key == "min": min = param_value
532 elif key == "max": max = param_value
533 elif key == "limited": limited = param_value
534
535 elif key == "useFixedWidthFont":
536 maskededit_kwargs[key] = param_value
537
538 elif key == "oob_color":
539 maskededit_kwargs['invalidBackgroundColor'] = param_value
540
541 if reset_format:
542 if self.__fmt24hr:
543 if self.__displaySeconds: maskededit_kwargs['autoformat'] = '24HRTIMEHHMMSS'
544 else: maskededit_kwargs['autoformat'] = '24HRTIMEHHMM'
545
546 # Set hour field to zero-pad, right-insert, require explicit field change,
547 # select entire field on entry, and require a resultant valid entry
548 # to allow character entry:
549 hourfield = Field(formatcodes='0r<SV', validRegex='0\d|1\d|2[0123]', validRequired=True)
550 else:
551 if self.__displaySeconds: maskededit_kwargs['autoformat'] = 'TIMEHHMMSS'
552 else: maskededit_kwargs['autoformat'] = 'TIMEHHMM'
553
554 # Set hour field to allow spaces (at start), right-insert,
555 # require explicit field change, select entire field on entry,
556 # and require a resultant valid entry to allow character entry:
557 hourfield = Field(formatcodes='_0<rSV', validRegex='0[1-9]| [1-9]|1[012]', validRequired=True)
558 ampmfield = Field(formatcodes='S', emptyInvalid = True, validRequired = True)
559
560 # Field 1 is always a zero-padded right-insert minute field,
561 # similarly configured as above:
562 minutefield = Field(formatcodes='0r<SV', validRegex='[0-5]\d', validRequired=True)
563
564 fields = [ hourfield, minutefield ]
565 if self.__displaySeconds:
566 fields.append(copy.copy(minutefield)) # second field has same constraints as field 1
567
568 if not self.__fmt24hr:
569 fields.append(ampmfield)
570
571 # set fields argument:
572 maskededit_kwargs['fields'] = fields
573
574 # This allows range validation if set
575 maskededit_kwargs['validFunc'] = self.IsInBounds
576
577 # This allows range limits to affect insertion into control or not
578 # dynamically without affecting individual field constraint validation
579 maskededit_kwargs['retainFieldValidation'] = True
580
581 if hasattr(self, 'controlInitialized') and self.controlInitialized:
582 self.SetCtrlParameters(**maskededit_kwargs) # set appropriate parameters
583
584 # Validate initial value and set if appropriate
585 try:
586 self.SetBounds(min, max)
587 self.SetLimited(limited)
588 self.SetValue(value)
589 except:
339983ff 590 self.SetValue('00:00:00')
c878ceea 591## dbg(indent=0)
fffd96b7
RD
592 return {} # no arguments to return
593 else:
c878ceea 594## dbg(indent=0)
fffd96b7
RD
595 return maskededit_kwargs
596
d14a1e28
RD
597
598 def BindSpinButton(self, sb):
599 """
600 This function binds an externally created spin button to the control, so that
601 up/down events from the button automatically change the control.
602 """
c878ceea 603## dbg('TimeCtrl::BindSpinButton')
d14a1e28
RD
604 self.__spinButton = sb
605 if self.__spinButton:
606 # bind event handlers to spin ctrl
b881fc78
RD
607 self.__spinButton.Bind(wx.EVT_SPIN_UP, self.__OnSpinUp, self.__spinButton)
608 self.__spinButton.Bind(wx.EVT_SPIN_DOWN, self.__OnSpinDown, self.__spinButton)
d14a1e28
RD
609
610
611 def __repr__(self):
d4b73b1b 612 return "<TimeCtrl: %s>" % self.GetValue()
d14a1e28
RD
613
614
615 def SetValue(self, value):
616 """
617 Validating SetValue function for time values:
618 This function will do dynamic type checking on the value argument,
619 and convert wxDateTime, mxDateTime, or 12/24 format time string
620 into the appropriate format string for the control.
621 """
c878ceea 622## dbg('TimeCtrl::SetValue(%s)' % repr(value), indent=1)
d14a1e28
RD
623 try:
624 strtime = self._toGUI(self.__validateValue(value))
625 except:
c878ceea 626## dbg('validation failed', indent=0)
d14a1e28
RD
627 raise
628
c878ceea 629## dbg('strtime:', strtime)
d14a1e28 630 self._SetValue(strtime)
c878ceea 631## dbg(indent=0)
d14a1e28
RD
632
633 def GetValue(self,
634 as_wxDateTime = False,
635 as_mxDateTime = False,
636 as_wxTimeSpan = False,
637 as_mxDateTimeDelta = False):
f54a36bb
RD
638 """
639 This function returns the value of the display as a string by default, but
640 supports return as a wx.DateTime, mx.DateTime, wx.TimeSpan, or mx.DateTimeDelta,
641 if requested. (Evaluated in the order above-- first one wins!)
642 """
d14a1e28
RD
643
644
645 if as_wxDateTime or as_mxDateTime or as_wxTimeSpan or as_mxDateTimeDelta:
646 value = self.GetWxDateTime()
647 if as_wxDateTime:
648 pass
649 elif as_mxDateTime:
650 value = DateTime.DateTime(1970, 1, 1, value.GetHour(), value.GetMinute(), value.GetSecond())
651 elif as_wxTimeSpan:
b881fc78 652 value = wx.TimeSpan(value.GetHour(), value.GetMinute(), value.GetSecond())
d14a1e28
RD
653 elif as_mxDateTimeDelta:
654 value = DateTime.DateTimeDelta(0, value.GetHour(), value.GetMinute(), value.GetSecond())
655 else:
fffd96b7 656 value = BaseMaskedTextCtrl.GetValue(self)
d14a1e28
RD
657 return value
658
659
660 def SetWxDateTime(self, wxdt):
661 """
f54a36bb 662 Because SetValue can take a wx.DateTime, this is now just an alias.
d14a1e28
RD
663 """
664 self.SetValue(wxdt)
665
666
667 def GetWxDateTime(self, value=None):
668 """
d4b73b1b 669 This function is the conversion engine for TimeCtrl; it takes
d14a1e28
RD
670 one of the following types:
671 time string
672 wxDateTime
673 wxTimeSpan
674 mxDateTime
675 mxDateTimeDelta
f54a36bb 676 and converts it to a wx.DateTime that always has Jan 1, 1970 as its date
d14a1e28 677 portion, so that range comparisons around values can work using
f54a36bb 678 wx.DateTime's built-in comparison function. If a value is not
d14a1e28
RD
679 provided to convert, the string value of the control will be used.
680 If the value is not one of the accepted types, a ValueError will be
681 raised.
682 """
683 global accept_mx
c878ceea
RD
684## dbg(suspend=1)
685## dbg('TimeCtrl::GetWxDateTime(%s)' % repr(value), indent=1)
d14a1e28 686 if value is None:
c878ceea 687## dbg('getting control value')
d14a1e28 688 value = self.GetValue()
c878ceea 689## dbg('value = "%s"' % value)
d14a1e28
RD
690
691 if type(value) == types.UnicodeType:
692 value = str(value) # convert to regular string
693
694 valid = True # assume true
695 if type(value) == types.StringType:
696
697 # Construct constant wxDateTime, then try to parse the string:
b881fc78 698 wxdt = wx.DateTimeFromDMY(1, 0, 1970)
c878ceea 699## dbg('attempting conversion')
d14a1e28
RD
700 value = value.strip() # (parser doesn't like leading spaces)
701 checkTime = wxdt.ParseTime(value)
702 valid = checkTime == len(value) # entire string parsed?
c878ceea 703## dbg('checkTime == len(value)?', valid)
d14a1e28
RD
704
705 if not valid:
339983ff
RD
706 # deal with bug/deficiency in wx.DateTime:
707 if wxdt.Format('%p') not in ('AM', 'PM') and checkTime in (5,8):
708 # couldn't parse the AM/PM field
709 raise ValueError('cannot convert string "%s" to valid time for the current locale; please use 24hr time instead' % value)
710 else:
c878ceea 711## dbg(indent=0, suspend=0)
339983ff 712 raise ValueError('cannot convert string "%s" to valid time' % value)
d14a1e28
RD
713
714 else:
b881fc78 715 if isinstance(value, wx.DateTime):
d14a1e28 716 hour, minute, second = value.GetHour(), value.GetMinute(), value.GetSecond()
b881fc78 717 elif isinstance(value, wx.TimeSpan):
d14a1e28
RD
718 totalseconds = value.GetSeconds()
719 hour = totalseconds / 3600
720 minute = totalseconds / 60 - (hour * 60)
721 second = totalseconds - ((hour * 3600) + (minute * 60))
722
723 elif accept_mx and isinstance(value, DateTime.DateTimeType):
724 hour, minute, second = value.hour, value.minute, value.second
725 elif accept_mx and isinstance(value, DateTime.DateTimeDeltaType):
726 hour, minute, second = value.hour, value.minute, value.second
727 else:
728 # Not a valid function argument
729 if accept_mx:
730 error = 'GetWxDateTime requires wxDateTime, mxDateTime or parsable time string, passed %s'% repr(value)
731 else:
732 error = 'GetWxDateTime requires wxDateTime or parsable time string, passed %s'% repr(value)
c878ceea 733## dbg(indent=0, suspend=0)
d14a1e28
RD
734 raise ValueError(error)
735
b881fc78 736 wxdt = wx.DateTimeFromDMY(1, 0, 1970)
d14a1e28
RD
737 wxdt.SetHour(hour)
738 wxdt.SetMinute(minute)
739 wxdt.SetSecond(second)
740
c878ceea 741## dbg('wxdt:', wxdt, indent=0, suspend=0)
d14a1e28
RD
742 return wxdt
743
744
745 def SetMxDateTime(self, mxdt):
746 """
f54a36bb 747 Because SetValue can take an mx.DateTime, (if DateTime is importable),
d14a1e28
RD
748 this is now just an alias.
749 """
750 self.SetValue(value)
751
752
753 def GetMxDateTime(self, value=None):
f54a36bb
RD
754 """
755 Returns the value of the control as an mx.DateTime, with the date
756 portion set to January 1, 1970.
757 """
d14a1e28
RD
758 if value is None:
759 t = self.GetValue(as_mxDateTime=True)
760 else:
761 # Convert string 1st to wxDateTime, then use components, since
762 # mx' DateTime.Parser.TimeFromString() doesn't handle AM/PM:
763 wxdt = self.GetWxDateTime(value)
764 hour, minute, second = wxdt.GetHour(), wxdt.GetMinute(), wxdt.GetSecond()
765 t = DateTime.DateTime(1970,1,1) + DateTimeDelta(0, hour, minute, second)
766 return t
767
768
769 def SetMin(self, min=None):
770 """
771 Sets the minimum value of the control. If a value of None
772 is provided, then the control will have no explicit minimum value.
773 If the value specified is greater than the current maximum value,
774 then the function returns 0 and the minimum will not change from
775 its current setting. On success, the function returns 1.
776
777 If successful and the current value is lower than the new lower
778 bound, if the control is limited, the value will be automatically
779 adjusted to the new minimum value; if not limited, the value in the
780 control will be colored as invalid.
781 """
c878ceea 782## dbg('TimeCtrl::SetMin(%s)'% repr(min), indent=1)
d14a1e28
RD
783 if min is not None:
784 try:
785 min = self.GetWxDateTime(min)
786 self.__min = self._toGUI(min)
787 except:
c878ceea 788## dbg('exception occurred', indent=0)
d14a1e28
RD
789 return False
790 else:
791 self.__min = min
792
793 if self.IsLimited() and not self.IsInBounds():
794 self.SetLimited(self.__limited) # force limited value:
795 else:
796 self._CheckValid()
797 ret = True
c878ceea 798## dbg('ret:', ret, indent=0)
d14a1e28
RD
799 return ret
800
801
802 def GetMin(self, as_string = False):
803 """
804 Gets the minimum value of the control.
805 If None, it will return None. Otherwise it will return
806 the current minimum bound on the control, as a wxDateTime
807 by default, or as a string if as_string argument is True.
808 """
c878ceea
RD
809## dbg(suspend=1)
810## dbg('TimeCtrl::GetMin, as_string?', as_string, indent=1)
d14a1e28 811 if self.__min is None:
c878ceea 812## dbg('(min == None)')
d14a1e28
RD
813 ret = self.__min
814 elif as_string:
815 ret = self.__min
c878ceea 816## dbg('ret:', ret)
d14a1e28
RD
817 else:
818 try:
819 ret = self.GetWxDateTime(self.__min)
820 except:
c878ceea
RD
821## dbg(suspend=0)
822## dbg('exception occurred', indent=0)
823 raise
824## dbg('ret:', repr(ret))
825## dbg(indent=0, suspend=0)
d14a1e28
RD
826 return ret
827
828
829 def SetMax(self, max=None):
830 """
831 Sets the maximum value of the control. If a value of None
832 is provided, then the control will have no explicit maximum value.
833 If the value specified is less than the current minimum value, then
834 the function returns False and the maximum will not change from its
835 current setting. On success, the function returns True.
836
837 If successful and the current value is greater than the new upper
838 bound, if the control is limited the value will be automatically
839 adjusted to this maximum value; if not limited, the value in the
840 control will be colored as invalid.
841 """
c878ceea 842## dbg('TimeCtrl::SetMax(%s)' % repr(max), indent=1)
d14a1e28
RD
843 if max is not None:
844 try:
845 max = self.GetWxDateTime(max)
846 self.__max = self._toGUI(max)
847 except:
c878ceea 848## dbg('exception occurred', indent=0)
d14a1e28
RD
849 return False
850 else:
851 self.__max = max
c878ceea 852## dbg('max:', repr(self.__max))
d14a1e28
RD
853 if self.IsLimited() and not self.IsInBounds():
854 self.SetLimited(self.__limited) # force limited value:
855 else:
856 self._CheckValid()
857 ret = True
c878ceea 858## dbg('ret:', ret, indent=0)
d14a1e28
RD
859 return ret
860
861
862 def GetMax(self, as_string = False):
863 """
864 Gets the minimum value of the control.
865 If None, it will return None. Otherwise it will return
866 the current minimum bound on the control, as a wxDateTime
867 by default, or as a string if as_string argument is True.
868 """
c878ceea
RD
869## dbg(suspend=1)
870## dbg('TimeCtrl::GetMin, as_string?', as_string, indent=1)
d14a1e28 871 if self.__max is None:
c878ceea 872## dbg('(max == None)')
d14a1e28
RD
873 ret = self.__max
874 elif as_string:
875 ret = self.__max
c878ceea 876## dbg('ret:', ret)
d14a1e28
RD
877 else:
878 try:
879 ret = self.GetWxDateTime(self.__max)
880 except:
c878ceea
RD
881## dbg(suspend=0)
882## dbg('exception occurred', indent=0)
d14a1e28 883 raise
c878ceea
RD
884## dbg('ret:', repr(ret))
885## dbg(indent=0, suspend=0)
d14a1e28
RD
886 return ret
887
888
889 def SetBounds(self, min=None, max=None):
890 """
891 This function is a convenience function for setting the min and max
892 values at the same time. The function only applies the maximum bound
893 if setting the minimum bound is successful, and returns True
894 only if both operations succeed.
f54a36bb 895 **NOTE:** leaving out an argument will remove the corresponding bound.
d14a1e28
RD
896 """
897 ret = self.SetMin(min)
898 return ret and self.SetMax(max)
899
900
901 def GetBounds(self, as_string = False):
902 """
903 This function returns a two-tuple (min,max), indicating the
904 current bounds of the control. Each value can be None if
905 that bound is not set.
906 """
907 return (self.GetMin(as_string), self.GetMax(as_string))
908
909
910 def SetLimited(self, limited):
911 """
912 If called with a value of True, this function will cause the control
913 to limit the value to fall within the bounds currently specified.
914 If the control's value currently exceeds the bounds, it will then
915 be limited accordingly.
916
917 If called with a value of 0, this function will disable value
918 limiting, but coloring of out-of-bounds values will still take
919 place if bounds have been set for the control.
920 """
c878ceea 921## dbg('TimeCtrl::SetLimited(%d)' % limited, indent=1)
d14a1e28
RD
922 self.__limited = limited
923
924 if not limited:
925 self.SetMaskParameters(validRequired = False)
926 self._CheckValid()
c878ceea 927## dbg(indent=0)
d14a1e28
RD
928 return
929
c878ceea 930## dbg('requiring valid value')
d14a1e28
RD
931 self.SetMaskParameters(validRequired = True)
932
933 min = self.GetMin()
934 max = self.GetMax()
935 if min is None or max is None:
c878ceea 936## dbg('both bounds not set; no further action taken')
d14a1e28
RD
937 return # can't limit without 2 bounds
938
939 elif not self.IsInBounds():
940 # set value to the nearest bound:
941 try:
942 value = self.GetWxDateTime()
943 except:
c878ceea 944## dbg('exception occurred', indent=0)
d14a1e28
RD
945 raise
946
947 if min <= max: # valid range doesn't span midnight
c878ceea 948## dbg('min <= max')
d14a1e28
RD
949 # which makes the "nearest bound" computation trickier...
950
951 # determine how long the "invalid" pie wedge is, and cut
952 # this interval in half for comparison purposes:
953
954 # Note: relies on min and max and value date portions
955 # always being the same.
b881fc78 956 interval = (min + wx.TimeSpan(24, 0, 0, 0)) - max
d14a1e28 957
b881fc78 958 half_interval = wx.TimeSpan(
d14a1e28
RD
959 0, # hours
960 0, # minutes
961 interval.GetSeconds() / 2, # seconds
962 0) # msec
963
964 if value < min: # min is on next day, so use value on
965 # "next day" for "nearest" interval calculation:
b881fc78 966 cmp_value = value + wx.TimeSpan(24, 0, 0, 0)
d14a1e28
RD
967 else: # "before midnight; ok
968 cmp_value = value
969
970 if (cmp_value - max) > half_interval:
c878ceea 971## dbg('forcing value to min (%s)' % min.FormatTime())
d14a1e28
RD
972 self.SetValue(min)
973 else:
c878ceea 974## dbg('forcing value to max (%s)' % max.FormatTime())
d14a1e28
RD
975 self.SetValue(max)
976 else:
c878ceea 977## dbg('max < min')
d14a1e28
RD
978 # therefore max < value < min guaranteed to be true,
979 # so "nearest bound" calculation is much easier:
980 if (value - max) >= (min - value):
981 # current value closer to min; pick that edge of pie wedge
c878ceea 982## dbg('forcing value to min (%s)' % min.FormatTime())
d14a1e28
RD
983 self.SetValue(min)
984 else:
c878ceea 985## dbg('forcing value to max (%s)' % max.FormatTime())
d14a1e28
RD
986 self.SetValue(max)
987
c878ceea 988## dbg(indent=0)
d14a1e28
RD
989
990
991
992 def IsLimited(self):
993 """
994 Returns True if the control is currently limiting the
f54a36bb 995 value to fall within any current bounds. *Note:* can
d14a1e28
RD
996 be set even if there are no current bounds.
997 """
998 return self.__limited
999
1000
1001 def IsInBounds(self, value=None):
1002 """
1003 Returns True if no value is specified and the current value
1004 of the control falls within the current bounds. As the clock
1005 is a "circle", both minimum and maximum bounds must be set for
1006 a value to ever be considered "out of bounds". This function can
1007 also be called with a value to see if that value would fall within
1008 the current bounds of the given control.
1009 """
1010 if value is not None:
1011 try:
1012 value = self.GetWxDateTime(value) # try to regularize passed value
1013 except ValueError:
c878ceea 1014## dbg('ValueError getting wxDateTime for %s' % repr(value), indent=0)
d14a1e28
RD
1015 raise
1016
c878ceea 1017## dbg('TimeCtrl::IsInBounds(%s)' % repr(value), indent=1)
d14a1e28 1018 if self.__min is None or self.__max is None:
c878ceea 1019## dbg(indent=0)
d14a1e28
RD
1020 return True
1021
1022 elif value is None:
1023 try:
1024 value = self.GetWxDateTime()
1025 except:
c878ceea
RD
1026## dbg('exception occurred', indent=0)
1027 raise
d14a1e28 1028
c878ceea 1029## dbg('value:', value.FormatTime())
d14a1e28
RD
1030
1031 # Get wxDateTime representations of bounds:
1032 min = self.GetMin()
1033 max = self.GetMax()
1034
b881fc78 1035 midnight = wx.DateTimeFromDMY(1, 0, 1970)
d14a1e28
RD
1036 if min <= max: # they don't span midnight
1037 ret = min <= value <= max
1038
1039 else:
1040 # have to break into 2 tests; to be in bounds
1041 # either "min" <= value (<= midnight of *next day*)
1042 # or midnight <= value <= "max"
1043 ret = min <= value or (midnight <= value <= max)
c878ceea 1044## dbg('in bounds?', ret, indent=0)
d14a1e28
RD
1045 return ret
1046
1047
1048 def IsValid( self, value ):
1049 """
1050 Can be used to determine if a given value would be a legal and
1051 in-bounds value for the control.
1052 """
1053 try:
1054 self.__validateValue(value)
1055 return True
1056 except ValueError:
1057 return False
1058
fffd96b7
RD
1059 def SetFormat(self, format):
1060 self.SetParameters(format=format)
1061
1062 def GetFormat(self):
1063 if self.__displaySeconds:
1064 if self.__fmt24hr: return '24HHMMSS'
1065 else: return 'HHMMSS'
1066 else:
1067 if self.__fmt24hr: return '24HHMM'
1068 else: return 'HHMM'
d14a1e28
RD
1069
1070#-------------------------------------------------------------------------------------------------------------
1071# these are private functions and overrides:
1072
1073
1074 def __OnTextChange(self, event=None):
c878ceea 1075## dbg('TimeCtrl::OnTextChange', indent=1)
d14a1e28 1076
fffd96b7 1077 # Allow Maskedtext base control to color as appropriate,
d14a1e28
RD
1078 # and Skip the EVT_TEXT event (if appropriate.)
1079 ##! WS: For some inexplicable reason, every wxTextCtrl.SetValue()
1080 ## call is generating two (2) EVT_TEXT events. (!)
1081 ## The the only mechanism I can find to mask this problem is to
1082 ## keep track of last value seen, and declare a valid EVT_TEXT
1083 ## event iff the value has actually changed. The masked edit
1084 ## OnTextChange routine does this, and returns True on a valid event,
1085 ## False otherwise.
fffd96b7 1086 if not BaseMaskedTextCtrl._OnTextChange(self, event):
d14a1e28
RD
1087 return
1088
c878ceea 1089## dbg('firing TimeUpdatedEvent...')
d14a1e28
RD
1090 evt = TimeUpdatedEvent(self.GetId(), self.GetValue())
1091 evt.SetEventObject(self)
1092 self.GetEventHandler().ProcessEvent(evt)
c878ceea 1093## dbg(indent=0)
d14a1e28
RD
1094
1095
1096 def SetInsertionPoint(self, pos):
1097 """
f54a36bb
RD
1098 This override records the specified position and associated cell before
1099 calling base class' function. This is necessary to handle the optional
1100 spin button, because the insertion point is lost when the focus shifts
1101 to the spin button.
d14a1e28 1102 """
c878ceea 1103## dbg('TimeCtrl::SetInsertionPoint', pos, indent=1)
fffd96b7 1104 BaseMaskedTextCtrl.SetInsertionPoint(self, pos) # (causes EVT_TEXT event to fire)
d14a1e28 1105 self.__posCurrent = self.GetInsertionPoint()
c878ceea 1106## dbg(indent=0)
d14a1e28
RD
1107
1108
1109 def SetSelection(self, sel_start, sel_to):
c878ceea 1110## dbg('TimeCtrl::SetSelection', sel_start, sel_to, indent=1)
d14a1e28
RD
1111
1112 # Adjust selection range to legal extent if not already
1113 if sel_start < 0:
1114 sel_start = 0
1115
1116 if self.__posCurrent != sel_start: # force selection and insertion point to match
1117 self.SetInsertionPoint(sel_start)
1118 cell_start, cell_end = self._FindField(sel_start)._extent
1119 if not cell_start <= sel_to <= cell_end:
1120 sel_to = cell_end
1121
1122 self.__bSelection = sel_start != sel_to
fffd96b7 1123 BaseMaskedTextCtrl.SetSelection(self, sel_start, sel_to)
c878ceea 1124## dbg(indent=0)
d14a1e28
RD
1125
1126
1127 def __OnSpin(self, key):
1128 """
1129 This is the function that gets called in response to up/down arrow or
1130 bound spin button events.
1131 """
1132 self.__IncrementValue(key, self.__posCurrent) # changes the value
1133
1134 # Ensure adjusted control regains focus and has adjusted portion
1135 # selected:
1136 self.SetFocus()
1137 start, end = self._FindField(self.__posCurrent)._extent
1138 self.SetInsertionPoint(start)
1139 self.SetSelection(start, end)
c878ceea 1140## dbg('current position:', self.__posCurrent)
d14a1e28
RD
1141
1142
1143 def __OnSpinUp(self, event):
1144 """
1145 Event handler for any bound spin button on EVT_SPIN_UP;
1146 causes control to behave as if up arrow was pressed.
1147 """
c878ceea 1148## dbg('TimeCtrl::OnSpinUp', indent=1)
2461468b 1149 self.__OnSpin(wx.WXK_UP)
d14a1e28 1150 keep_processing = False
c878ceea 1151## dbg(indent=0)
d14a1e28
RD
1152 return keep_processing
1153
1154
1155 def __OnSpinDown(self, event):
1156 """
1157 Event handler for any bound spin button on EVT_SPIN_DOWN;
1158 causes control to behave as if down arrow was pressed.
1159 """
c878ceea 1160## dbg('TimeCtrl::OnSpinDown', indent=1)
2461468b 1161 self.__OnSpin(wx.WXK_DOWN)
d14a1e28 1162 keep_processing = False
c878ceea 1163## dbg(indent=0)
d14a1e28
RD
1164 return keep_processing
1165
1166
1167 def __OnChar(self, event):
1168 """
1169 Handler to explicitly look for ':' keyevents, and if found,
1170 clear the m_shiftDown field, so it will behave as forward tab.
1171 It then calls the base control's _OnChar routine with the modified
1172 event instance.
1173 """
c878ceea 1174## dbg('TimeCtrl::OnChar', indent=1)
d14a1e28 1175 keycode = event.GetKeyCode()
c878ceea 1176## dbg('keycode:', keycode)
d14a1e28 1177 if keycode == ord(':'):
c878ceea 1178## dbg('colon seen! removing shift attribute')
d14a1e28 1179 event.m_shiftDown = False
fffd96b7 1180 BaseMaskedTextCtrl._OnChar(self, event ) ## handle each keypress
c878ceea 1181## dbg(indent=0)
d14a1e28
RD
1182
1183
1184 def __OnSetToNow(self, event):
1185 """
1186 This is the key handler for '!' and 'c'; this allows the user to
1187 quickly set the value of the control to the current time.
1188 """
b881fc78 1189 self.SetValue(wx.DateTime_Now().FormatTime())
d14a1e28
RD
1190 keep_processing = False
1191 return keep_processing
1192
1193
1194 def __LimitSelection(self, event):
1195 """
1196 Event handler for motion events; this handler
1197 changes limits the selection to the new cell boundaries.
1198 """
c878ceea 1199## dbg('TimeCtrl::LimitSelection', indent=1)
d14a1e28
RD
1200 pos = self.GetInsertionPoint()
1201 self.__posCurrent = pos
1202 sel_start, sel_to = self.GetSelection()
1203 selection = sel_start != sel_to
1204 if selection:
1205 # only allow selection to end of current cell:
1206 start, end = self._FindField(sel_start)._extent
1207 if sel_to < pos: sel_to = start
1208 elif sel_to > pos: sel_to = end
1209
c878ceea 1210## dbg('new pos =', self.__posCurrent, 'select to ', sel_to)
d14a1e28
RD
1211 self.SetInsertionPoint(self.__posCurrent)
1212 self.SetSelection(self.__posCurrent, sel_to)
1213 if event: event.Skip()
c878ceea 1214## dbg(indent=0)
d14a1e28
RD
1215
1216
1217 def __IncrementValue(self, key, pos):
c878ceea 1218## dbg('TimeCtrl::IncrementValue', key, pos, indent=1)
d14a1e28
RD
1219 text = self.GetValue()
1220 field = self._FindField(pos)
c878ceea 1221## dbg('field: ', field._index)
d14a1e28
RD
1222 start, end = field._extent
1223 slice = text[start:end]
b881fc78 1224 if key == wx.WXK_UP: increment = 1
d14a1e28
RD
1225 else: increment = -1
1226
1227 if slice in ('A', 'P'):
1228 if slice == 'A': newslice = 'P'
1229 elif slice == 'P': newslice = 'A'
1230 newvalue = text[:start] + newslice + text[end:]
1231
1232 elif field._index == 0:
1233 # adjusting this field is trickier, as its value can affect the
1234 # am/pm setting. So, we use wxDateTime to generate a new value for us:
1235 # (Use a fixed date not subject to DST variations:)
b881fc78 1236 converter = wx.DateTimeFromDMY(1, 0, 1970)
c878ceea 1237## dbg('text: "%s"' % text)
d14a1e28
RD
1238 converter.ParseTime(text.strip())
1239 currenthour = converter.GetHour()
c878ceea 1240## dbg('current hour:', currenthour)
d14a1e28 1241 newhour = (currenthour + increment) % 24
c878ceea 1242## dbg('newhour:', newhour)
d14a1e28 1243 converter.SetHour(newhour)
c878ceea 1244## dbg('converter.GetHour():', converter.GetHour())
d14a1e28
RD
1245 newvalue = converter # take advantage of auto-conversion for am/pm in .SetValue()
1246
1247 else: # minute or second field; handled the same way:
1248 newslice = "%02d" % ((int(slice) + increment) % 60)
1249 newvalue = text[:start] + newslice + text[end:]
1250
1251 try:
1252 self.SetValue(newvalue)
1253
1254 except ValueError: # must not be in bounds:
b881fc78
RD
1255 if not wx.Validator_IsSilent():
1256 wx.Bell()
c878ceea 1257## dbg(indent=0)
d14a1e28
RD
1258
1259
1260 def _toGUI( self, wxdt ):
1261 """
1262 This function takes a wxdt as an unambiguous representation of a time, and
1263 converts it to a string appropriate for the format of the control.
1264 """
1265 if self.__fmt24hr:
fffd96b7
RD
1266 if self.__displaySeconds: strval = wxdt.Format('%H:%M:%S')
1267 else: strval = wxdt.Format('%H:%M')
d14a1e28 1268 else:
fffd96b7
RD
1269 if self.__displaySeconds: strval = wxdt.Format('%I:%M:%S %p')
1270 else: strval = wxdt.Format('%I:%M %p')
d14a1e28
RD
1271
1272 return strval
1273
1274
1275 def __validateValue( self, value ):
1276 """
1277 This function converts the value to a wxDateTime if not already one,
1278 does bounds checking and raises ValueError if argument is
1279 not a valid value for the control as currently specified.
1280 It is used by both the SetValue() and the IsValid() methods.
1281 """
c878ceea 1282## dbg('TimeCtrl::__validateValue(%s)' % repr(value), indent=1)
d14a1e28 1283 if not value:
c878ceea 1284## dbg(indent=0)
d14a1e28
RD
1285 raise ValueError('%s not a valid time value' % repr(value))
1286
1287 valid = True # assume true
1288 try:
1289 value = self.GetWxDateTime(value) # regularize form; can generate ValueError if problem doing so
1290 except:
c878ceea 1291## dbg('exception occurred', indent=0)
d14a1e28
RD
1292 raise
1293
1294 if self.IsLimited() and not self.IsInBounds(value):
c878ceea 1295## dbg(indent=0)
d14a1e28
RD
1296 raise ValueError (
1297 'value %s is not within the bounds of the control' % str(value) )
c878ceea 1298## dbg(indent=0)
d14a1e28
RD
1299 return value
1300
1301#----------------------------------------------------------------------------
d4b73b1b 1302# Test jig for TimeCtrl:
d14a1e28
RD
1303
1304if __name__ == '__main__':
1305 import traceback
1306
b881fc78 1307 class TestPanel(wx.Panel):
d14a1e28 1308 def __init__(self, parent, id,
b881fc78 1309 pos = wx.DefaultPosition, size = wx.DefaultSize,
d14a1e28 1310 fmt24hr = 0, test_mx = 0,
b881fc78 1311 style = wx.TAB_TRAVERSAL ):
d14a1e28 1312
b881fc78 1313 wx.Panel.__init__(self, parent, id, pos, size, style)
d14a1e28
RD
1314
1315 self.test_mx = test_mx
1316
d4b73b1b 1317 self.tc = TimeCtrl(self, 10, fmt24hr = fmt24hr)
b881fc78 1318 sb = wx.SpinButton( self, 20, wx.DefaultPosition, (-1,20), 0 )
d14a1e28
RD
1319 self.tc.BindSpinButton(sb)
1320
b881fc78
RD
1321 sizer = wx.BoxSizer( wx.HORIZONTAL )
1322 sizer.Add( self.tc, 0, wx.ALIGN_CENTRE|wx.LEFT|wx.TOP|wx.BOTTOM, 5 )
1323 sizer.Add( sb, 0, wx.ALIGN_CENTRE|wx.RIGHT|wx.TOP|wx.BOTTOM, 5 )
d14a1e28
RD
1324
1325 self.SetAutoLayout( True )
1326 self.SetSizer( sizer )
1327 sizer.Fit( self )
1328 sizer.SetSizeHints( self )
1329
b881fc78 1330 self.Bind(EVT_TIMEUPDATE, self.OnTimeChange, self.tc)
d14a1e28
RD
1331
1332 def OnTimeChange(self, event):
c878ceea 1333## dbg('OnTimeChange: value = ', event.GetValue())
d14a1e28 1334 wxdt = self.tc.GetWxDateTime()
c878ceea 1335## dbg('wxdt =', wxdt.GetHour(), wxdt.GetMinute(), wxdt.GetSecond())
d14a1e28
RD
1336 if self.test_mx:
1337 mxdt = self.tc.GetMxDateTime()
c878ceea 1338## dbg('mxdt =', mxdt.hour, mxdt.minute, mxdt.second)
d14a1e28
RD
1339
1340
b881fc78 1341 class MyApp(wx.App):
d14a1e28
RD
1342 def OnInit(self):
1343 import sys
1344 fmt24hr = '24' in sys.argv
1345 test_mx = 'mx' in sys.argv
1346 try:
d4b73b1b 1347 frame = wx.Frame(None, -1, "TimeCtrl Test", (20,20), (100,100) )
b881fc78 1348 panel = TestPanel(frame, -1, (-1,-1), fmt24hr=fmt24hr, test_mx = test_mx)
d14a1e28
RD
1349 frame.Show(True)
1350 except:
1351 traceback.print_exc()
1352 return False
1353 return True
1354
1355 try:
1356 app = MyApp(0)
1357 app.MainLoop()
1358 except:
1359 traceback.print_exc()
f54a36bb
RD
1360__i=0
1361
1362## CHANGELOG:
1363## ====================
1364## Version 1.3
1365## 1. Converted docstrings to reST format, added doc for ePyDoc.
1366## 2. Renamed helper functions, vars etc. not intended to be visible in public
1367## interface to code.
1368##
fffd96b7
RD
1369## Version 1.2
1370## 1. Changed parameter name display_seconds to displaySeconds, to follow
1371## other masked edit conventions.
1372## 2. Added format parameter, to remove need to use both fmt24hr and displaySeconds.
1373## 3. Changed inheritance to use BaseMaskedTextCtrl, to remove exposure of
1374## nonsensical parameter methods from the control, so it will work
1375## properly with Boa.