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 - def __on_key_down(self, evt):
205 code = evt.KeyCode 206 if code == wx.WXK_TAB: 207 self.cal.Navigate() 208 elif code in (wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER): 209 self.EndModal(wx.ID_OK) 210 elif code == wx.WXK_ESCAPE: 211 self.EndModal(wx.ID_CANCEL) 212 else: 213 evt.Skip()
214 215 #============================================================
216 -class cDateMatchProvider(gmMatchProvider.cMatchProvider):
217 """Turns strings into candidate dates. 218 219 Matching on "all" (*, '') will pop up a calendar :-) 220 """
221 - def __init__(self):
222 223 gmMatchProvider.cMatchProvider.__init__(self) 224 225 self.setThresholds(aPhrase = 1, aWord = 998, aSubstring = 999) 226 self.word_separators = None
227 # self.ignored_chars("""[?!."'\\(){}\[\]<>~#*$%^_]+""") 228 #-------------------------------------------------------- 229 # external API 230 #-------------------------------------------------------- 231 #-------------------------------------------------------- 232 # base class API 233 #-------------------------------------------------------- 234 # internal matching algorithms 235 # 236 # if we end up here: 237 # - aFragment will not be "None" 238 # - aFragment will be lower case 239 # - we _do_ deliver matches (whether we find any is a different story) 240 #--------------------------------------------------------
241 - def getMatchesByPhrase(self, aFragment):
242 """Return matches for aFragment at start of phrases.""" 243 matches = gmDateTime.str2pydt_matches(str2parse = aFragment.strip()) 244 245 if len(matches) == 0: 246 return (False, []) 247 248 items = [] 249 for match in matches: 250 if match['data'] is None: 251 items.append ({ 252 'data': None, 253 'field_label': match['label'], 254 'list_label': match['label'] 255 }) 256 continue 257 258 data = match['data'].replace ( 259 hour = 11, 260 minute = 11, 261 second = 11, 262 microsecond = 111111 263 ) 264 list_label = gmDateTime.pydt_strftime ( 265 data, 266 format = '%A, %d. %B %Y (%x)', 267 accuracy = gmDateTime.acc_days 268 ) 269 items.append ({ 270 'data': data, 271 'field_label': match['label'], 272 'list_label': list_label 273 }) 274 275 return (True, items)
276 #--------------------------------------------------------
277 - def getMatchesByWord(self, aFragment):
278 """Return matches for aFragment at start of words inside phrases.""" 279 return self.getMatchesByPhrase(aFragment)
280 #--------------------------------------------------------
281 - def getMatchesBySubstr(self, aFragment):
282 """Return matches for aFragment as a true substring.""" 283 return self.getMatchesByPhrase(aFragment)
284 #--------------------------------------------------------
285 - def getAllMatches(self):
286 """Return all items.""" 287 288 matches = (False, []) 289 return matches
290 291 # # consider this: 292 # dlg = cCalendarDatePickerDlg(None) 293 # # FIXME: show below parent 294 # dlg.CentreOnScreen() 295 # 296 # if dlg.ShowModal() == wx.ID_OK: 297 # date = dlg.cal.Date 298 # if date is not None: 299 # if date.IsValid(): 300 # date = gmDateTime.wxDate2py_dt(wxDate = date).replace ( 301 # hour = 11, 302 # minute = 11, 303 # second = 11, 304 # microsecond = 111111 305 # ) 306 # lbl = gmDateTime.pydt_strftime(date, format = '%Y-%m-%d', accuracy = gmDateTime.acc_days) 307 # matches = (True, [{'data': date, 'label': lbl}]) 308 # dlg.Destroy() 309 # 310 # return matches 311 312 #============================================================
313 -class cDateInputPhraseWheel(gmPhraseWheel.cPhraseWheel):
314
315 - def __init__(self, *args, **kwargs):
316 317 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs) 318 319 self.matcher = cDateMatchProvider() 320 self.phrase_separators = None 321 322 self.static_tooltip_extra = _('<ALT-C/K>: pick from (c/k)alendar')
323 #-------------------------------------------------------- 324 # internal helpers 325 #-------------------------------------------------------- 326 # def __text2timestamp(self): 327 # 328 # self._update_candidates_in_picklist(val = self.GetValue().strip()) 329 # 330 # if len(self._current_match_candidates) == 1: 331 # return self._current_match_candidates[0]['data'] 332 # 333 # return None 334 #--------------------------------------------------------
335 - def __pick_from_calendar(self):
336 dlg = cCalendarDatePickerDlg(self) 337 # FIXME: show below parent 338 dlg.CentreOnScreen() 339 decision = dlg.ShowModal() 340 date = dlg.cal.Date 341 dlg.Destroy() 342 343 if decision != wx.ID_OK: 344 return 345 346 if date is None: 347 return 348 349 if not date.IsValid(): 350 return 351 352 date = gmDateTime.wxDate2py_dt(wxDate = date).replace ( 353 hour = 11, 354 minute = 11, 355 second = 11, 356 microsecond = 111111 357 ) 358 val = gmDateTime.pydt_strftime(date, format = '%Y-%m-%d', accuracy = gmDateTime.acc_days) 359 self.SetText(value = val, data = date, suppress_smarts = True)
360 361 #-------------------------------------------------------- 362 # phrasewheel internal API 363 #--------------------------------------------------------
364 - def _on_lose_focus(self, event):
365 # no valid date yet ? 366 if len(self._data) == 0: 367 self._set_data_to_first_match() 368 date = self.GetData() 369 if date is not None: 370 self.SetValue(gmDateTime.pydt_strftime(date, format = '%Y-%m-%d', accuracy = gmDateTime.acc_days)) 371 372 # let the base class do its thing 373 super(cDateInputPhraseWheel, self)._on_lose_focus(event)
374 375 #--------------------------------------------------------
376 - def _picklist_item2display_string(self, item=None):
377 data = item['data'] 378 if data is not None: 379 return gmDateTime.pydt_strftime(data, format = '%Y-%m-%d', accuracy = gmDateTime.acc_days) 380 return item['field_label']
381 382 #--------------------------------------------------------
383 - def _on_key_down(self, event):
384 385 # <ALT-C> / <ALT-K> -> calendar 386 if event.AltDown() is True: 387 char = chr(event.GetUnicodeKey()) 388 if char in 'ckCK': 389 self.__pick_from_calendar() 390 return 391 392 super(cDateInputPhraseWheel, self)._on_key_down(event)
393 394 #--------------------------------------------------------
395 - def _get_data_tooltip(self):
396 if len(self._data) == 0: 397 return '' 398 399 date = self.GetData() 400 # if match provider only provided completions 401 # but not a full date with it 402 if date is None: 403 return '' 404 405 return gmDateTime.pydt_strftime ( 406 date, 407 format = '%A, %d. %B %Y (%x)', 408 accuracy = gmDateTime.acc_days 409 )
410 411 #-------------------------------------------------------- 412 # external API 413 #--------------------------------------------------------
414 - def SetValue(self, value):
415 416 if isinstance(value, pyDT.datetime): 417 date = value.replace ( 418 hour = 11, 419 minute = 11, 420 second = 11, 421 microsecond = 111111 422 ) 423 self.SetText(data = date, suppress_smarts = True) 424 return 425 426 if value is None: 427 value = '' 428 429 super(self.__class__, self).SetValue(value)
430 431 #--------------------------------------------------------
432 - def SetText(self, value='', data=None, suppress_smarts=False):
433 434 if data is not None: 435 if isinstance(data, gmDateTime.cFuzzyTimestamp): 436 data = data.timestamp.replace ( 437 hour = 11, 438 minute = 11, 439 second = 11, 440 microsecond = 111111 441 ) 442 if value.strip() == '': 443 value = gmDateTime.pydt_strftime(data, format = '%Y-%m-%d', accuracy = gmDateTime.acc_days) 444 445 super(self.__class__, self).SetText(value = value, data = data, suppress_smarts = suppress_smarts)
446 447 #--------------------------------------------------------
448 - def SetData(self, data=None):
449 if data is None: 450 gmPhraseWheel.cPhraseWheel.SetText(self, '', None) 451 return 452 self.SetText(data = data)
453 454 #--------------------------------------------------------
455 - def GetData(self):
456 if len(self._data) == 0: 457 self._set_data_to_first_match() 458 459 return super(self.__class__, self).GetData()
460 461 #--------------------------------------------------------
462 - def is_valid_timestamp(self, allow_empty=True):
463 if len(self._data) > 0: 464 self.display_as_valid(True) 465 return True 466 467 if self.GetValue().strip() == '': 468 if allow_empty: 469 self.display_as_valid(True) 470 return True 471 else: 472 self.display_as_valid(False) 473 return False 474 475 # skip showing calendar on '*' from here 476 if self.GetValue().strip() == '*': 477 self.display_as_valid(False) 478 return False 479 480 # try to auto-snap to first match 481 self._set_data_to_first_match() 482 if len(self._data) == 0: 483 self.display_as_valid(False) 484 return False 485 486 date = self.GetData() 487 self.SetValue(gmDateTime.pydt_strftime(date, format = '%Y-%m-%d', accuracy = gmDateTime.acc_days))#, none_str = u'') 488 self.display_as_valid(True) 489 return True
490 491 #-------------------------------------------------------- 492 # properties 493 #--------------------------------------------------------
494 - def _get_date(self):
495 return self.GetData()
496
497 - def _set_date(self, date):
498 raise AttributeError('._set_date not implemented')
499 # val = gmDateTime.pydt_strftime(date, format = '%Y-%m-%d', accuracy = gmDateTime.acc_days) 500 # self.data = date.replace ( 501 # hour = 11, 502 # minute = 11, 503 # second = 11, 504 # microsecond = 111111 505 # ) 506 507 date = property(_get_date, _set_date)
508 509 #============================================================
510 -class cMatchProvider_FuzzyTimestamp(gmMatchProvider.cMatchProvider):
511 - def __init__(self):
512 self.__allow_past = 1 513 self.__shifting_base = None 514 515 gmMatchProvider.cMatchProvider.__init__(self) 516 517 self.setThresholds(aPhrase = 1, aWord = 998, aSubstring = 999) 518 self.word_separators = None
519 # self.ignored_chars("""[?!."'\\(){}\[\]<>~#*$%^_]+""") 520 #-------------------------------------------------------- 521 # external API 522 #-------------------------------------------------------- 523 #-------------------------------------------------------- 524 # base class API 525 #-------------------------------------------------------- 526 # internal matching algorithms 527 # 528 # if we end up here: 529 # - aFragment will not be "None" 530 # - aFragment will be lower case 531 # - we _do_ deliver matches (whether we find any is a different story) 532 #--------------------------------------------------------
533 - def getMatchesByPhrase(self, aFragment):
534 """Return matches for aFragment at start of phrases.""" 535 matches = gmDateTime.str2fuzzy_timestamp_matches(aFragment.strip()) 536 537 if len(matches) == 0: 538 return (False, []) 539 540 items = [] 541 for match in matches: 542 items.append ({ 543 'data': match['data'], 544 'field_label': match['label'], 545 'list_label': match['label'] 546 }) 547 548 return (True, items)
549 #--------------------------------------------------------
550 - def getMatchesByWord(self, aFragment):
551 """Return matches for aFragment at start of words inside phrases.""" 552 return self.getMatchesByPhrase(aFragment)
553 #--------------------------------------------------------
554 - def getMatchesBySubstr(self, aFragment):
555 """Return matches for aFragment as a true substring.""" 556 return self.getMatchesByPhrase(aFragment)
557 #--------------------------------------------------------
558 - def getAllMatches(self):
559 """Return all items.""" 560 return (False, [])
561 562 #==================================================
563 -class cFuzzyTimestampInput(gmPhraseWheel.cPhraseWheel):
564
565 - def __init__(self, *args, **kwargs):
566 567 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs) 568 569 self.matcher = cMatchProvider_FuzzyTimestamp() 570 self.phrase_separators = None 571 self.selection_only = True 572 self.selection_only_error_msg = _('Cannot interpret input as timestamp.') 573 self.display_accuracy = None
574 575 #-------------------------------------------------------- 576 # internal helpers 577 #--------------------------------------------------------
578 - def __text2timestamp(self, val=None):
579 if val is None: 580 val = self.GetValue() 581 val = val.strip() 582 if val == '': 583 return None 584 success, matches = self.matcher.getMatchesByPhrase(val) 585 if len(matches) == 1: 586 return matches[0]['data'] 587 return None
588 589 #-------------------------------------------------------- 590 # phrasewheel internal API 591 #--------------------------------------------------------
592 - def _on_lose_focus(self, event):
593 # are we valid ? 594 if self.data is None: 595 # no, so try 596 date = self.__text2timestamp() 597 if date is not None: 598 self.SetValue(value = date.format_accurately(accuracy = self.display_accuracy)) 599 self.data = date 600 601 # let the base class do its thing 602 gmPhraseWheel.cPhraseWheel._on_lose_focus(self, event)
603 604 #--------------------------------------------------------
605 - def _picklist_item2display_string(self, item=None):
606 data = item['data'] 607 if data is not None: 608 return data.format_accurately(accuracy = self.display_accuracy) 609 return item['field_label']
610 611 #-------------------------------------------------------- 612 # external API 613 #--------------------------------------------------------
614 - def SetText(self, value='', data=None, suppress_smarts=False):
615 616 if data is not None: 617 if isinstance(data, pyDT.datetime): 618 data = gmDateTime.cFuzzyTimestamp(timestamp=data) 619 if value.strip() == '': 620 value = data.format_accurately(accuracy = self.display_accuracy) 621 622 gmPhraseWheel.cPhraseWheel.SetText(self, value = value, data = data, suppress_smarts = suppress_smarts)
623 624 #--------------------------------------------------------
625 - def SetData(self, data=None):
626 if data is None: 627 gmPhraseWheel.cPhraseWheel.SetText(self, '', None) 628 else: 629 if isinstance(data, pyDT.datetime): 630 data = gmDateTime.cFuzzyTimestamp(timestamp=data) 631 gmPhraseWheel.cPhraseWheel.SetText(self, value = data.format_accurately(accuracy = self.display_accuracy), data = data)
632 633 #--------------------------------------------------------
634 - def is_valid_timestamp(self, empty_is_valid=True):
635 if self.GetData() is not None: 636 return True 637 638 # skip empty value 639 if self.GetValue().strip() == '': 640 if empty_is_valid: 641 return True 642 return False 643 644 date = self.__text2timestamp() 645 if date is None: 646 return False 647 648 self.SetText ( 649 value = date.format_accurately(accuracy = self.display_accuracy), 650 data = date, 651 suppress_smarts = True 652 ) 653 654 return True
655 656 #================================================== 657 # main 658 #-------------------------------------------------- 659 if __name__ == '__main__': 660 661 if len(sys.argv) < 2: 662 sys.exit() 663 664 if sys.argv[1] != 'test': 665 sys.exit() 666 667 gmI18N.activate_locale() 668 gmI18N.install_domain(domain='gnumed') 669 gmDateTime.init() 670 671 #----------------------------------------------------
672 - def test_cli():
673 mp = cMatchProvider_FuzzyTimestamp() 674 mp.word_separators = None 675 mp.setThresholds(aWord = 998, aSubstring = 999) 676 val = None 677 while val != 'exit': 678 print("************************************") 679 val = input('Enter date fragment ("exit" to quit): ') 680 found, matches = mp.getMatches(aFragment=val) 681 for match in matches: 682 #print match 683 print(match['label']) 684 print(match['data']) 685 print("---------------")
686 #--------------------------------------------------------
687 - def test_fuzzy_picker():
688 app = wx.PyWidgetTester(size = (300, 40)) 689 app.SetWidget(cFuzzyTimestampInput, id=-1, size=(180,20), pos=(10,20)) 690 app.MainLoop()
691 #--------------------------------------------------------
692 - def test_picker():
693 app = wx.PyWidgetTester(size = (300, 40)) 694 app.SetWidget(cDateInputPhraseWheel, id=-1, size=(180,20), pos=(10,20)) 695 app.MainLoop()
696 #-------------------------------------------------------- 697 #test_cli() 698 #test_fuzzy_picker() 699 test_picker() 700 701 #================================================== 702