Gnumed.timelinelib.wxgui.components.pydatetimepicker

Package Gnumed :: Package timelinelib :: Package wxgui :: Package components :: Module pydatetimepicker |
[frames] | no frames]

# Source Code for Module Gnumed.timelinelib.wxgui.components.pydatetimepicker

      1  # Copyright (C) 2009, 2010, 2011  Rickard Lindberg, Roger Lindberg
      2  #
      3  # This file is part of Timeline.
      4  #
      5  # Timeline is free software: you can redistribute it and/or modify
      6  # it under the terms of the GNU General Public License as published by
      7  # the Free Software Foundation, either version 3 of the License, or
      8  # (at your option) any later version.
      9  #
     10  # Timeline is distributed in the hope that it will be useful,
     11  # but WITHOUT ANY WARRANTY; without even the implied warranty of
     12  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13  # GNU General Public License for more details.
     14  #
     15  # You should have received a copy of the GNU General Public License
     16  # along with Timeline.  If not, see <http://www.gnu.org/licenses/>.
     17
     18
     19  import os.path
     20  import datetime
     21
     22  import wx.calendar
     23
     24  from timelinelib.config.paths import ICONS_DIR
     25  from timelinelib.time import PyTimeType
     26  from timelinelib.wxgui.utils import _display_error_message
     27
     28
     29 -class PyDateTimePicker(wx.Panel):
     30
     31 -    def __init__(self, parent, show_time=True, config=None):
     32          wx.Panel.__init__(self, parent)
     33          self.config = config
     34          self._create_gui()
     35          self.controller = PyDateTimePickerController(
     36              self.date_picker, self.time_picker, datetime.datetime.now)
     37          self.show_time(show_time)
     38
     39 -    def show_time(self, show=True):
     40          self.time_picker.Show(show)
     41          self.GetSizer().Layout()
     42
     43 -    def get_value(self):
     44          return self.controller.get_value()
     45
     46 -    def set_value(self, value):
     47          self.controller.set_value(value)
     48
     49 -    def _create_gui(self):
     50          self.date_picker = PyDatePicker(self)
     51          image = wx.Bitmap(os.path.join(ICONS_DIR, "calendar.png"))
     52          self.date_button = wx.BitmapButton(self, bitmap=image)
     53          self.Bind(wx.EVT_BUTTON, self._date_button_on_click, self.date_button)
     54          self.time_picker = PyTimePicker(self)
     55          # Layout
     56          sizer = wx.BoxSizer(wx.HORIZONTAL)
     57          sizer.Add(self.date_picker, proportion=1,
     58                    flag=wx.ALIGN_CENTER_VERTICAL)
     59          sizer.Add(self.date_button, proportion=0,
     60                    flag=wx.ALIGN_CENTER_VERTICAL)
     61          sizer.Add(self.time_picker, proportion=0,
     62                    flag=wx.ALIGN_CENTER_VERTICAL)
     63          self.SetSizerAndFit(sizer)
     64
     65 -    def _date_button_on_click(self, evt):
     66          try:
     67              wx_date = self._py_date_to_wx_date(self.date_picker.get_py_date())
     68              calendar_popup = CalendarPopup(self, wx_date, self.config)
     69              calendar_popup.Bind(wx.calendar.EVT_CALENDAR_SEL_CHANGED,
     70                                  self._calendar_on_date_changed)
     71              calendar_popup.Bind(wx.calendar.EVT_CALENDAR,
     72                                  self._calendar_on_date_changed_dclick)
     73              btn = evt.GetEventObject()
     74              pos = btn.ClientToScreen((0,0))
     75              sz = btn.GetSize()
     76              calendar_popup.Position(pos, (0, sz[1]))
     77              calendar_popup.Popup()
     78              self.calendar_popup = calendar_popup
     79          except ValueError:
     80               _display_error_message(_("Invalid date"))
     81
     82
     83 -    def _calendar_on_date_changed(self, evt):
     84          wx_date = evt.GetEventObject().GetDate()
     85          py_date = datetime.datetime(wx_date.Year, wx_date.Month+1, wx_date.Day)
     86          self.date_picker.set_py_date(py_date)
     87
     88 -    def _calendar_on_date_changed_dclick(self, evt):
     89          self.time_picker.SetFocus()
     90          self.calendar_popup.Dismiss()
     91
     92 -    def _py_date_to_wx_date(self, py_date):
     93          return wx.DateTimeFromDMY(py_date.day, py_date.month-1, py_date.year,
     94                                    0, 0, 0)
     95
     96
     97 -class PyDateTimePickerController(object):
     98
     99 -    def __init__(self, date_picker, time_picker, now_fn):
    100          self.date_picker = date_picker
    101          self.time_picker = time_picker
    102          self.now_fn = now_fn
    103
    104 -    def get_value(self):
    105          time = datetime.time(0, 0)
    106          if self.time_picker.IsShown():
    107              time = self.time_picker.get_py_time()
    108          return datetime.datetime.combine(self.date_picker.get_py_date(), time)
    109
    110 -    def set_value(self, py_date_time):
    111          if py_date_time == None:
    112              py_date_time = self.now_fn()
    113          self.date_picker.set_py_date(py_date_time.date())
    114          self.time_picker.set_py_time(py_date_time.time())
    115
    116
    117 -class CalendarPopup(wx.PopupTransientWindow):
    118
    119 -    def __init__(self, parent, wx_date, config):
    120          self.config = config
    121          wx.PopupTransientWindow.__init__(self, parent, style=wx.BORDER_NONE)
    122          self._create_gui(wx_date)
    123          self.controller = CalendarPopupController(self)
    124          self._bind_events()
    125
    126 -    def _create_gui(self, wx_date):
    127          BORDER = 2
    128          self.cal = self._create_calendar_control(wx_date, BORDER)
    129          size = self.cal.GetBestSize()
    130          self.SetSize((size.width + BORDER * 2, size.height + BORDER * 2))
    131
    132 -    def _create_calendar_control(self, wx_date, border):
    133          style = self._get_cal_style()
    134          cal = wx.calendar.CalendarCtrl(self, -1, wx_date,
    135                                         pos=(border,border), style=style)
    136          self._set_cal_range(cal)
    137          return cal
    138
    139 -    def _get_cal_style(self):
    140          style = (wx.calendar.CAL_SHOW_HOLIDAYS |
    141                   wx.calendar.CAL_SEQUENTIAL_MONTH_SELECTION)
    142          if self.config.week_start == "monday":
    143              style |= wx.calendar.CAL_MONDAY_FIRST
    144          else:
    145              style |= wx.calendar.CAL_SUNDAY_FIRST
    146          return style
    147
    148 -    def _set_cal_range(self, cal):
    149          min_date, msg = PyTimeType().get_min_time()
    150          max_date, msg = PyTimeType().get_max_time()
    151          min_date = self._py_date_to_wx_date(min_date)
    152          max_date = self._py_date_to_wx_date(max_date) - wx.DateSpan.Day()
    153          cal.SetDateRange(min_date, max_date)
    154
    155 -    def _py_date_to_wx_date(self, py_date):
    156          return wx.DateTimeFromDMY(py_date.day, py_date.month - 1, py_date.year,
    157                                    0, 0, 0)
    158
    159 -    def _bind_events(self):
    160          def on_month(evt):
    161              self.controller.on_month()
    162          def on_day(evt):
    163              self.controller.on_day()
    164          self.cal.Bind(wx.calendar.EVT_CALENDAR_MONTH, on_month)
    165          self.cal.Bind(wx.calendar.EVT_CALENDAR_DAY, on_day)
    166
    167 -    def OnDismiss(self):
    168          self.controller.on_dismiss()
    169
    170
    171 -class CalendarPopupController(object):
    172
    173 -    def __init__(self, calendar_popup):
    174          self.calendar_popup = calendar_popup
    175          self.repop = False
    176          self.repoped = False
    177
    178 -    def on_month(self):
    179          self.repop = True
    180
    181 -    def on_day(self):
    182          self.repop = True
    183
    184 -    def on_dismiss(self):
    185          # This funny code makes the calender control stay open when you change
    186          # month or day. The control is closed on a double-click on a day or
    187          # a single click outside of the control
    188          if self.repop and not self.repoped:
    189              self.calendar_popup.Popup()
    190              self.repoped = True
    191
    192
    193 -class PyDatePicker(wx.TextCtrl):
    194
    195 -    def __init__(self, parent):
    196          wx.TextCtrl.__init__(self, parent, style=wx.TE_PROCESS_ENTER)
    197          self.controller = PyDatePickerController(self)
    198          self._bind_events()
    199          self._resize_to_fit_text()
    200
    201 -    def get_py_date(self):
    202          return self.controller.get_py_date()
    203
    204 -    def set_py_date(self, py_date):
    205          self.controller.set_py_date(py_date)
    206
    207 -    def get_date_string(self):
    208          return self.GetValue()
    209
    210 -    def set_date_string(self, date_string):
    211          return self.SetValue(date_string)
    212
    213 -    def _bind_events(self):
    214          def on_set_focus(evt):
    215              # CallAfter is a trick to prevent default behavior of selecting all
    216              # text when a TextCtrl is given focus
    217              wx.CallAfter(self.controller.on_set_focus)
    218          self.Bind(wx.EVT_SET_FOCUS, on_set_focus)
    219          def on_kill_focus(evt):
    220              # Trick to not make selection text disappear when focus is lost (we
    221              # remove the selection instead)
    222              self.controller.on_kill_focus()
    223              self.SetSelection(0, 0)
    224          self.Bind(wx.EVT_KILL_FOCUS, on_kill_focus)
    225          def on_char(evt):
    226              if evt.GetKeyCode() == wx.WXK_TAB:
    227                  if evt.ShiftDown():
    228                      skip = self.controller.on_shift_tab()
    229                  else:
    230                      skip = self.controller.on_tab()
    231              else:
    232                  skip = True
    233              evt.Skip(skip)
    234          self.Bind(wx.EVT_CHAR, on_char)
    235          def on_text(evt):
    236              self.controller.on_text_changed()
    237          self.Bind(wx.EVT_TEXT, on_text)
    238          def on_key_down(evt):
    239              if evt.GetKeyCode() == wx.WXK_UP:
    240                  self.controller.on_up()
    241              elif evt.GetKeyCode() == wx.WXK_DOWN:
    242                  self.controller.on_down()
    243              else:
    244                  evt.Skip()
    245          self.Bind(wx.EVT_KEY_DOWN, on_key_down)
    246
    247 -    def _resize_to_fit_text(self):
    248          w, h = self.GetTextExtent("0000-00-00")
    249          width = w + 20
    250          self.SetMinSize((width, -1))
    251
    252
    253 -class PyDatePickerController(object):
    254
    255 -    def __init__(self, py_date_picker, error_bg="pink"):
    256          self.py_date_picker = py_date_picker
    257          self.error_bg = error_bg
    258          self.original_bg = self.py_date_picker.GetBackgroundColour()
    259          self.separator = PyTimeType().event_date_string(PyTimeType().now())[4]
    260          self.region_year = 0
    261          self.region_month = 1
    262          self.region_day = 2
    263          self.region_siblings = ((self.region_year, self.region_month),
    264                                  (self.region_month, self.region_day))
    265          self.preferred_day = None
    266          self.save_preferred_day = True
    267          self.last_selection = None
    268
    269 -    def get_py_date(self):
    270          try:
    271              (year, month, day) = self._parse_year_month_day()
    272              py_date = datetime.date(year, month, day)
    273              self._ensure_within_allowed_period(py_date)
    274              return py_date
    275          except ValueError:
    276              raise ValueError("Invalid date.")
    277
    278 -    def set_py_date(self, py_date):
    279          date_string = PyTimeType().event_date_string(py_date)
    280          self.py_date_picker.set_date_string(date_string)
    281
    282 -    def on_set_focus(self):
    283          if self.last_selection:
    284              start, end = self.last_selection
    285              self.py_date_picker.SetSelection(start, end)
    286          else:
    287              self._select_region_if_possible(self.region_year)
    288              self.last_selection = self.py_date_picker.GetSelection()
    289
    290 -    def on_kill_focus(self):
    291          if self.last_selection:
    292              self.last_selection = self.py_date_picker.GetSelection()
    293
    294 -    def on_tab(self):
    295          for (left_region, right_region) in self.region_siblings:
    296              if self._insertion_point_in_region(left_region):
    297                  self._select_region_if_possible(right_region)
    298                  return False
    299          return True
    300
    301 -    def on_shift_tab(self):
    302          for (left_region, right_region) in self.region_siblings:
    303              if self._insertion_point_in_region(right_region):
    304                  self._select_region_if_possible(left_region)
    305                  return False
    306          return True
    307
    308 -    def on_text_changed(self):
    309          self._change_background_depending_on_date_validity()
    310          if self._current_date_is_valid():
    311              current_date = self.get_py_date()
    312              # To prevent saving of preferred day when year or month is changed
    313              # in on_up() and on_down()...
    314              # Save preferred day only when text is entered in the date text
    315              # control and not when up or down keys has been used.
    316              # When up and down keys are used, the preferred day is saved in
    317              # on_up() and on_down() only when day is changed.
    318              if self.save_preferred_day:
    319                  self._save_preferred_day(current_date)
    320
    321 -    def on_up(self):
    322          def increment_year(date):
    323              if date.year < PyTimeType().get_max_time()[0].year - 1:
    324                  return self._set_valid_day(date.year + 1, date.month, date.day)
    325              return date
    326          def increment_month(date):
    327              if date.month < 12:
    328                  return self._set_valid_day(date.year, date.month + 1,
    329                                             date.day)
    330              elif date.year < PyTimeType().get_max_time()[0].year - 1:
    331                  return self._set_valid_day(date.year + 1, 1, date.day)
    332              return date
    333          def increment_day(date):
    334              if date <  PyTimeType().get_max_time()[0].date() - datetime.timedelta(days=1):
    335                  return date + datetime.timedelta(days=1)
    336              return date
    337          if not self._current_date_is_valid():
    338              return
    339          selection = self.py_date_picker.GetSelection()
    340          current_date = self.get_py_date()
    341          if self._insertion_point_in_region(self.region_year):
    342              new_date = increment_year(current_date)
    343          elif self._insertion_point_in_region(self.region_month):
    344              new_date = increment_month(current_date)
    345          else:
    346              new_date = increment_day(current_date)
    347              self._save_preferred_day(new_date)
    348          if current_date != new_date:
    349              self._set_new_date_and_restore_selection(new_date, selection)
    350
    351 -    def on_down(self):
    352          def decrement_year(date):
    353              if date.year > PyTimeType().get_min_time()[0].year:
    354                  return self._set_valid_day(date.year - 1, date.month, date.day)
    355              return date
    356          def decrement_month(date):
    357              if date.month > 1:
    358                  return self._set_valid_day(date.year, date.month - 1, date.day)
    359              elif date.year > PyTimeType().get_min_time()[0].year:
    360                  return self._set_valid_day(date.year - 1, 12, date.day)
    361              return date
    362          def decrement_day(date):
    363              if date.day > 1:
    364                  return date.replace(day=date.day - 1)
    365              elif date.month > 1:
    366                  return self._set_valid_day(date.year, date.month - 1, 31)
    367              elif date.year > PyTimeType().get_min_time()[0].year:
    368                  return self._set_valid_day(date.year - 1, 12, 31)
    369              return date
    370          if not self._current_date_is_valid():
    371              return
    372          selection = self.py_date_picker.GetSelection()
    373          current_date = self.get_py_date()
    374          if self._insertion_point_in_region(self.region_year):
    375              new_date = decrement_year(current_date)
    376          elif self._insertion_point_in_region(self.region_month):
    377              new_date = decrement_month(current_date)
    378          else:
    379              new_date = decrement_day(current_date)
    380              self._save_preferred_day(new_date)
    381          if current_date != new_date:
    382              self._set_new_date_and_restore_selection(new_date, selection)
    383
    384 -    def _change_background_depending_on_date_validity(self):
    385          if self._current_date_is_valid():
    386              self.py_date_picker.SetBackgroundColour(self.original_bg)
    387          else:
    388              self.py_date_picker.SetBackgroundColour(self.error_bg)
    389          self.py_date_picker.SetFocus()
    390          self.py_date_picker.Refresh()
    391
    392 -    def _parse_year_month_day(self):
    393          components = self.py_date_picker.get_date_string().split(self.separator)
    394          if len(components) != 3:
    395              raise ValueError()
    396          year  = int(components[self.region_year])
    397          month = int(components[self.region_month])
    398          day   = int(components[self.region_day])
    399          return (year, month, day)
    400
    401 -    def _ensure_within_allowed_period(self, py_date):
    402          py_date_time = datetime.datetime(py_date.year, py_date.month, py_date.day)
    403          if (py_date_time >= PyTimeType().get_max_time()[0] or
    404              py_date_time <  PyTimeType().get_min_time()[0]):
    405              raise ValueError()
    406
    407 -    def _set_new_date_and_restore_selection(self, new_date, selection):
    408          def restore_selection(selection):
    409              self.py_date_picker.SetSelection(selection[0], selection[1])
    410          self.save_preferred_day = False
    411          if self.preferred_day != None:
    412              new_date = self._set_valid_day(new_date.year, new_date.month,
    413                                             self.preferred_day)
    414          self.set_py_date(new_date)
    415          restore_selection(selection)
    416          self.save_preferred_day = True
    417
    418 -    def _set_valid_day(self, new_year, new_month, new_day):
    419          done = False
    420          while not done:
    421              try:
    422                  date = datetime.date(year=new_year, month=new_month, day=new_day)
    423                  done = True
    424              except Exception, ex:
    425                  new_day -= 1
    426          return date
    427
    428 -    def _save_preferred_day(self, date):
    429          if date.day > 28:
    430              self.preferred_day = date.day
    431          else:
    432              self.preferred_day = None
    433
    434 -    def _current_date_is_valid(self):
    435          try:
    436              self.get_py_date()
    437          except ValueError:
    438              return False
    439          return True
    440
    441 -    def _select_region_if_possible(self, region):
    442          region_range = self._get_region_range(region)
    443          if region_range:
    444              self.py_date_picker.SetSelection(region_range[0], region_range[-1])
    445
    446 -    def _insertion_point_in_region(self, n):
    447          region_range = self._get_region_range(n)
    448          if region_range:
    449              return self.py_date_picker.GetInsertionPoint() in region_range
    450
    451 -    def _get_region_range(self, n):
    452          # Returns a range of valid cursor positions for a valid region year,
    453          # month or day.
    454          def region_is_not_valid(region):
    455              return region not in (self.region_year, self.region_month,
    456                                    self.region_day)
    457          def date_has_exactly_two_seperators(datestring):
    458              return len(datestring.split(self.separator)) == 3
    459          def calculate_pos_range(region, datestring):
    460              pos_of_separator1 = datestring.find(self.separator)
    461              pos_of_separator2 = datestring.find(self.separator,
    462                                                  pos_of_separator1 + 1)
    463              if region == self.region_year:
    464                  return range(0, pos_of_separator1 + 1)
    465              elif region == self.region_month:
    466                  return range(pos_of_separator1 + 1, pos_of_separator2 + 1)
    467              else:
    468                  return range(pos_of_separator2 + 1, len(datestring) + 1)
    469          if region_is_not_valid(n):
    470              return None
    471          date = self.py_date_picker.get_date_string()
    472          if not date_has_exactly_two_seperators(date):
    473              return None
    474          pos_range = calculate_pos_range(n, date)
    475          return pos_range
    476
    477
    478 -class PyTimePicker(wx.TextCtrl):
    479
    480 -    def __init__(self, parent):
    481          wx.TextCtrl.__init__(self, parent, style=wx.TE_PROCESS_ENTER)
    482          self.controller = PyTimePickerController(self)
    483          self._bind_events()
    484          self._resize_to_fit_text()
    485
    486 -    def get_py_time(self):
    487          return self.controller.get_py_time()
    488
    489 -    def set_py_time(self, py_time):
    490          self.controller.set_py_time(py_time)
    491
    492 -    def get_time_string(self):
    493          return self.GetValue()
    494
    495 -    def set_time_string(self, time_string):
    496          self.SetValue(time_string)
    497
    498 -    def _bind_events(self):
    499          def on_set_focus(evt):
    500              # CallAfter is a trick to prevent default behavior of selecting all
    501              # text when a TextCtrl is given focus
    502              wx.CallAfter(self.controller.on_set_focus)
    503          self.Bind(wx.EVT_SET_FOCUS, on_set_focus)
    504          def on_kill_focus(evt):
    505              # Trick to not make selection text disappear when focus is lost (we
    506              # remove the selection instead)
    507              self.controller.on_kill_focus()
    508              self.SetSelection(0, 0)
    509          self.Bind(wx.EVT_KILL_FOCUS, on_kill_focus)
    510          def on_char(evt):
    511              if evt.GetKeyCode() == wx.WXK_TAB:
    512                  if evt.ShiftDown():
    513                      skip = self.controller.on_shift_tab()
    514                  else:
    515                      skip = self.controller.on_tab()
    516              else:
    517                  skip = True
    518              evt.Skip(skip)
    519          self.Bind(wx.EVT_CHAR, on_char)
    520          def on_text(evt):
    521              self.controller.on_text_changed()
    522          self.Bind(wx.EVT_TEXT, on_text)
    523          def on_key_down(evt):
    524              if evt.GetKeyCode() == wx.WXK_UP:
    525                  self.controller.on_up()
    526              elif evt.GetKeyCode() == wx.WXK_DOWN:
    527                  self.controller.on_down()
    528              else:
    529                  evt.Skip()
    530          self.Bind(wx.EVT_KEY_DOWN, on_key_down)
    531
    532 -    def _resize_to_fit_text(self):
    533          w, h = self.GetTextExtent("00:00")
    534          width = w + 20
    535          self.SetMinSize((width, -1))
    536
    537
    538 -class PyTimePickerController(object):
    539
    540 -    def __init__(self, py_time_picker):
    541          self.py_time_picker = py_time_picker
    542          self.original_bg = self.py_time_picker.GetBackgroundColour()
    543          self.separator = PyTimeType().event_time_string(PyTimeType().now())[2]
    544          self.hour_part = 0
    545          self.minute_part = 1
    546          self.last_selection = None
    547
    548 -    def get_py_time(self):
    549          try:
    550              split = self.py_time_picker.get_time_string().split(self.separator)
    551              if len(split) != 2:
    552                  raise ValueError()
    553              hour_string, minute_string = split
    554              hour = int(hour_string)
    555              minute = int(minute_string)
    556              return datetime.time(hour, minute)
    557          except ValueError:
    558              raise ValueError("Invalid time.")
    559
    560 -    def set_py_time(self, py_time):
    561          time_string = PyTimeType().event_time_string(py_time)
    562          self.py_time_picker.set_time_string(time_string)
    563
    564 -    def on_set_focus(self):
    565          if self.last_selection:
    566              start, end = self.last_selection
    567              self.py_time_picker.SetSelection(start, end)
    568          else:
    569              self._select_part(self.hour_part)
    570
    571 -    def on_kill_focus(self):
    572          self.last_selection = self.py_time_picker.GetSelection()
    573
    574 -    def on_tab(self):
    575          if self._in_minute_part():
    576              return True
    577          self._select_part(self.minute_part)
    578          return False
    579
    580 -    def on_shift_tab(self):
    581          if self._in_hour_part():
    582              return True
    583          self._select_part(self.hour_part)
    584          return False
    585
    586 -    def on_text_changed(self):
    587          try:
    588              self.get_py_time()
    589              self.py_time_picker.SetBackgroundColour(self.original_bg)
    590          except ValueError:
    591              self.py_time_picker.SetBackgroundColour("pink")
    592          self.py_time_picker.Refresh()
    593
    594 -    def on_up(self):
    595          def increment_hour(time):
    596              new_hour = time.hour + 1
    597              if new_hour > 23:
    598                  new_hour = 0
    599              return time.replace(hour=new_hour)
    600          def increment_minutes(time):
    601              new_hour = time.hour
    602              new_minute = time.minute + 1
    603              if new_minute > 59:
    604                  new_minute = 0
    605                  new_hour = time.hour + 1
    606                  if new_hour > 23:
    607                      new_hour = 0
    608              return time.replace(hour=new_hour, minute=new_minute)
    609          if not self._time_is_valid():
    610              return
    611          selection = self.py_time_picker.GetSelection()
    612          current_time = self.get_py_time()
    613          if self._in_hour_part():
    614              new_time = increment_hour(current_time)
    615          else:
    616              new_time = increment_minutes(current_time)
    617          if current_time != new_time:
    618              self._set_new_time_and_restore_selection(new_time, selection)
    619
    620 -    def on_down(self):
    621          def decrement_hour(time):
    622              new_hour = time.hour - 1
    623              if new_hour < 0:
    624                  new_hour = 23
    625              return time.replace(hour=new_hour)
    626          def decrement_minutes(time):
    627              new_hour = time.hour
    628              new_minute = time.minute - 1
    629              if new_minute < 0:
    630                  new_minute = 59
    631                  new_hour = time.hour - 1
    632                  if new_hour < 0:
    633                      new_hour = 23
    634              return time.replace(hour=new_hour, minute=new_minute)
    635          if not self._time_is_valid():
    636              return
    637          selection = self.py_time_picker.GetSelection()
    638          current_time = self.get_py_time()
    639          if self._in_hour_part():
    640              new_time = decrement_hour(current_time)
    641          else:
    642              new_time = decrement_minutes(current_time)
    643          if current_time != new_time:
    644              self._set_new_time_and_restore_selection(new_time, selection)
    645
    646 -    def _set_new_time_and_restore_selection(self, new_time, selection):
    647          def restore_selection(selection):
    648              self.py_time_picker.SetSelection(selection[0], selection[1])
    649          self.set_py_time(new_time)
    650          restore_selection(selection)
    651
    652 -    def _time_is_valid(self):
    653          try:
    654              self.get_py_time()
    655          except ValueError:
    656              return False
    657          return True
    658
    659 -    def _select_part(self, part):
    660          if self._separator_pos() == -1:
    661              return
    662          if part == self.hour_part:
    663              self.py_time_picker.SetSelection(0, self._separator_pos())
    664          else:
    665              time_string_len = len(self.py_time_picker.get_time_string())
    666              self.py_time_picker.SetSelection(self._separator_pos() + 1, time_string_len)
    667          self.preferred_part = part
    668
    669 -    def _in_hour_part(self):
    670          if self._separator_pos() == -1:
    671              return
    672          return self.py_time_picker.GetInsertionPoint() <= self._separator_pos()
    673
    674 -    def _in_minute_part(self):
    675          if self._separator_pos() == -1:
    676              return
    677          return self.py_time_picker.GetInsertionPoint() > self._separator_pos()
    678
    679 -    def _separator_pos(self):
    680          return self.py_time_picker.get_time_string().find(self.separator)
    681