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