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