diff --git a/wxPython/CHANGES.txt b/wxPython/CHANGES.txt
index 01c14d1fb8..d48e6a7950 100644
--- a/wxPython/CHANGES.txt
+++ b/wxPython/CHANGES.txt
@@ -176,7 +176,7 @@ example.
Added wxPython.lib.mixins.rubberband module from Robb Shecter.
-
+Added wxTimeCtrl from Will Sadkin.
diff --git a/wxPython/demo/Main.py b/wxPython/demo/Main.py
index 82736b72fe..51458681d7 100644
--- a/wxPython/demo/Main.py
+++ b/wxPython/demo/Main.py
@@ -37,6 +37,7 @@ _treeList = [
'wxKeyEvents',
'wxWizard',
'wxXmlResourceHandler',
+ 'wxTimeCtrl',
]),
# managed windows == things with a caption you can close
@@ -129,6 +130,7 @@ _treeList = [
'wxRightTextCtrl',
'wxStyledTextCtrl_1',
'wxStyledTextCtrl_2',
+ 'wxTimeCtrl',
]),
# How to lay out the controls in a frame/dialog
diff --git a/wxPython/demo/wxTimeCtrl.py b/wxPython/demo/wxTimeCtrl.py
new file mode 100644
index 0000000000..b824ff0586
--- /dev/null
+++ b/wxPython/demo/wxTimeCtrl.py
@@ -0,0 +1,117 @@
+from wxPython.wx import *
+from wxPython.lib.timectrl import *
+
+import string
+
+#----------------------------------------------------------------------
+
+class TestPanel(wxPanel):
+ def __init__(self, parent, log):
+ wxPanel.__init__(self, parent, -1)
+ self.log = log
+ panel = wxPanel(self, -1)
+ grid = wxFlexGridSizer( 0, 2, 20, 0 )
+
+ text1 = wxStaticText( panel, 10, "A 12-hour format wxTimeCtrl:", wxDefaultPosition, wxDefaultSize, 0 )
+ grid.AddWindow( text1, 0, wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL, 5 )
+
+ hsizer1 = wxBoxSizer( wxHORIZONTAL )
+ self.time12 = wxTimeCtrl(panel, 20, name="12 hour time")
+ hsizer1.AddWindow( self.time12, 0, wxALIGN_CENTRE, 5 )
+ spin1 = wxSpinButton( panel, 30, wxDefaultPosition, wxSize(-1,20), 0 )
+ self.time12.BindSpinButton(spin1)
+ hsizer1.AddWindow( spin1, 0, wxALIGN_CENTRE, 5 )
+ grid.AddSizer( hsizer1, 0, wxALIGN_CENTER_VERTICAL|wxLEFT, 5 )
+
+ text2 = wxStaticText( panel, 40, "A 24-hour format wxTimeCtrl:", wxDefaultPosition, wxDefaultSize, 0 )
+ grid.AddWindow( text2, 0, wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL|wxLEFT|wxTOP|wxBOTTOM, 5 )
+
+ hsizer2 = wxBoxSizer( wxHORIZONTAL )
+ self.time24 = wxTimeCtrl(panel, 50, fmt24hr=true, name="24 hour time")
+ hsizer2.AddWindow( self.time24, 0, wxALIGN_CENTRE, 5 )
+ spin2 = wxSpinButton( panel, 60, wxDefaultPosition, wxSize(-1,20), 0 )
+ self.time24.BindSpinButton(spin2)
+ hsizer2.AddWindow( spin2, 0, wxALIGN_CENTRE, 5 )
+ grid.AddSizer( hsizer2, 0, wxALIGN_CENTER_VERTICAL|wxLEFT, 5 )
+
+ panel.SetAutoLayout(true)
+ panel.SetSizer(grid)
+ grid.Fit(panel)
+ panel.Move((50,50))
+ self.panel = panel
+
+ EVT_TIMEUPDATE(self, self.time12.GetId(), self.OnTimeChange)
+ EVT_TIMEUPDATE(self, self.time24.GetId(), self.OnTimeChange)
+
+
+ def OnTimeChange(self, event):
+ timectrl = self.panel.FindWindowById(event.GetId())
+ self.log.write('%s = %s\n' % (
+ timectrl.GetName(), timectrl.GetValue() ) )
+
+
+#----------------------------------------------------------------------
+
+def runTest(frame, nb, log):
+ win = TestPanel(nb, log)
+ return win
+
+#----------------------------------------------------------------------
+
+# It's nice to be able to use HTML here, but line breaks in the triple quoted string
+# cause the resulting output to look funny, as they seem to be interpreted by the
+# parser...
+
+overview = """
+
+wxTimeCtrl provides a multi-cell control that allows manipulation of a time value. It supports 12 or 24 hour format, and you can use wxDateTime or mxDateTimet o get/set values from the control.
+
+Left/right/tab keys to switch cells within a wxTimeCtrl, and the up/down arrows act like a spin control. wxTimeCtrl also allows for an actual spin button to be attached to the control, so that it acts like the up/down arrow keys.
+
+Here's the interface for wxTimeCtrl:
+
+
+
+wxTimeCtrl(parent, id = -1,
+ value = '12:00:00 AM',
+ pos = wxDefaultPosition,
+ size = wxDefaultSize,
+ fmt24hr = false,
+ spinButton = None,
+ style = wxTE_PROCESS_TAB,
+ name = "time")
+
+
+ - value
- If no initial value is set, the default will be midnight; if an illegal string is specified, a ValueError will result. (You can always later set the initial time with SetValue() after instantiation of the control.)
+
size- The size of the control will be automatically adjusted for 12/24 hour format if wxDefaultSize is specified.
+
- fmt24hr
- If true, control will display time in 24 hour time format; if false, it will use 12 hour AM/PM format. SetValue() will adjust values accordingly for the control, based on the format specified.
+
- spinButton
- If specified, this button's events will be bound to the behavior of the wxTimeCtrl, working like up/down cursor key events. (See BindSpinButton.)
+
- style
- By default, wxTimeCtrl will process TAB events, by allowing tab to the different cells within the control.
+
+
+- SetValue(time_string)
- Sets the value of the control to a particular time, given a valid time string; raises ValueError on invalid value
+
- GetValue()
- Retrieves the string value of the time from the control
+
+ - SetWxDateTime(wxDateTime)
- Uses the time portion of a wxDateTime to construct a value for the control.
+
- GetWxDateTime()
- Retrieves the value of the control, and applies it to the wxDateTimeFromHMS() constructor, and returns the resulting value. (This returns the date portion as "today".)
+
+ - SetMxDateTime(mxDateTime)
- Uses the time portion of an mxDateTime to construct a value for the control. NOTE: This imports mx.DateTime at runtime only if this or the Get function is called.
+
- GetMxDateTime()
- Retrieves the value of the control and applies it to the DateTime.Time() constructor, and returns the resulting value. (mxDateTime is smart enough to know this is just a time value.)
+
+ - BindSpinButton(wxSpinBtton)
- Binds an externally created spin button to the control, so that up/down spin events change the active cell or selection in the control (in addition to the up/down cursor keys.) (This is primarily to allow you to create a "standard" interface to time controls, as seen in Windows.)
+
+ - EVT_TIMEUPDATE(win, id, func)
- func is fired whenever the value of the control changes.
+
+
+"""
+
+
+
+
+if __name__ == '__main__':
+ import sys,os
+ import run
+ run.main(['', os.path.basename(sys.argv[0])])
+
+
+
diff --git a/wxPython/wxPython/lib/timectrl.py b/wxPython/wxPython/lib/timectrl.py
new file mode 100644
index 0000000000..331bd93850
--- /dev/null
+++ b/wxPython/wxPython/lib/timectrl.py
@@ -0,0 +1,697 @@
+#----------------------------------------------------------------------------
+# Name: wxTimeCtrl.py
+# Author: Will Sadkin
+# Created: 09/19/2002
+# Copyright: (c) 2002 by Will Sadkin, 2002
+# License: wxWindows license
+#----------------------------------------------------------------------------
+# NOTE:
+# This was written way it is because of the lack of masked edit controls
+# in wxWindows/wxPython. I would also have preferred to derive this
+# control from a wxSpinCtrl rather than wxTextCtrl, but the wxTextCtrl
+# component of that control is inaccessible through the interface exposed in
+# wxPython.
+#
+# wxTimeCtrl does not use validators, because it does careful manipulation
+# of the cursor in the text window on each keystroke, and validation is
+# cursor-position specific, so the control intercepts the key codes before the
+# validator would fire.
+#
+
+from wxPython.wx import *
+import string
+
+# The following bit of function is for debugging the subsequent code.
+# To turn on debugging output, set _debug to 1
+_debug = 0
+_indent = 0
+
+def _dbg(*args, **kwargs):
+ global _indent
+
+ if _debug:
+ if len(args):
+ if _indent: print ' ' * 3 * _indent,
+ for arg in args: print arg,
+ print
+ # else do nothing
+
+ # post process args:
+ for kwarg, value in kwargs.items():
+ if kwarg == 'indent' and value: _indent = _indent + 1
+ elif kwarg == 'indent' and value == 0: _indent = _indent - 1
+ if _indent < 0: _indent = 0
+
+
+
+# This class of event fires whenever the value of the time changes in the control:
+wxEVT_TIMEVAL_UPDATED = wxNewId()
+def EVT_TIMEUPDATE(win, id, func):
+ """Used to trap events indicating that the current time has been changed."""
+ win.Connect(id, -1, wxEVT_TIMEVAL_UPDATED, func)
+
+class TimeUpdatedEvent(wxPyCommandEvent):
+ def __init__(self, id, value ='12:00:00 AM'):
+ wxPyCommandEvent.__init__(self, wxEVT_TIMEVAL_UPDATED, id)
+ self.value = value
+ def GetValue(self):
+ """Retrieve the value of the float control at the time this event was generated"""
+ return self.value
+
+
+
+class wxTimeCtrl(wxTextCtrl):
+ def __init__ (
+ self, parent, id=-1, value = '12:00:00 AM',
+ pos = wxDefaultPosition, size = wxDefaultSize,
+ fmt24hr=0,
+ spinButton = None,
+ style = wxTE_PROCESS_TAB, name = "time"
+ ):
+ self.__fmt24hr = fmt24hr
+ if size == wxDefaultSize:
+ # set appropriate default sizes depending on format:
+ if self.__fmt24hr: size = wxSize(52, -1)
+ else: size = wxSize(70, -1)
+
+ wxTextCtrl.__init__(self, parent, id, value='',
+ pos=pos, size=size, style=style, name=name)
+
+
+ # Set up all the positions of the cells in the wxTimeCtrl (once):
+ # Format of control is:
+ # hh:mm:ss xM
+ # 1
+ # positions: 01234567890
+
+ self.__listCells = ['hour', 'minute', 'second', 'am_pm']
+ self.__listCellRange = [(0,1,2), (3,4,5), (6,7,8), (9,10,11)]
+ self.__listDelimPos = [2,5,8]
+
+ # Create dictionary of cell ranges, indexed by name or position in the range:
+ self.__dictCellRange = {}
+ for i in range(4):
+ self.__dictCellRange[self.__listCells[i]] = self.__listCellRange[i]
+
+ for cell in self.__listCells:
+ for i in self.__dictCellRange[cell]:
+ self.__dictCellRange[i] = self.__dictCellRange[cell]
+
+
+ # Create lists of starting and ending positions for each range, and a dictionary of starting
+ # positions indexed by name
+ self.__listStartCellPos = []
+ self.__listEndCellPos = []
+ for tup in self.__listCellRange:
+ self.__listStartCellPos.append(tup[0]) # 1st char of cell
+ self.__listEndCellPos.append(tup[1]) # last char of cell (not including delimiter)
+
+ self.__dictStartCellPos = {}
+ for i in range(4):
+ self.__dictStartCellPos[self.__listCells[i]] = self.__listStartCellPos[i]
+
+ if self.__fmt24hr: self.__lastCell = 'second'
+ else: self.__lastCell = 'am_pm'
+
+ # Validate initial value and set if appropriate
+ try:
+ self.SetValue(value)
+ except ValueError:
+ self.SetValue('12:00:00 AM')
+
+ # set initial position and selection state
+ self.__SetCurrentCell(self.__dictStartCellPos['hour'])
+ self.__bSelection = false
+
+ EVT_TEXT(self, self.GetId(), self.__OnTextChange)
+ EVT_SET_FOCUS(self, self.__OnFocus)
+ EVT_CHAR(self, self.__OnChar)
+
+ if spinButton:
+ self.BindSpinbutton(spinButton) # bind spin button up/down events to this control
+
+
+ def SetValue(self, value):
+ """
+ Validating SetValue function for time strings, doing 12/24 format conversion as appropriate.
+ """
+ _dbg('wxTimeCtrl::SetValue', indent=1)
+ dict_range = self.__dictCellRange
+ dict_start = self.__dictStartCellPos
+
+ fmt12len = dict_range['am_pm'][-1]
+ fmt24len = dict_range['second'][-1]
+ try:
+ separators_correct = value[2] == ':' and value[5] == ':'
+ len_ok = len(value) in (fmt12len, fmt24len)
+
+ if len(value) > fmt24len:
+ separators_correct = separators_correct and value[8] == ' '
+ hour = int(value[dict_range['hour'][0]:dict_range['hour'][-1]])
+ hour_ok = ((hour in range(0,24) and len(value) == fmt24len)
+ or (hour in range(1,13) and len(value) == fmt12len
+ and value[dict_start['am_pm']:] in ('AM', 'PM')))
+
+ minute = int(value[dict_range['minute'][0]:dict_range['minute'][-1]])
+ min_ok = minute in range(60)
+ second = int(value[dict_range['second'][0]:dict_range['second'][-1]])
+ sec_ok = second in range(60)
+
+ _dbg('len_ok =', len_ok, 'separators_correct =', separators_correct)
+ _dbg('hour =', hour, 'hour_ok =', hour_ok, 'min_ok =', min_ok, 'sec_ok =', sec_ok)
+
+ if len_ok and hour_ok and min_ok and sec_ok and separators_correct:
+ _dbg('valid time string')
+
+
+ self.__hour = hour
+ if len(value) == fmt12len: # handle 12 hour format conversion for actual hour:
+ am = value[dict_start['am_pm']:] == 'AM'
+ if hour != 12 and not am:
+ self.__hour = hour = (hour+12) % 24
+ elif hour == 12:
+ if am: self.__hour = hour = 0
+
+ self.__minute = minute
+ self.__second = second
+
+ # valid time
+ need_to_convert = ((self.__fmt24hr and len(value) == fmt12len)
+ or (not self.__fmt24hr and len(value) == fmt24len))
+ _dbg('need_to_convert =', need_to_convert)
+
+ if need_to_convert: #convert to 12/24 hour format as specified:
+ dict_start = self.__dictStartCellPos
+ if self.__fmt24hr and len(value) == fmt12len:
+ text = '%.2d:%.2d:%.2d' % (hour, minute, second)
+ else:
+ if hour > 12:
+ hour = hour - 12
+ am_pm = 'PM'
+ elif hour == 12:
+ am_pm = 'PM'
+ else:
+ if hour == 0: hour = 12
+ am_pm = 'AM'
+ text = '%2d:%.2d:%.2d %s' % (hour, minute, second, am_pm)
+ else:
+ text = value
+ _dbg('text=', text)
+ wxTextCtrl.SetValue(self, text)
+ _dbg('firing TimeUpdatedEvent...')
+ self.GetEventHandler().ProcessEvent(TimeUpdatedEvent(self.GetId(), text))
+ else:
+ _dbg('len_ok:', len_ok, 'separators_correct =', separators_correct)
+ _dbg('hour_ok:', hour_ok, 'min_ok:', min_ok, 'sec_ok:', sec_ok, indent=0)
+ raise ValueError, 'value is not a valid time string'
+
+ except (TypeError, ValueError):
+ _dbg(indent=0)
+ raise ValueError, 'value is not a valid time string'
+ _dbg(indent=0)
+
+ def SetFromWxDateTime(self, wxdt):
+ value = '%2d:%.2d:%.2d' % (wxdt.GetHour(), wxdt.GetMinute(), wxdt.GetSecond())
+ self.SetValue(value)
+
+ def GetWxDateTime(self):
+ t = wxDateTimeFromHMS(self.__hour, self.__minute, self.__second)
+ return t
+
+ def SetMxDateTime(self, mxdt):
+ from mx import DateTime
+ value = '%2d:%.2d:%.2d' % (mxdt.hour, mxdt.minute, mxdt.second)
+ self.SetValue(value)
+
+ def GetMxDateTime(self):
+ from mx import DateTime
+ t = DateTime.Time(self.__hour, self.__minute, self.__second)
+ return t
+
+ def BindSpinButton(self, sb):
+ """
+ This function binds an externally created spin button to the control, so that
+ up/down events from the button automatically change the control.
+ """
+ _dbg('wxTimeCtrl::BindSpinButton')
+ self.__spinButton = sb
+ if self.__spinButton:
+ EVT_SPIN_UP(self.__spinButton, self.__spinButton.GetId(), self.__OnSpinUp) # bind event handler to spin ctrl
+ EVT_SPIN_DOWN(self.__spinButton, self.__spinButton.GetId(), self.__OnSpinDown) # bind event handler to spin ctrl
+
+
+#-------------------------------------------------------------------------------------------------------------
+# these are private functions and overrides:
+
+ def __SetCurrentCell(self, pos):
+ """
+ Sets state variables that indicate the current cell and position within the control.
+ """
+ self.__posCurrent = pos
+ self.__cellStart, self.__cellEnd = self.__dictCellRange[pos][0], self.__dictCellRange[pos][-1]
+
+ def SetInsertionPoint(self, pos):
+ """
+ Records the specified position and associated cell before calling base class' function.
+ """
+ self.__SetCurrentCell(pos)
+ wxTextCtrl.SetInsertionPoint(self, pos) # (causes EVT_TEXT event to fire)
+
+
+ def __OnTextChange(self, event):
+ """
+ This private event handler is required to retain the current position information of the cursor
+ after update to the underlying text control is done.
+ """
+ _dbg('wxTimeCtrl::OnTextChange', indent=1)
+ self.__SetCurrentCell(self.__posCurrent) # ensure cell range vars are set
+
+ # Note: must call self.SetSelection here to preserve insertion point cursor after update!
+ # (I don't know why, but this does the trick!)
+ if self.__bSelection:
+ _dbg('reselecting from ', self.__posCurrent, 'to', self.__posSelectTo)
+ self.SetSelection(self.__posCurrent, self.__posSelectTo)
+ else:
+ self.SetSelection(self.__posCurrent, self.__posCurrent)
+ event.Skip()
+ _dbg(indent=0)
+
+ def __OnFocus(self, event):
+ """
+ This internal event handler ensures legal setting of input cursor on (re)focus to the control.
+ """
+ _dbg('wxTimeCtrl::OnFocus; ctrl id=', event.GetId())
+ self.__SetCurrentCell(0)
+ self.__bSelection = false
+ self.__posSelectTo = self.__posCurrent
+ self.SetInsertionPoint(self.__posCurrent)
+ event.Skip()
+
+
+ def __OnSpinUp(self, event):
+ """
+ Event handler for any bound spin button on EVT_SPIN_UP;
+ causes control to behave as if up arrow was pressed.
+ """
+ _dbg('wxTimeCtrl::OnSpinUp')
+ pos = self.GetInsertionPoint()
+ self.IncrementValue(WXK_UP, pos)
+ self.SetInsertionPoint(pos)
+
+ def __OnSpinDown(self, event):
+ """
+ Event handler for any bound spin button on EVT_SPIN_DOWN;
+ causes control to behave as if down arrow was pressed.
+ """
+ _dbg('wxTimeCtrl::OnSpinDown')
+ pos = self.GetInsertionPoint()
+ self.IncrementValue(WXK_DOWN, pos)
+ self.SetInsertionPoint(pos)
+
+
+ def __OnChar(self, event):
+ """
+ This private event handler is the main control point for the wxTimeCtrl.
+ It governs whether input characters are accepted and if so, handles them
+ so as to provide appropriate cursor and selection behavior for the control.
+ """
+ _dbg('wxTimeCtrl::OnChar', indent=1)
+
+ # NOTE: Returning without calling event.Skip() eats the event before it
+ # gets to the text control...
+
+ key = event.GetKeyCode()
+ text = self.GetValue()
+ pos = self.GetInsertionPoint()
+ if pos != self.__posCurrent:
+ _dbg("insertion point has moved; resetting current cell")
+ self.__SetCurrentCell(pos)
+ self.SetSelection(self.__posCurrent, self.__posCurrent)
+
+ sel_start, sel_to = self.GetSelection()
+ selection = sel_start != sel_to
+ if not selection:
+ self.__bSelection = false # predict unselection of entire region
+
+ _dbg('keycode = ', key)
+ _dbg('pos = ', pos)
+
+ if key in (WXK_DELETE, WXK_BACK): # don't allow deletion
+ _dbg(indent=0)
+ return
+
+ elif key == WXK_TAB: # skip to next field if applicable:
+ _dbg('key == WXK_TAB')
+ dict_range = self.__dictCellRange
+ dict_start = self.__dictStartCellPos
+ if event.ShiftDown(): # tabbing backwords
+
+ ###(NOTE: doesn't work; wxTE_PROCESS_TAB doesn't appear to send us this event!)
+
+ _dbg('event.ShiftDown()')
+ if pos in dict_range['hour']: # already in 1st field
+ self.__SetCurrentCell(dict_start['hour']) # ensure we have our member vars set
+ event.Skip() #then do normal tab processing for the form
+
+ elif pos in dict_range['minute']: # skip to hours field
+ new_pos = dict_start['hour']
+ elif pos in dict_range['second']: # skip to minutes field
+ new_pos = dict_start['minute']
+ elif pos in dict_range['am_pm']: # skip to seconds field
+ new_pos = dict_start['second']
+
+ self.__bSelection = true
+ self.__posSelectTo = new_pos+2
+ self.SetInsertionPoint(new_pos) # force insert point to jump to next cell (swallowing TAB)
+
+ else:
+ if pos in dict_range[self.__lastCell]: # already in last field; ensure we have our members set
+ self.__SetCurrentCell(dict_start[self.__lastCell])
+ _dbg('tab in last cell')
+ event.Skip() # then do normal tab processing for the form
+ _dbg(indent=0)
+ return
+
+ if pos in dict_range['second']: # skip to AM/PM field (if not last cell)
+ new_pos = dict_start['am_pm']
+ elif pos in dict_range['minute']: # skip to seconds field
+ new_pos = dict_start['second']
+ elif pos in dict_range['hour']: # skip to minutes field
+ new_pos = dict_start['minute']
+
+ self.__bSelection = true
+ self.__posSelectTo = new_pos+2
+ self.SetInsertionPoint(new_pos) # force insert point to jump to next cell (swallowing TAB)
+
+ elif key == WXK_LEFT: # move left; set insertion point as appropriate:
+ _dbg('key == WXK_LEFT')
+ if event.ShiftDown(): # selecting a range...
+ _dbg('event.ShiftDown()')
+ if sel_to != pos:
+ if sel_to - 1 == pos: # allow unselection of position
+ self.__bSelection = false # predict unselection of entire region
+ event.Skip()
+ if pos in self.__listStartCellPos: # can't select pass delimiters
+ _dbg(indent=0)
+ return
+ elif pos in self.__listEndCellPos: # can't use normal selection, because position ends up
+ # at delimeter
+ _dbg('set selection from', pos-1, 'to', self.__posCurrent)
+ self.__bSelection = true
+ self.__posSelectTo = pos
+ self.__posCurrent = pos-1
+ self.SetSelection(self.__posCurrent, self.__posSelectTo)
+ _dbg(indent=0)
+ return
+ else: event.Skip() # allow selection
+
+ # else... not selecting
+ if selection:
+ _dbg('sel_start=', sel_start, 'sel_to=', sel_to, '(Clearing selection)')
+ self.SetSelection(pos,pos) # clear selection
+ self.__bSelection = false
+
+ if pos == 0: # let base ctrl handle left bound case
+ event.Skip()
+ elif pos in self.__listStartCellPos: # skip (left) OVER the colon/space:
+ self.SetInsertionPoint(pos-1) # (this causes a EVT_TEXT)
+ self.__SetCurrentCell(pos-2) # set resulting position as "current"
+ else:
+ self.__SetCurrentCell(pos-1) # record the new cell position after the event is finishedI
+ # and update spinbutton as necessary
+ event.Skip() # let base control handle event
+
+ elif key == WXK_RIGHT: # move right
+ _dbg('key == WXK_RIGHT')
+ if event.ShiftDown():
+ _dbg('event.ShiftDown()')
+ if sel_to in self.__listDelimPos: # can't select pass delimiters
+ _dbg(indent=0)
+ return
+ elif pos in self.__listEndCellPos: # can't use normal selection, because position ends up
+ # at delimeter
+ _dbg('set selection from', self.__posCurrent, 'to', pos+1)
+ self.__bSelection = true
+ self.__posSelectTo = pos+1
+ self.SetSelection(self.__posCurrent, self.__posSelectTo)
+ _dbg(indent=0)
+ return
+ else: event.Skip()
+
+ else:
+ if selection:
+ _dbg('sel_start=', sel_start, 'sel_to=', sel_to, '(Clearing selection)')
+ pos = sel_start
+ self.SetSelection(pos,pos) # clear selection
+ self.__bSelection = false
+ if pos == self.__dictStartCellPos[self.__lastCell]+1:
+ _dbg(indent=0)
+ return # don't allow cursor past last cell
+ if pos in self.__listEndCellPos: # skip (right) OVER the colon/space:
+ self.SetInsertionPoint(pos+1) # (this causes a EVT_TEXT)
+ self.__SetCurrentCell(pos+2) # set resulting position
+ else:
+ self.__SetCurrentCell(pos+1) # record the new cell position after the event is finished
+ self.__bSelection = false
+ event.Skip()
+
+ elif key in (WXK_UP, WXK_DOWN):
+ _dbg('key in (WXK_UP, WXK_DOWN)')
+ self.IncrementValue(key, pos) # increment/decrement as appropriate
+ self.SetInsertionPoint(pos)
+
+ elif key < WXK_SPACE or key == WXK_DELETE or key > 255:
+ event.Skip() # non alphanumeric; process normally (Right thing to do?)
+
+ elif chr(key) in string.digits: # let ChangeValue validate and update current position
+ self.ChangeValue(chr(key), pos) # handle event (and swallow it)
+
+ elif chr(key) in ('A', 'P', 'M', ' '): # let ChangeValue validate and update current position
+ self.ChangeValue(chr(key), pos) # handle event (and swallow it)
+
+ else: # disallowed char; swallow event
+ _dbg(indent=0)
+ return
+ _dbg(indent=0)
+
+ def IncrementValue(self, key, pos):
+ _dbg('wxTimeCtrl::IncrementValue', key, pos)
+ text = self.GetValue()
+
+ sel_start, sel_to = self.GetSelection()
+ selection = sel_start != sel_to
+ cell_selected = selection and sel_to -1 != pos
+
+ dict_start = self.__dictStartCellPos
+
+ # Determine whether we should change the entire cell or just a portion of it:
+ if( not selection
+ or cell_selected
+ or text[pos] == ' '
+ or text[pos] == '9' and text[pos-1] == ' ' and key == WXK_UP
+ or text[pos] == '1' and text[pos-1] == ' ' and key == WXK_DOWN
+ or pos >= dict_start['am_pm']):
+ _dbg(indent=1)
+ self.IncrementCell(key, pos)
+ _dbg(indent=0)
+ else:
+ if key == WXK_UP: inc = 1
+ else: inc = -1
+
+ if pos == dict_start['hour'] and not self.__fmt24hr:
+ if text[pos] == ' ': digit = '1' # allow ' ' or 1 for 1st digit in 12hr format
+ else: digit = ' '
+ else:
+ if pos == dict_start['hour']:
+ if int(text[pos + 1]) >3: mod = 2 # allow for 20-23
+ else: mod = 3 # allow 00-19
+ elif pos == dict_start['hour'] + 1:
+ if self.__fmt24hr:
+ if text[pos - 1] == '2': mod = 4 # allow hours 20-23
+ else: mod = 10 # allow hours 00-19
+ else:
+ if text[pos - 1] == '1': mod = 3 # allow hours 10-12
+ else: mod = 10 # allow 0-9
+
+ elif pos in (dict_start['minute'],
+ dict_start['second']): mod = 6 # allow minutes/seconds 00-59
+ else: mod = 10
+
+ digit = '%d' % ((int(text[pos]) + inc) % mod)
+
+ _dbg(indent=1)
+ _dbg("new digit = \'%s\'" % digit)
+ self.ChangeValue(digit, pos)
+ _dbg(indent=0)
+
+
+
+ def IncrementCell(self, key, pos):
+ _dbg('wxTimeCtrl::IncrementCell', key, pos)
+ self.__SetCurrentCell(pos) # determine current cell
+ hour, minute, second = self.__hour, self.__minute, self.__second
+ text = self.GetValue()
+ dict_start = self.__dictStartCellPos
+ if key == WXK_UP: inc = 1
+ else: inc = -1
+
+ if self.__cellStart == dict_start['am_pm']:
+ am = text[dict_start['am_pm']:] == 'AM'
+ if am: hour = hour + 12
+ else: hour = hour - 12
+ else:
+ if self.__cellStart == dict_start['hour']:
+ hour = (hour + inc) % 24
+ elif self.__cellStart == dict_start['minute']:
+ minute = (minute + inc) % 60
+ elif self.__cellStart == dict_start['second']:
+ second = (second + inc) % 60
+
+ newvalue = '%.2d:%.2d:%.2d' % (hour, minute, second)
+
+ self.__posCurrent = self.__cellStart
+ self.__posSelectTo = self.__cellEnd
+ self.__bSelection = true
+ _dbg(indent=1)
+ self.SetValue(newvalue)
+ _dbg(indent=0)
+
+
+ def ChangeValue(self, char, pos):
+ _dbg('wxTimeCtrl::ChangeValue', "\'" + char + "\'", pos)
+ text = self.GetValue()
+
+ self.__SetCurrentCell(pos)
+ sel_start, sel_to = self.GetSelection()
+ self.__posSelectTo = sel_to
+ self.__bSelection = selection = sel_start != sel_to
+ cell_selected = selection and sel_to -1 != pos
+
+ dict_start = self.__dictStartCellPos
+
+ if pos in self.__listDelimPos: return # don't allow change of punctuation
+
+ elif( 0 < pos < dict_start['am_pm'] and char not in string.digits):
+ return # AM/PM not allowed in this position
+
+ # See if we're changing the hour cell, and validate/update appropriately:
+ #
+ hour_start = dict_start['hour'] # (ie. 0)
+
+ if pos == hour_start: # if at 1st position,
+ if self.__fmt24hr: # and using 24 hour format
+ if char not in ('0', '1', '2'): # return if digit not 0,1, or 2
+ return
+ if cell_selected: # replace cell contents
+ newtext = '%.2d' % int(char) + text[hour_start+2:]
+ else: # relace current position
+ newtext = char + text[pos+1:]
+ else: # (12 hour format)
+ if char not in ('1', ' '): # can only type a 1 or space
+ return
+ if text[pos+1] not in ('0', '1', '2'): # and then, only if other column is 0,1, or 2
+ return
+ if( char == ' ' # and char isn't space and
+ and (cell_selected or text[pos+1] == '0')): # 2nd column is 0 or cell isn't selected
+ return
+ # else... ok
+ if cell_selected: # replace cell contents
+ newtext = '%2d' % int(char) + text[hour_start+2:]
+ else: # relace current position
+ newtext = char + text[pos+1:]
+ if char == ' ': self.SetInsertionPoint(pos+1) # move insert point to legal position
+
+ elif pos == hour_start+1: # if editing 2nd position of hour
+ if( not self.__fmt24hr # and using 12 hour format
+ and text[hour_start] == '1' # if 1st char is 1,
+ and char not in ('0', '1', '2')): # disallow anything bug 0,1, or 2
+ return
+ newtext = text[hour_start] + char + text[hour_start+2:] # else any digit ok
+
+ # Do the same sort of validation for minute and second cells
+ elif pos in (dict_start['minute'], dict_start['second']):
+ if cell_selected: # if cell selected, replace value
+ newtext = text[:pos] + '%.2d' % int(char) + text[pos+2:]
+ elif int(char) > 5: return # else disallow > 59 for minute and second fields
+ else:
+ newtext = text[:pos] + char + text[pos+1:] # else ok
+
+ elif pos in (dict_start['minute']+1, dict_start['second']+1):
+ newtext = text[:pos] + char + text[pos+1:] # all digits ok for 2nd digit of minute/second
+
+ # Process AM/PM cell
+ elif pos == dict_start['am_pm']:
+ if char not in ('A','P'): return # disallow all but A or P as 1st char of column
+ newtext = text[:pos] + char + text[pos+1:]
+ else: return # not a valid position
+
+ # update member position vars and set selection to character changed
+ self.__posCurrent = pos+1
+ self.__SetCurrentCell(self.__posCurrent)
+ _dbg(indent=1)
+ _dbg('newtext=', newtext)
+ _dbg(indent=0)
+ self.SetValue(newtext)
+ self.SetInsertionPoint(pos+1)
+
+
+class TestPanel(wxPanel):
+ def __init__(self, parent, id,
+ pos = wxPyDefaultPosition, size = wxPyDefaultSize,
+ fmt24hr = 0, test_mx = 0,
+ style = wxTAB_TRAVERSAL ):
+
+ self.test_mx = test_mx
+ wxPanel.__init__(self, parent, id, pos, size, style)
+
+ sizer = wxBoxSizer( wxHORIZONTAL )
+ self.tc = wxTimeCtrl(self, 10, fmt24hr = fmt24hr)
+ sizer.AddWindow( self.tc, 0, wxALIGN_CENTRE|wxLEFT|wxTOP|wxBOTTOM, 5 )
+ sb = wxSpinButton( self, 20, wxDefaultPosition, wxSize(-1,20), 0 )
+ self.tc.BindSpinButton(sb)
+ sizer.AddWindow( sb, 0, wxALIGN_CENTRE|wxRIGHT|wxTOP|wxBOTTOM, 5 )
+
+ self.SetAutoLayout( true )
+ self.SetSizer( sizer )
+ sizer.Fit( self )
+ sizer.SetSizeHints( self )
+
+ EVT_TIMEUPDATE(self, self.tc.GetId(), self.OnTimeChange)
+
+ def OnTimeChange(self, event):
+ _dbg('OnTimeChange: value = ', event.GetValue())
+ wxdt = self.tc.GetWxDateTime()
+ _dbg('wxdt =', wxdt.GetHour(), wxdt.GetMinute(), wxdt.GetSecond())
+ if self.test_mx:
+ mxdt = self.tc.GetMxDateTime()
+ _dbg('mxdt =', mxdt.hour, mxdt.minute, mxdt.second)
+
+
+#----------------------------------------------------------------------------
+
+
+if __name__ == '__main__':
+ import traceback
+
+ class MyApp(wxApp):
+
+ def OnInit(self):
+ import sys
+ fmt24hr = '24' in sys.argv
+ test_mx = 'mx' in sys.argv
+
+ try:
+ frame = wxFrame(NULL, -1, "Junk", wxPoint(20,20), wxSize(100,100) )
+ panel = TestPanel(frame, -1, wxPoint(-1,-1), fmt24hr=fmt24hr, test_mx = test_mx)
+ frame.Show(true)
+ except:
+ traceback.print_exc()
+ return false
+
+ return true
+
+ try:
+ app = MyApp(0)
+ app.MainLoop()
+ except:
+ traceback.print_exc()