Gnumed.wxpython.gmDateTimeInput

Package Gnumed :: Package wxpython :: Module gmDateTimeInput |
[frames] | no frames]

# Source Code for Module Gnumed.wxpython.gmDateTimeInput

      1  """GNUmed date input widget
      2
      3  All GNUmed date input should happen via classes in
      4  this module.
      5
      6  @copyright: author(s)
      7  """
      8  #==============================================================================
      9  __author__  = "K. Hilbert <Karsten.Hilbert@gmx.net>"
     10  __licence__ = "GPL v2 or later (details at http://www.gnu.org)"
     11
     12  # standard libary
     13  import re, string, sys, time, datetime as pyDT, logging
     14
     15
     16  # 3rd party
     17  import wx
     18  try:
     19          import wx.calendar as wxcal
     20  except ImportError:
     21          # Phoenix
     22          import wx.adv as wxcal
     23
     24
     25  # GNUmed specific
     26  if __name__ == '__main__':
     27          sys.path.insert(0, '../../')
     28  from Gnumed.pycommon import gmMatchProvider
     29  from Gnumed.pycommon import gmDateTime
     30  from Gnumed.pycommon import gmI18N
     31  from Gnumed.wxpython import gmPhraseWheel
     32  from Gnumed.wxpython import gmGuiHelpers
     33
     34  _log = logging.getLogger('gm.ui')
     35
     36  #============================================================
     37  #class cIntervalMatchProvider(gmMatchProvider.cMatchProvider):
     38  #       """Turns strings into candidate intervals."""
     39  #       def __init__(self):
     40  #
     41  #               gmMatchProvider.cMatchProvider.__init__(self)
     42  #
     43  #               self.setThresholds(aPhrase = 1, aWord = 998, aSubstring = 999)
     44  #               self.word_separators = None
     45  ##              self.ignored_chars("""[?!."'\\(){}\[\]<>~#*$%^_]+""")
     46  #       #--------------------------------------------------------
     47  #       # external API
     48  #       #--------------------------------------------------------
     49  #       #--------------------------------------------------------
     50  #       # base class API
     51  #       #--------------------------------------------------------
     52  #       def getMatchesByPhrase(self, aFragment):
     53  #               intv = gmDateTime.str2interval(str_interval = aFragment)
     54  #
     55  #               if intv is None:
     56  #                       return (False, [])
     57  #
     58  #               items = [{
     59  #                       'data': intv,
     60  #                       'field_label': gmDateTime.format_interval(intv, gmDateTime.acc_minutes),
     61  #                       'list_label': gmDateTime.format_interval(intv, gmDateTime.acc_minutes)
     62  #               }]
     63  #
     64  #               return (True, items)
     65  #       #--------------------------------------------------------
     66  #       def getMatchesByWord(self, aFragment):
     67  #               return self.getMatchesByPhrase(aFragment)
     68  #       #--------------------------------------------------------
     69  #       def getMatchesBySubstr(self, aFragment):
     70  #               return self.getMatchesByPhrase(aFragment)
     71  #       #--------------------------------------------------------
     72  #       def getAllMatches(self):
     73  #               matches = (False, [])
     74  #               return matches
     75
     76  #============================================================
     77 -class cIntervalPhraseWheel(gmPhraseWheel.cPhraseWheel):
     78
     79 -        def __init__(self, *args, **kwargs):
     80
     81                  gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
     82                  self.phrase_separators = None
     83                  self.display_accuracy = None
     84          #--------------------------------------------------------
     85          # phrasewheel internal API
     86          #--------------------------------------------------------
     87 -        def _update_candidates_in_picklist(self, val):
     88                  intv = gmDateTime.str2interval(str_interval = val)
     89                  if intv is None:
     90                          self._current_match_candidates = []
     91                  else:
     92                          self._current_match_candidates = [{
     93                                  'data': intv,
     94                                  'field_label': gmDateTime.format_interval(intv, gmDateTime.acc_minutes),
     95                                  'list_label': gmDateTime.format_interval(intv, gmDateTime.acc_minutes)
     96                          }]
     97                  self._picklist.SetItems(self._current_match_candidates)
     98          #---------------------------------------------------------
     99  #       def _on_lose_focus(self, event):
    100  #               # are we valid ?
    101  #               if len(self._data) == 0:
    102  #                       self._set_data_to_first_match()
    103  #
    104  #               # let the base class do its thing
    105  #               super(cIntervalPhraseWheel, self)._on_lose_focus(event)
    106          #--------------------------------------------------------
    107 -        def _picklist_item2display_string(self, item=None):
    108                  intv = item['data']
    109                  if intv is not None:
    110                          return gmDateTime.format_interval (
    111                                  interval = intv,
    112                                  accuracy_wanted = self.display_accuracy
    113                          )
    114                  return item['field_label']
    115          #--------------------------------------------------------
    116 -        def _get_data_tooltip(self):
    117                  intv = self.GetData()
    118                  if intv is None:
    119                          return ''
    120                  return gmDateTime.format_interval (
    121                          interval = intv,
    122                          accuracy_wanted = self.display_accuracy
    123                  )
    124          #--------------------------------------------------------
    125          # external API
    126          #--------------------------------------------------------
    127 -        def SetValue(self, value):
    128
    129                  if isinstance(value, pyDT.timedelta):
    130                          self.SetText(data = value, suppress_smarts = True)
    131                          return
    132
    133                  if value is None:
    134                          value = ''
    135
    136                  super(cIntervalPhraseWheel, self).SetValue(value)
    137          #--------------------------------------------------------
    138 -        def SetText(self, value='', data=None, suppress_smarts=False):
    139
    140                  if data is not None:
    141                          if value.strip() == '':
    142                                  value = gmDateTime.format_interval (
    143                                          interval = data,
    144                                          accuracy_wanted = self.display_accuracy
    145                                  )
    146
    147                  super(cIntervalPhraseWheel, self).SetText(value = value, data = data, suppress_smarts = suppress_smarts)
    148          #--------------------------------------------------------
    149 -        def SetData(self, data=None):
    150                  if data is None:
    151                          super(cIntervalPhraseWheel, self).SetText('', None)
    152                          return
    153
    154                  value = gmDateTime.format_interval (
    155                          interval = data,
    156                          accuracy_wanted = self.display_accuracy
    157                  )
    158                  super(cIntervalPhraseWheel, self).SetText(value = value, data = data)
    159          #--------------------------------------------------------
    160 -        def GetData(self):
    161                  if len(self._data) == 0:
    162                          self._set_data_to_first_match()
    163
    164                  return super(cIntervalPhraseWheel, self).GetData()
    165
    166  #============================================================
    167 -class cCalendarDatePickerDlg(wx.Dialog):
    168          """Shows a calendar control from which the user can pick a date."""
    169 -        def __init__(self, parent):
    170
    171                  wx.Dialog.__init__(self, parent, title = _('Pick a date ...'))
    172                  panel = wx.Panel(self, -1)
    173
    174                  sizer = wx.BoxSizer(wx.VERTICAL)
    175                  panel.SetSizer(sizer)
    176
    177                  cal = wxcal.CalendarCtrl(panel)
    178
    179                  if sys.platform != 'win32':
    180                          # gtk truncates the year - this fixes it
    181                          w, h = cal.Size
    182                          cal.Size = (w+25, h)
    183                          cal.MinSize = cal.Size
    184
    185                  sizer.Add(cal, 0)
    186
    187                  button_sizer = wx.BoxSizer(wx.HORIZONTAL)
    188                  button_sizer.Add((0, 0), 1)
    189                  btn_ok = wx.Button(panel, wx.ID_OK)
    190                  btn_ok.SetDefault()
    191                  button_sizer.Add(btn_ok, 0, wx.ALL, 2)
    192                  button_sizer.Add((0, 0), 1)
    193                  btn_can = wx.Button(panel, wx.ID_CANCEL)
    194                  button_sizer.Add(btn_can, 0, wx.ALL, 2)
    195                  button_sizer.Add((0, 0), 1)
    196                  sizer.Add(button_sizer, 1, wx.EXPAND | wx.ALL, 10)
    197                  sizer.Fit(panel)
    198                  self.ClientSize = panel.Size
    199
    200                  cal.Bind(wx.EVT_KEY_DOWN, self.__on_key_down)
    201                  cal.SetFocus()
    202                  self.cal = cal
    203
    204          #-----------------------------------------------------------
    205 -        def __on_key_down(self, evt):
    206                  code = evt.KeyCode
    207                  if code == wx.WXK_TAB:
    208                          self.cal.Navigate()
    209                  elif code in (wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER):
    210                          self.EndModal(wx.ID_OK)
    211                  elif code == wx.WXK_ESCAPE:
    212                          self.EndModal(wx.ID_CANCEL)
    213                  else:
    214                          evt.Skip()
    215
    216  #============================================================
    217 -class cDateMatchProvider(gmMatchProvider.cMatchProvider):
    218          """Turns strings into candidate dates.
    219
    220          Matching on "all" (*, '') will pop up a calendar :-)
    221          """
    222 -        def __init__(self):
    223
    224                  gmMatchProvider.cMatchProvider.__init__(self)
    225
    226                  self.setThresholds(aPhrase = 1, aWord = 998, aSubstring = 999)
    227                  self.word_separators = None
    228  #               self.ignored_chars("""[?!."'\\(){}\[\]<>~#*$%^_]+""")
    229          #--------------------------------------------------------
    230          # external API
    231          #--------------------------------------------------------
    232          #--------------------------------------------------------
    233          # base class API
    234          #--------------------------------------------------------
    235          # internal matching algorithms
    236          #
    237          # if we end up here:
    238          #       - aFragment will not be "None"
    239          #   - aFragment will be lower case
    240          #       - we _do_ deliver matches (whether we find any is a different story)
    241          #--------------------------------------------------------
    242 -        def getMatchesByPhrase(self, aFragment):
    243                  """Return matches for aFragment at start of phrases."""
    244                  matches = gmDateTime.str2pydt_matches(str2parse = aFragment.strip())
    245
    246                  if len(matches) == 0:
    247                          return (False, [])
    248
    249                  items = []
    250                  for match in matches:
    251                          if match['data'] is None:
    252                                  items.append ({
    253                                          'data': None,
    254                                          'field_label': match['label'],
    255                                          'list_label': match['label']
    256                                  })
    257                                  continue
    258
    259                          data = match['data'].replace (
    260                                  hour = 11,
    261                                  minute = 11,
    262                                  second = 11,
    263                                  microsecond = 111111
    264                          )
    265                          list_label = gmDateTime.pydt_strftime (
    266                                  data,
    267                                  format = '%A, %d. %B %Y (%x)',
    268                                  accuracy = gmDateTime.acc_days
    269                          )
    270                          items.append ({
    271                                  'data': data,
    272                                  'field_label': match['label'],
    273                                  'list_label': list_label
    274                          })
    275
    276                  return (True, items)
    277          #--------------------------------------------------------
    278 -        def getMatchesByWord(self, aFragment):
    279                  """Return matches for aFragment at start of words inside phrases."""
    280                  return self.getMatchesByPhrase(aFragment)
    281          #--------------------------------------------------------
    282 -        def getMatchesBySubstr(self, aFragment):
    283                  """Return matches for aFragment as a true substring."""
    284                  return self.getMatchesByPhrase(aFragment)
    285          #--------------------------------------------------------
    286 -        def getAllMatches(self):
    287                  """Return all items."""
    288
    289                  matches = (False, [])
    290                  return matches
    291
    292  #               # consider this:
    293  #               dlg = cCalendarDatePickerDlg(None)
    294  #               # FIXME: show below parent
    295  #               dlg.CentreOnScreen()
    296  #
    297  #               if dlg.ShowModal() == wx.ID_OK:
    298  #                       date = dlg.cal.Date
    299  #                       if date is not None:
    300  #                               if date.IsValid():
    301  #                                       date = gmDateTime.wxDate2py_dt(wxDate = date).replace (
    302  #                                               hour = 11,
    303  #                                               minute = 11,
    304  #                                               second = 11,
    305  #                                               microsecond = 111111
    306  #                                       )
    307  #                                       lbl = gmDateTime.pydt_strftime(date, format = '%Y-%m-%d', accuracy = gmDateTime.acc_days)
    308  #                                       matches = (True, [{'data': date, 'label': lbl}])
    309  #               dlg.Destroy()
    310  #
    311  #               return matches
    312
    313  #============================================================
    314 -class cDateInputPhraseWheel(gmPhraseWheel.cPhraseWheel):
    315
    316 -        def __init__(self, *args, **kwargs):
    317
    318                  gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
    319
    320                  self.matcher = cDateMatchProvider()
    321                  self.phrase_separators = None
    322
    323                  self.static_tooltip_extra = _('<ALT-C/K>: pick from (c/k)alendar')
    324
    325                  self.__weekday_keys = [wx.WXK_F1, wx.WXK_F2, wx.WXK_F3, wx.WXK_F4, wx.WXK_F5, wx.WXK_F6, wx.WXK_F7]
    326
    327          #--------------------------------------------------------
    328          # internal helpers
    329          #--------------------------------------------------------
    330  #       def __text2timestamp(self):
    331  #
    332  #               self._update_candidates_in_picklist(val = self.GetValue().strip())
    333  #
    334  #               if len(self._current_match_candidates) == 1:
    335  #                       return self._current_match_candidates[0]['data']
    336  #
    337  #               return None
    338          #--------------------------------------------------------
    339 -        def __pick_from_calendar(self):
    340                  dlg = cCalendarDatePickerDlg(self)
    341                  # FIXME: show below parent
    342                  dlg.CentreOnScreen()
    343                  decision = dlg.ShowModal()
    344                  date = dlg.cal.Date
    345                  dlg.Destroy()
    346
    347                  if decision != wx.ID_OK:
    348                          return
    349
    350                  if date is None:
    351                          return
    352
    353                  if not date.IsValid():
    354                          return
    355
    356                  date = gmDateTime.wxDate2py_dt(wxDate = date).replace (
    357                          hour = 11,
    358                          minute = 11,
    359                          second = 11,
    360                          microsecond = 111111
    361                  )
    362                  val = gmDateTime.pydt_strftime(date, format = '%Y-%m-%d', accuracy = gmDateTime.acc_days)
    363                  self.SetText(value = val, data = date, suppress_smarts = True)
    364
    365          #--------------------------------------------------------
    366 -        def __pick_from_weekday(self, weekday):
    367                  self.is_valid_timestamp(empty_is_valid = True)
    368                  target_date = gmDateTime.get_date_of_weekday_in_week_of_date(weekday, base_dt = self.date)
    369                  val = gmDateTime.pydt_strftime(target_date, format = '%Y-%m-%d', accuracy = gmDateTime.acc_days)
    370                  self.SetText(value = val, data = target_date, suppress_smarts = True)
    371
    372          #--------------------------------------------------------
    373          # phrasewheel internal API
    374          #--------------------------------------------------------
    375 -        def _on_lose_focus(self, event):
    376                  # no valid date yet ?
    377                  if len(self._data) == 0:
    378                          self._set_data_to_first_match()
    379                          date = self.GetData()
    380                          if date is not None:
    381                                  self.SetValue(gmDateTime.pydt_strftime(date, format = '%Y-%m-%d', accuracy = gmDateTime.acc_days))
    382
    383                  # let the base class do its thing
    384                  super(cDateInputPhraseWheel, self)._on_lose_focus(event)
    385
    386          #--------------------------------------------------------
    387 -        def _picklist_item2display_string(self, item=None):
    388                  data = item['data']
    389                  if data is not None:
    390                          return gmDateTime.pydt_strftime(data, format = '%Y-%m-%d', accuracy = gmDateTime.acc_days)
    391                  return item['field_label']
    392
    393          #--------------------------------------------------------
    394 -        def _on_key_down(self, event):
    395
    396                  if event.GetUnicodeKey() == wx.WXK_NONE:
    397                          key = event.GetKeyCode()
    398                          if key in self.__weekday_keys:
    399                                  self.__pick_from_weekday(self.__weekday_keys.index(key))
    400                                  return
    401
    402                  # <ALT-C> / <ALT-K> -> calendar
    403                  if event.AltDown() is True:
    404                          char = chr(event.GetUnicodeKey())
    405                          if char in 'ckCK':
    406                                  self.__pick_from_calendar()
    407                                  return
    408
    409                  super()._on_key_down(event)
    410
    411          #--------------------------------------------------------
    412 -        def _get_data_tooltip(self):
    413                  if len(self._data) == 0:
    414                          return ''
    415
    416                  date = self.GetData()
    417                  # if match provider only provided completions
    418                  # but not a full date with it
    419                  if date is None:
    420                          return ''
    421
    422                  now = gmDateTime.pydt_now_here()
    423                  if date > now:
    424                          intv = date - now
    425                          template = _('%s\n (a %s in %s)')
    426                  else:
    427                          intv = now - date
    428                          template = _('%s\n (a %s %s ago)')
    429                  return template % (
    430                          gmDateTime.pydt_strftime(date, format = '%B %d %Y -- %c', accuracy = gmDateTime.acc_days),
    431                          gmDateTime.pydt_strftime(date, format = '%A', accuracy = gmDateTime.acc_days),
    432                          gmDateTime.format_interval(interval = intv, accuracy_wanted = gmDateTime.acc_days, verbose = True)
    433                  )
    434
    435          #--------------------------------------------------------
    436          # external API
    437          #--------------------------------------------------------
    438 -        def SetValue(self, value):
    439
    440                  if isinstance(value, pyDT.datetime):
    441                          date = value.replace (
    442                                  hour = 11,
    443                                  minute = 11,
    444                                  second = 11,
    445                                  microsecond = 111111
    446                          )
    447                          self.SetText(data = date, suppress_smarts = True)
    448                          return
    449
    450                  if value is None:
    451                          value = ''
    452
    453                  super().SetValue(value)
    454
    455          #--------------------------------------------------------
    456 -        def SetText(self, value='', data=None, suppress_smarts=False):
    457
    458                  if data is not None:
    459                          if isinstance(data, gmDateTime.cFuzzyTimestamp):
    460                                  data = data.timestamp.replace (
    461                                          hour = 11,
    462                                          minute = 11,
    463                                          second = 11,
    464                                          microsecond = 111111
    465                                  )
    466                          if value.strip() == '':
    467                                  value = gmDateTime.pydt_strftime(data, format = '%Y-%m-%d', accuracy = gmDateTime.acc_days)
    468
    469                  super().SetText(value = value, data = data, suppress_smarts = suppress_smarts)
    470
    471          #--------------------------------------------------------
    472 -        def SetData(self, data=None):
    473                  if data is None:
    474                          gmPhraseWheel.cPhraseWheel.SetText(self, '', None)
    475                          return
    476                  self.SetText(data = data)
    477
    478          #--------------------------------------------------------
    479 -        def GetData(self):
    480                  if len(self._data) == 0:
    481                          self._set_data_to_first_match()
    482
    483                  return super(self.__class__, self).GetData()
    484
    485          #--------------------------------------------------------
    486 -        def is_valid_timestamp(self, empty_is_valid=True):
    487                  if len(self._data) > 0:
    488                          self.display_as_valid(True)
    489                          return True
    490
    491                  if self.GetValue().strip() == '':
    492                          if empty_is_valid:
    493                                  self.display_as_valid(True)
    494                                  return True
    495                          else:
    496                                  self.display_as_valid(False)
    497                                  return False
    498
    499                  # skip showing calendar on '*' from here
    500                  if self.GetValue().strip() == '*':
    501                          self.display_as_valid(False)
    502                          return False
    503
    504                  # try to auto-snap to first match
    505                  self._set_data_to_first_match()
    506                  if len(self._data) == 0:
    507                          self.display_as_valid(False)
    508                          return False
    509
    510                  date = self.GetData()
    511                  self.SetValue(gmDateTime.pydt_strftime(date, format = '%Y-%m-%d', accuracy = gmDateTime.acc_days))#, none_str = u'')
    512                  self.display_as_valid(True)
    513                  return True
    514
    515          #--------------------------------------------------------
    516          # properties
    517          #--------------------------------------------------------
    518 -        def _get_date(self):
    519                  return self.GetData()
    520
    521 -        def _set_date(self, date):
    522                  raise AttributeError('._set_date not implemented')
    523  #               val = gmDateTime.pydt_strftime(date, format = '%Y-%m-%d', accuracy = gmDateTime.acc_days)
    524  #               self.data = date.replace (
    525  #                       hour = 11,
    526  #                       minute = 11,
    527  #                       second = 11,
    528  #                       microsecond = 111111
    529  #               )
    530
    531          date = property(_get_date, _set_date)
    532
    533  #============================================================
    534 -class cMatchProvider_FuzzyTimestamp(gmMatchProvider.cMatchProvider):
    535 -        def __init__(self):
    536                  self.__allow_past = 1
    537                  self.__shifting_base = None
    538
    539                  gmMatchProvider.cMatchProvider.__init__(self)
    540
    541                  self.setThresholds(aPhrase = 1, aWord = 998, aSubstring = 999)
    542                  self.word_separators = None
    543  #               self.ignored_chars("""[?!."'\\(){}\[\]<>~#*$%^_]+""")
    544          #--------------------------------------------------------
    545          # external API
    546          #--------------------------------------------------------
    547          #--------------------------------------------------------
    548          # base class API
    549          #--------------------------------------------------------
    550          # internal matching algorithms
    551          #
    552          # if we end up here:
    553          #       - aFragment will not be "None"
    554          #   - aFragment will be lower case
    555          #       - we _do_ deliver matches (whether we find any is a different story)
    556          #--------------------------------------------------------
    557 -        def getMatchesByPhrase(self, aFragment):
    558                  """Return matches for aFragment at start of phrases."""
    559                  matches = gmDateTime.str2fuzzy_timestamp_matches(aFragment.strip())
    560
    561                  if len(matches) == 0:
    562                          return (False, [])
    563
    564                  items = []
    565                  for match in matches:
    566                          items.append ({
    567                                  'data': match['data'],
    568                                  'field_label': match['label'],
    569                                  'list_label': match['label']
    570                          })
    571
    572                  return (True, items)
    573          #--------------------------------------------------------
    574 -        def getMatchesByWord(self, aFragment):
    575                  """Return matches for aFragment at start of words inside phrases."""
    576                  return self.getMatchesByPhrase(aFragment)
    577          #--------------------------------------------------------
    578 -        def getMatchesBySubstr(self, aFragment):
    579                  """Return matches for aFragment as a true substring."""
    580                  return self.getMatchesByPhrase(aFragment)
    581          #--------------------------------------------------------
    582 -        def getAllMatches(self):
    583                  """Return all items."""
    584                  return (False, [])
    585
    586  #==================================================
    587 -class cFuzzyTimestampInput(gmPhraseWheel.cPhraseWheel):
    588
    589 -        def __init__(self, *args, **kwargs):
    590
    591                  gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
    592
    593                  self.matcher = cMatchProvider_FuzzyTimestamp()
    594                  self.phrase_separators = None
    595                  self.selection_only = True
    596                  self.selection_only_error_msg = _('Cannot interpret input as timestamp.')
    597                  self.display_accuracy = None
    598
    599                  self.__weekday_keys = [wx.WXK_F1, wx.WXK_F2, wx.WXK_F3, wx.WXK_F4, wx.WXK_F5, wx.WXK_F6, wx.WXK_F7]
    600
    601          #--------------------------------------------------------
    602          # internal helpers
    603          #--------------------------------------------------------
    604 -        def __text2timestamp(self, val=None):
    605                  if val is None:
    606                          val = self.GetValue()
    607                  val = val.strip()
    608                  if val == '':
    609                          return None
    610                  success, matches = self.matcher.getMatchesByPhrase(val)
    611                  if len(matches) == 1:
    612                          return matches[0]['data']
    613                  return None
    614
    615          #--------------------------------------------------------
    616 -        def __pick_from_weekday(self, weekday):
    617                  self.is_valid_timestamp(empty_is_valid = True)
    618                  dt = self.GetData()
    619                  if dt is not None:
    620                          dt = dt.timestamp
    621                  target_date = gmDateTime.get_date_of_weekday_in_week_of_date(weekday, base_dt = dt)
    622                  self.SetText(data = target_date, suppress_smarts = True)
    623
    624          #--------------------------------------------------------
    625          # phrasewheel internal API
    626          #--------------------------------------------------------
    627 -        def _on_lose_focus(self, event):
    628                  # are we valid ?
    629                  if self.data is None:
    630                          # no, so try
    631                          date = self.__text2timestamp()
    632                          if date is not None:
    633                                  self.SetValue(value = date.format_accurately(accuracy = self.display_accuracy))
    634                                  self.data = date
    635
    636                  # let the base class do its thing
    637                  gmPhraseWheel.cPhraseWheel._on_lose_focus(self, event)
    638
    639          #--------------------------------------------------------
    640 -        def _picklist_item2display_string(self, item=None):
    641                  data = item['data']
    642                  if data is not None:
    643                          return data.format_accurately(accuracy = self.display_accuracy)
    644                  return item['field_label']
    645
    646          #--------------------------------------------------------
    647 -        def _on_key_down(self, event):
    648
    649                  if event.GetUnicodeKey() == wx.WXK_NONE:
    650                          key = event.GetKeyCode()
    651                          if key in self.__weekday_keys:
    652                                  self.__pick_from_weekday(self.__weekday_keys.index(key))
    653                                  return
    654
    655  #               # <ALT-C> / <ALT-K> -> calendar
    656  #               if event.AltDown() is True:
    657  #                       char = chr(event.GetUnicodeKey())
    658  #                       if char in 'ckCK':
    659  #                               self.__pick_from_calendar()
    660  #                               return
    661
    662                  super()._on_key_down(event)
    663
    664          #--------------------------------------------------------
    665 -        def _get_data_tooltip(self):
    666                  if len(self._data) == 0:
    667                          return ''
    668
    669                  date = self.GetData()
    670                  # if match provider only provided completions
    671                  # but not a full date with it
    672                  if date is None:
    673                          return ''
    674                  ts = date.timestamp
    675                  now = gmDateTime.pydt_now_here()
    676                  if ts > now:
    677                          intv = ts - now
    678                          template = _('%s\n %s\n in %s')
    679                  else:
    680                          intv = now - ts
    681                          template = _('%s\n%s\n%s ago')
    682                  txt = template % (
    683                          date.format_accurately(self.display_accuracy),
    684                          gmDateTime.pydt_strftime (
    685                                  ts,
    686                                  format = '%A, %B-%d %Y (%c)',
    687                          ),
    688                          gmDateTime.format_interval (
    689                                  interval = intv,
    690                                  accuracy_wanted = gmDateTime.acc_days,
    691                                  verbose = True
    692                          )
    693                  )
    694                  return txt
    695
    696          #--------------------------------------------------------
    697          # external API
    698          #--------------------------------------------------------
    699 -        def SetText(self, value='', data=None, suppress_smarts=False):
    700
    701                  if data is not None:
    702                          if isinstance(data, pyDT.datetime):
    703                                  data = gmDateTime.cFuzzyTimestamp(timestamp = data)
    704                          if value.strip() == '':
    705                                  value = data.format_accurately(accuracy = self.display_accuracy)
    706
    707                  super().SetText(value = value, data = data, suppress_smarts = suppress_smarts)
    708
    709          #--------------------------------------------------------
    710 -        def SetData(self, data=None):
    711                  if data is None:
    712                          gmPhraseWheel.cPhraseWheel.SetText(self, '', None)
    713                  else:
    714                          if isinstance(data, pyDT.datetime):
    715                                  data = gmDateTime.cFuzzyTimestamp(timestamp=data)
    716                          gmPhraseWheel.cPhraseWheel.SetText(self, value = data.format_accurately(accuracy = self.display_accuracy), data = data)
    717
    718          #--------------------------------------------------------
    719 -        def is_valid_timestamp(self, empty_is_valid=True):
    720                  if self.GetData() is not None:
    721                          return True
    722
    723                  # skip empty value
    724                  if self.GetValue().strip() == '':
    725                          if empty_is_valid:
    726                                  return True
    727                          return False
    728
    729                  date = self.__text2timestamp()
    730                  if date is None:
    731                          return False
    732
    733                  self.SetText (
    734                          value = date.format_accurately(accuracy = self.display_accuracy),
    735                          data = date,
    736                          suppress_smarts = True
    737                  )
    738
    739                  return True
    740
    741  #==================================================
    742  # main
    743  #--------------------------------------------------
    744  if __name__ == '__main__':
    745
    746          if len(sys.argv) < 2:
    747                  sys.exit()
    748
    749          if sys.argv[1] != 'test':
    750                  sys.exit()
    751
    752          gmI18N.activate_locale()
    753          gmI18N.install_domain(domain='gnumed')
    754          gmDateTime.init()
    755
    756          #----------------------------------------------------
    757 -        def test_cli():
    758                  mp = cMatchProvider_FuzzyTimestamp()
    759                  mp.word_separators = None
    760                  mp.setThresholds(aWord = 998, aSubstring = 999)
    761                  val = None
    762                  while val != 'exit':
    763                          print("************************************")
    764                          val = input('Enter date fragment ("exit" to quit): ')
    765                          found, matches = mp.getMatches(aFragment=val)
    766                          for match in matches:
    767                                  #print match
    768                                  print(match['label'])
    769                                  print(match['data'])
    770                                  print("---------------")
    771          #--------------------------------------------------------
    772 -        def test_fuzzy_picker():
    773                  app = wx.PyWidgetTester(size = (300, 40))
    774                  app.SetWidget(cFuzzyTimestampInput, id=-1, size=(180,20), pos=(10,20))
    775                  app.MainLoop()
    776          #--------------------------------------------------------
    777 -        def test_picker():
    778                  app = wx.PyWidgetTester(size = (300, 40))
    779                  app.SetWidget(cDateInputPhraseWheel, id=-1, size=(180,20), pos=(10,20))
    780                  app.MainLoop()
    781          #--------------------------------------------------------
    782          #test_cli()
    783          #test_fuzzy_picker()
    784          test_picker()
    785
    786  #==================================================
    787