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