Package Gnumed :: Package timelinelib :: Package calendar :: Package bosparanian :: Module timepicker |
[frames] | no frames]

# Source Code for Module Gnumed.timelinelib.calendar.bosparanian.timepicker

      1  # Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018  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 wx
     20
     21  from timelinelib.calendar.bosparanian.bosparanian import BosparanianDateTime, is_valid_time
     22  from timelinelib.calendar.bosparanian.dateformatter import BosparanianDateFormatter
     23  from timelinelib.calendar.bosparanian.time import BosparanianDelta
     24  from timelinelib.calendar.bosparanian.timetype import BosparanianTimeType
     25
     26
     27 -class BosparanianDateTimePicker(wx.Panel):
     28
     29 -    def __init__(self, parent, show_time=True, config=None, on_change=None):
     30          wx.Panel.__init__(self, parent)
     31          self.config = config
     32          self._create_gui(on_change)
     33          self.controller = BosparanianDateTimePickerController(
     34              self.date_picker, self.time_picker, BosparanianTimeType().now)
     35          self.show_time(show_time)
     36          self.parent = parent
     37
     38 -    def on_return(self):
     39          try:
     40              self.parent.on_return()
     41          except AttributeError:
     42              pass
     43
     44 -    def on_escape(self):
     45          try:
     46              self.parent.on_escape()
     47          except AttributeError:
     48              pass
     49
     50 -    def show_time(self, show=True):
     51          self.time_picker.Show(show)
     52          self.GetSizer().Layout()
     53
     54 -    def get_value(self):
     55          try:
     56              return self.controller.get_value()
     57          except ValueError:
     58              pass
     59
     60 -    def set_value(self, value):
     61          self.controller.set_value(value)
     62
     63 -    def _create_gui(self, on_change):
     64          self.date_picker = BosparanianDatePicker(self, on_change)
     65          self.time_picker = BosparanianTimePicker(self, on_change)
     66          # Layout
     67          sizer = wx.BoxSizer(wx.HORIZONTAL)
     68          sizer.Add(self.date_picker, proportion=1,
     69                    flag=wx.ALIGN_CENTER_VERTICAL)
     70          sizer.Add(self.time_picker, proportion=0,
     71                    flag=wx.ALIGN_CENTER_VERTICAL)
     72          self.SetSizerAndFit(sizer)
     73
     74
     75 -class BosparanianDateTimePickerController(object):
     76
     77 -    def __init__(self, date_picker, time_picker, now_fn):
     78          self.date_picker = date_picker
     79          self.time_picker = time_picker
     80          self.now_fn = now_fn
     81
     82 -    def get_value(self):
     83          if self.time_picker.IsShown():
     84              hour, minute, second = self.time_picker.get_value()
     85          else:
     86              hour, minute, second = (0, 0, 0)
     87          year, month, day = self.date_picker.get_value()
     88          return BosparanianDateTime(year, month, day, hour, minute, second).to_time()
     89
     90 -    def set_value(self, time):
     91          if time is None:
     92              time = self.now_fn()
     93          self.date_picker.set_value(BosparanianDateTime.from_time(time).to_date_tuple())
     94          self.time_picker.set_value(BosparanianDateTime.from_time(time).to_time_tuple())
     95
     96
     97 -class BosparanianDatePicker(wx.TextCtrl):
     98
     99 -    def __init__(self, parent, on_change):
    100          wx.TextCtrl.__init__(self, parent, style=wx.TE_PROCESS_ENTER)
    101          self.controller = BosparanianDatePickerController(self, on_change=on_change)
    102          self._bind_events()
    103          self._resize_to_fit_text()
    104          self.parent = parent
    105
    106 -    def get_value(self):
    107          return self.controller.get_value()
    108
    109 -    def set_value(self, date):
    110          self.controller.set_value(date)
    111
    112 -    def get_date_string(self):
    113          return self.GetValue()
    114
    115 -    def set_date_string(self, date_string):
    116          date_str, bc_year = date_string
    117          return self.SetValue(date_str)
    118
    119 -    def _bind_events(self):
    120          def on_set_focus(evt):
    121              # CallAfter is a trick to prevent default behavior of selecting all
    122              # text when a TextCtrl is given focus
    123              wx.CallAfter(self.controller.on_set_focus)
    124          self.Bind(wx.EVT_SET_FOCUS, on_set_focus)
    125          def on_kill_focus(evt):
    126              # Trick to not make selection text disappear when focus is lost (we
    127              # remove the selection instead)
    128              self.controller.on_kill_focus()
    129              self.SetSelection(0, 0)
    130          self.Bind(wx.EVT_KILL_FOCUS, on_kill_focus)
    131          def on_char(evt):
    132              if evt.GetKeyCode() == wx.WXK_TAB:
    133                  if evt.ShiftDown():
    134                      skip = self.controller.on_shift_tab()
    135                  else:
    136                      skip = self.controller.on_tab()
    137              else:
    138                  skip = True
    139              evt.Skip(skip)
    140          self.Bind(wx.EVT_CHAR, on_char)
    141          def on_text(evt):
    142              self.controller.on_text_changed()
    143          self.Bind(wx.EVT_TEXT, on_text)
    144          def on_key_down(evt):
    145              if evt.GetKeyCode() == wx.WXK_UP:
    146                  self.controller.on_up()
    147              elif evt.GetKeyCode() == wx.WXK_DOWN:
    148                  self.controller.on_down()
    149              elif (evt.GetKeyCode() == wx.WXK_NUMPAD_ENTER or
    150                    evt.GetKeyCode() == wx.WXK_RETURN):
    151                  self.parent.on_return()
    152              elif (evt.GetKeyCode() == wx.WXK_ESCAPE):
    153                  self.parent.on_escape()
    154              else:
    155                  evt.Skip()
    156          self.Bind(wx.EVT_KEY_DOWN, on_key_down)
    157
    158 -    def _resize_to_fit_text(self):
    159          w, _ = self.GetTextExtent("0000BF-MMM-00")
    160          width = w + 20
    161          self.SetMinSize((width, -1))
    162
    163
    164 -class BosparanianDatePickerController(object):
    165
    166 -    def __init__(self, date_picker, error_bg="pink", on_change=None):
    167          self.date_picker = date_picker
    168          self.error_bg = error_bg
    169          self.original_bg = self.date_picker.GetBackgroundColour()
    170          self.date_formatter = BosparanianDateFormatter()
    171          self.separator = self.date_formatter.separator()
    172          self.region_year, self.region_month, self.region_day = self.date_formatter.get_regions()
    173          self.region_siblings = ((self.region_year, self.region_month),
    174                                  (self.region_month, self.region_day))
    175          self.preferred_day = None
    176          self.save_preferred_day = True
    177          self.last_selection = None
    178          self.on_change = on_change
    179
    180 -    def get_value(self):
    181          try:
    182              (year, month, day) = self._parse_year_month_day()
    183              self._ensure_within_allowed_period((year, month, day))
    184              return (year, month, day)
    185          except ValueError:
    186              raise ValueError("Invalid date.")
    187
    188 -    def set_value(self, value):
    189          year, month, day = value
    190          date_string = self.date_formatter.format(year, month, day)
    191          self.date_picker.set_date_string(date_string)
    192          self._on_change()
    193
    194 -    def _on_change(self):
    195          if self._current_date_is_valid() and not self.on_change is None:
    196              self.on_change()
    197
    198 -    def on_set_focus(self):
    199          if self.last_selection:
    200              start, end = self.last_selection
    201              self.date_picker.SetSelection(start, end)
    202          else:
    203              self._select_region_if_possible(self.region_year)
    204              self.last_selection = self.date_picker.GetSelection()
    205
    206 -    def on_kill_focus(self):
    207          if self.last_selection:
    208              self.last_selection = self.date_picker.GetSelection()
    209
    210 -    def on_tab(self):
    211          for (left_region, right_region) in self.region_siblings:
    212              if self._insertion_point_in_region(left_region):
    213                  self._select_region_if_possible(right_region)
    214                  return False
    215          return True
    216
    217 -    def on_shift_tab(self):
    218          for (left_region, right_region) in self.region_siblings:
    219              if self._insertion_point_in_region(right_region):
    220                  self._select_region_if_possible(left_region)
    221                  return False
    222          return True
    223
    224 -    def on_text_changed(self):
    225          self._change_background_depending_on_date_validity()
    226          if self._current_date_is_valid():
    227              current_date = self.get_value()
    228              # To prevent saving of preferred day when year or month is changed
    229              # in on_up() and on_down()...
    230              # Save preferred day only when text is entered in the date text
    231              # control and not when up or down keys has been used.
    232              # When up and down keys are used, the preferred day is saved in
    233              # on_up() and on_down() only when day is changed.
    234              if self.save_preferred_day:
    235                  self._save_preferred_day(current_date)
    236              self._on_change()
    237
    238 -    def on_up(self):
    239          max_year = BosparanianDateTime.from_time(BosparanianTimeType().get_max_time()).year
    240          def increment_year(date):
    241              year, month, day = date
    242              if year < max_year - 1:
    243                  return self._set_valid_day(year + 1, month, day)
    244              return date
    245          def increment_month(date):
    246              year, month, day = date
    247              if month < 13:
    248                  return self._set_valid_day(year, month + 1, day)
    249              elif year < max_year - 1:
    250                  return self._set_valid_day(year + 1, 1, day)
    251              return date
    252          def increment_day(date):
    253              year, month, day = date
    254              time = BosparanianDateTime.from_ymd(year, month, day).to_time()
    255              if time < BosparanianTimeType().get_max_time() - BosparanianDelta.from_days(1):
    256                  return BosparanianDateTime.from_time(time + BosparanianDelta.from_days(1)).to_date_tuple()
    257              return date
    258          if not self._current_date_is_valid():
    259              return
    260          selection = self.date_picker.GetSelection()
    261          current_date = self.get_value()
    262          if self._insertion_point_in_region(self.region_year):
    263              new_date = increment_year(current_date)
    264          elif self._insertion_point_in_region(self.region_month):
    265              new_date = increment_month(current_date)
    266          else:
    267              new_date = increment_day(current_date)
    268              self._save_preferred_day(new_date)
    269          if current_date != new_date:
    270              self._set_new_date_and_restore_selection(new_date, selection)
    271          self._on_change()
    272
    273 -    def on_down(self):
    274          def decrement_year(date):
    275              year, month, day = date
    276              if year > BosparanianDateTime.from_time(BosparanianTimeType().get_min_time()).year:
    277                  return self._set_valid_day(year - 1, month, day)
    278              return date
    279          def decrement_month(date):
    280              year, month, day = date
    281              if month > 1:
    282                  return self._set_valid_day(year, month - 1, day)
    283              elif year > BosparanianDateTime.from_time(BosparanianTimeType().get_min_time()).year:
    284                  return self._set_valid_day(year - 1, 13, day)
    285              return date
    286          def decrement_day(date):
    287              year, month, day = date
    288              if day > 1:
    289                  return self._set_valid_day(year, month, day - 1)
    290              elif month > 1:
    291                  return self._set_valid_day(year, month - 1, 30)
    292              elif year > BosparanianDateTime.from_time(BosparanianTimeType().get_min_time()).year:
    293                  return self._set_valid_day(year - 1, 13, 5)
    294              return date
    295          if not self._current_date_is_valid():
    296              return
    297          selection = self.date_picker.GetSelection()
    298          current_date = self.get_value()
    299          if self._insertion_point_in_region(self.region_year):
    300              new_date = decrement_year(current_date)
    301          elif self._insertion_point_in_region(self.region_month):
    302              new_date = decrement_month(current_date)
    303          else:
    304              year, month, day = current_date
    305              BosparanianDateTime.from_ymd(year, month, day)
    306              if BosparanianDateTime.from_ymd(year, month, day).to_time() == BosparanianTimeType().get_min_time():
    307                  return
    308              new_date = decrement_day(current_date)
    309              self._save_preferred_day(new_date)
    310          if current_date != new_date:
    311              self._set_new_date_and_restore_selection(new_date, selection)
    312          self._on_change()
    313
    314 -    def _change_background_depending_on_date_validity(self):
    315          if self._current_date_is_valid():
    316              self.date_picker.SetBackgroundColour(self.original_bg)
    317          else:
    318              self.date_picker.SetBackgroundColour(self.error_bg)
    319          self.date_picker.SetFocus()
    320          self.date_picker.Refresh()
    321
    322 -    def _parse_year_month_day(self):
    323          return self.date_formatter.parse(self.date_picker.get_date_string())
    324
    325 -    def _ensure_within_allowed_period(self, date):
    326          year, month, day = date
    327          time = BosparanianDateTime(year, month, day, 0, 0, 0).to_time()
    328          if (time >= BosparanianTimeType().get_max_time() or
    329              time < BosparanianTimeType().get_min_time()):
    330              raise ValueError()
    331
    332 -    def _set_new_date_and_restore_selection(self, new_date, selection):
    333          def restore_selection(selection):
    334              self.date_picker.SetSelection(selection[0], selection[1])
    335          self.save_preferred_day = False
    336          if self.preferred_day is not None:
    337              year, month, _ = new_date
    338              new_date = self._set_valid_day(year, month, self.preferred_day)
    339          self.set_value(new_date)
    340          restore_selection(selection)
    341          self.save_preferred_day = True
    342
    343 -    def _set_valid_day(self, new_year, new_month, new_day):
    344          done = False
    345          while not done:
    346              try:
    347                  date = BosparanianDateTime.from_ymd(new_year, new_month, new_day)
    348                  done = True
    349              except Exception:
    350                  new_day -= 1
    351          return date.to_date_tuple()
    352
    353 -    def _save_preferred_day(self, date):
    354          _, _, day = date
    355          if day > 28:
    356              self.preferred_day = day
    357          else:
    358              self.preferred_day = None
    359
    360 -    def _current_date_is_valid(self):
    361          try:
    362              self.get_value()
    363          except ValueError:
    364              return False
    365          return True
    366
    367 -    def _select_region_if_possible(self, region):
    368          region_range = self._get_region_range(region)
    369          if region_range:
    370              self.date_picker.SetSelection(region_range[0], region_range[-1])
    371
    372 -    def _insertion_point_in_region(self, n):
    373          region_range = self._get_region_range(n)
    374          if region_range:
    375              return self.date_picker.GetInsertionPoint() in region_range
    376
    377 -    def _get_region_range(self, n):
    378          # Returns a range of valid cursor positions for a valid region year,
    379          # month or day.
    380          def region_is_not_valid(region):
    381              return region not in (self.region_year, self.region_month,
    382                                    self.region_day)
    383          def date_has_exactly_two_seperators(datestring):
    384              return len(datestring.split(self.separator)) == 3
    385          def calculate_pos_range(region, datestring):
    386              pos_of_separator1 = datestring.find(self.separator)
    387              pos_of_separator2 = datestring.find(self.separator,
    388                                                  pos_of_separator1 + 1)
    389              if region == self.region_year:
    390                  return range(0, pos_of_separator1 + 1)
    391              elif region == self.region_month:
    392                  return range(pos_of_separator1 + 1, pos_of_separator2 + 1)
    393              else:
    394                  return range(pos_of_separator2 + 1, len(datestring) + 1)
    395          if region_is_not_valid(n):
    396              return None
    397          date = self.date_picker.get_date_string()
    398          if not date_has_exactly_two_seperators(date):
    399              return None
    400          pos_range = calculate_pos_range(n, date)
    401          return pos_range
    402
    403
    404 -class BosparanianTimePicker(wx.TextCtrl):
    405
    406 -    def __init__(self, parent, on_change):
    407          wx.TextCtrl.__init__(self, parent, style=wx.TE_PROCESS_ENTER)
    408          self.controller = BosparanianTimePickerController(self, on_change)
    409          self._bind_events()
    410          self._resize_to_fit_text()
    411          self.parent = parent
    412
    413 -    def get_value(self):
    414          return self.controller.get_value()
    415
    416 -    def set_value(self, value):
    417          self.controller.set_value(value)
    418
    419 -    def _get_time_string(self):
    420          return self.GetValue()
    421
    422 -    def set_time_string(self, time_string):
    423          self.SetValue(time_string)
    424
    425 -    def _bind_events(self):
    426          def on_set_focus(evt):
    427              # CallAfter is a trick to prevent default behavior of selecting all
    428              # text when a TextCtrl is given focus
    429              wx.CallAfter(self.controller.on_set_focus)
    430          self.Bind(wx.EVT_SET_FOCUS, on_set_focus)
    431          def on_kill_focus(evt):
    432              # Trick to not make selection text disappear when focus is lost (we
    433              # remove the selection instead)
    434              self.controller.on_kill_focus()
    435              self.SetSelection(0, 0)
    436          self.Bind(wx.EVT_KILL_FOCUS, on_kill_focus)
    437          def on_char(evt):
    438              if evt.GetKeyCode() == wx.WXK_TAB:
    439                  if evt.ShiftDown():
    440                      skip = self.controller.on_shift_tab()
    441                  else:
    442                      skip = self.controller.on_tab()
    443              else:
    444                  skip = True
    445              evt.Skip(skip)
    446          self.Bind(wx.EVT_CHAR, on_char)
    447          def on_text(evt):
    448              self.controller.on_text_changed()
    449          self.Bind(wx.EVT_TEXT, on_text)
    450          def on_key_down(evt):
    451              if evt.GetKeyCode() == wx.WXK_UP:
    452                  self.controller.on_up()
    453              elif evt.GetKeyCode() == wx.WXK_DOWN:
    454                  self.controller.on_down()
    455              elif (evt.GetKeyCode() == wx.WXK_NUMPAD_ENTER or
    456                    evt.GetKeyCode() == wx.WXK_RETURN):
    457                  self.parent.on_return()
    458              elif (evt.GetKeyCode() == wx.WXK_ESCAPE):
    459                  self.parent.on_escape()
    460              else:
    461                  evt.Skip()
    462          self.Bind(wx.EVT_KEY_DOWN, on_key_down)
    463
    464 -    def _resize_to_fit_text(self):
    465          w, _ = self.GetTextExtent("00:00")
    466          width = w + 20
    467          self.SetMinSize((width, -1))
    468
    469
    470 -class BosparanianTimePickerController(object):
    471
    472 -    def __init__(self, time_picker, on_change):
    473          self.time_picker = time_picker
    474          self.original_bg = self.time_picker.GetBackgroundColour()
    475          self.separator = ":"
    476          self.hour_part = 0
    477          self.minute_part = 1
    478          self.last_selection = None
    479          self.on_change = on_change
    480
    481 -    def get_value(self):
    482          try:
    483              split = self.time_picker._get_time_string().split(self.separator)
    484              if len(split) != 2:
    485                  raise ValueError()
    486              hour_string, minute_string = split
    487              hour = int(hour_string)
    488              minute = int(minute_string)
    489              if not is_valid_time(hour, minute, 0):
    490                  raise ValueError()
    491              return (hour, minute, 0)
    492          except ValueError:
    493              raise ValueError("Invalid time.")
    494
    495 -    def set_value(self, value):
    496          hour, minute, _ = value
    497          time_string = "%02d:%02d" % (hour, minute)
    498          self.time_picker.set_time_string(time_string)
    499          self._on_change()
    500
    501 -    def _on_change(self):
    502          if self._time_is_valid() and not self.on_change is None:
    503              self.on_change()
    504
    505 -    def on_set_focus(self):
    506          if self.last_selection:
    507              start, end = self.last_selection
    508              self.time_picker.SetSelection(start, end)
    509          else:
    510              self._select_part(self.hour_part)
    511
    512 -    def on_kill_focus(self):
    513          self.last_selection = self.time_picker.GetSelection()
    514
    515 -    def on_tab(self):
    516          if self._in_minute_part():
    517              return True
    518          self._select_part(self.minute_part)
    519          return False
    520
    521 -    def on_shift_tab(self):
    522          if self._in_hour_part():
    523              return True
    524          self._select_part(self.hour_part)
    525          return False
    526
    527 -    def on_text_changed(self):
    528          try:
    529              self.get_value()
    530              self.time_picker.SetBackgroundColour(self.original_bg)
    531          except ValueError:
    532              self.time_picker.SetBackgroundColour("pink")
    533          self.time_picker.Refresh()
    534
    535 -    def on_up(self):
    536          def increment_hour(time):
    537              hour, minute, second = time
    538              new_hour = hour + 1
    539              if new_hour > 23:
    540                  new_hour = 0
    541              return (new_hour, minute, second)
    542          def increment_minutes(time):
    543              hour, minute, second = time
    544              new_hour = hour
    545              new_minute = minute + 1
    546              if new_minute > 59:
    547                  new_minute = 0
    548                  new_hour = hour + 1
    549                  if new_hour > 23:
    550                      new_hour = 0
    551              return (new_hour, new_minute, second)
    552          if not self._time_is_valid():
    553              return
    554          selection = self.time_picker.GetSelection()
    555          current_time = self.get_value()
    556          if self._in_hour_part():
    557              new_time = increment_hour(current_time)
    558          else:
    559              new_time = increment_minutes(current_time)
    560          if current_time != new_time:
    561              self._set_new_time_and_restore_selection(new_time, selection)
    562          self._on_change()
    563
    564 -    def on_down(self):
    565          def decrement_hour(time):
    566              hour, minute, second = time
    567              new_hour = hour - 1
    568              if new_hour < 0:
    569                  new_hour = 23
    570              return (new_hour, minute, second)
    571          def decrement_minutes(time):
    572              hour, minute, second = time
    573              new_hour = hour
    574              new_minute = minute - 1
    575              if new_minute < 0:
    576                  new_minute = 59
    577                  new_hour = hour - 1
    578                  if new_hour < 0:
    579                      new_hour = 23
    580              return (new_hour, new_minute, second)
    581          if not self._time_is_valid():
    582              return
    583          selection = self.time_picker.GetSelection()
    584          current_time = self.get_value()
    585          if self._in_hour_part():
    586              new_time = decrement_hour(current_time)
    587          else:
    588              new_time = decrement_minutes(current_time)
    589          if current_time != new_time:
    590              self._set_new_time_and_restore_selection(new_time, selection)
    591          self._on_change()
    592
    593 -    def _set_new_time_and_restore_selection(self, new_time, selection):
    594          def restore_selection(selection):
    595              self.time_picker.SetSelection(selection[0], selection[1])
    596          self.set_value(new_time)
    597          restore_selection(selection)
    598
    599 -    def _time_is_valid(self):
    600          try:
    601              self.get_value()
    602          except ValueError:
    603              return False
    604          return True
    605
    606 -    def _select_part(self, part):
    607          if self._separator_pos() == -1:
    608              return
    609          if part == self.hour_part:
    610              self.time_picker.SetSelection(0, self._separator_pos())
    611          else:
    612              time_string_len = len(self.time_picker._get_time_string())
    613              self.time_picker.SetSelection(self._separator_pos() + 1, time_string_len)
    614          self.preferred_part = part
    615
    616 -    def _in_hour_part(self):
    617          if self._separator_pos() == -1:
    618              return
    619          return self.time_picker.GetInsertionPoint() <= self._separator_pos()
    620
    621 -    def _in_minute_part(self):
    622          if self._separator_pos() == -1:
    623              return
    624          return self.time_picker.GetInsertionPoint() > self._separator_pos()
    625
    626 -    def _separator_pos(self):
    627          return self.time_picker._get_time_string().find(self.separator)
    628