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