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