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 (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, gmGuiHelpers 
 30   
 31  _log = logging.getLogger('gm.ui') 
 32   
 33  #============================================================ 
34 -class cCalendarDatePickerDlg(wx.Dialog):
35 """Shows a calendar control from which the user can pick a date."""
36 - def __init__(self, parent):
37 38 wx.Dialog.__init__(self, parent, title = _('Pick a date ...')) 39 panel = wx.Panel(self, -1) 40 41 sizer = wx.BoxSizer(wx.VERTICAL) 42 panel.SetSizer(sizer) 43 44 cal = wx.calendar.CalendarCtrl(panel) 45 46 if sys.platform != 'win32': 47 # gtk truncates the year - this fixes it 48 w, h = cal.Size 49 cal.Size = (w+25, h) 50 cal.MinSize = cal.Size 51 52 sizer.Add(cal, 0) 53 54 button_sizer = wx.BoxSizer(wx.HORIZONTAL) 55 button_sizer.Add((0, 0), 1) 56 btn_ok = wx.Button(panel, wx.ID_OK) 57 btn_ok.SetDefault() 58 button_sizer.Add(btn_ok, 0, wx.ALL, 2) 59 button_sizer.Add((0, 0), 1) 60 btn_can = wx.Button(panel, wx.ID_CANCEL) 61 button_sizer.Add(btn_can, 0, wx.ALL, 2) 62 button_sizer.Add((0, 0), 1) 63 sizer.Add(button_sizer, 1, wx.EXPAND | wx.ALL, 10) 64 sizer.Fit(panel) 65 self.ClientSize = panel.Size 66 67 cal.Bind(wx.EVT_KEY_DOWN, self.__on_key_down) 68 cal.SetFocus() 69 self.cal = cal
70 #-----------------------------------------------------------
71 - def __on_key_down(self, evt):
72 code = evt.KeyCode 73 if code == wx.WXK_TAB: 74 self.cal.Navigate() 75 elif code in (wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER): 76 self.EndModal(wx.ID_OK) 77 elif code == wx.WXK_ESCAPE: 78 self.EndModal(wx.ID_CANCEL) 79 else: 80 evt.Skip()
81 82 #============================================================
83 -class cDateMatchProvider(gmMatchProvider.cMatchProvider):
84 """Turns strings into candidate dates. 85 86 Matching on "all" (*, '') will pop up a calendar :-) 87 """
88 - def __init__(self):
89 90 gmMatchProvider.cMatchProvider.__init__(self) 91 92 self.setThresholds(aPhrase = 1, aWord = 998, aSubstring = 999) 93 self.word_separators = None
94 # self.ignored_chars("""[?!."'\\(){}\[\]<>~#*$%^_]+""") 95 #-------------------------------------------------------- 96 # external API 97 #-------------------------------------------------------- 98 #-------------------------------------------------------- 99 # base class API 100 #-------------------------------------------------------- 101 # internal matching algorithms 102 # 103 # if we end up here: 104 # - aFragment will not be "None" 105 # - aFragment will be lower case 106 # - we _do_ deliver matches (whether we find any is a different story) 107 #--------------------------------------------------------
108 - def getMatchesByPhrase(self, aFragment):
109 """Return matches for aFragment at start of phrases.""" 110 matches = gmDateTime.str2pydt_matches(str2parse = aFragment.strip()) 111 if len(matches) > 0: 112 return (True, matches) 113 else: 114 return (False, [])
115 #--------------------------------------------------------
116 - def getMatchesByWord(self, aFragment):
117 """Return matches for aFragment at start of words inside phrases.""" 118 return self.getMatchesByPhrase(aFragment)
119 #--------------------------------------------------------
120 - def getMatchesBySubstr(self, aFragment):
121 """Return matches for aFragment as a true substring.""" 122 return self.getMatchesByPhrase(aFragment)
123 #--------------------------------------------------------
124 - def getAllMatches(self):
125 """Return all items.""" 126 127 matches = (False, []) 128 return matches 129 130 dlg = cCalendarDatePickerDlg(None) 131 # FIXME: show below parent 132 dlg.CentreOnScreen() 133 134 if dlg.ShowModal() == wx.ID_OK: 135 date = dlg.cal.Date 136 if date is not None: 137 if date.IsValid(): 138 date = gmDateTime.wxDate2py_dt(wxDate = date) 139 matches = (True, [{'data': date, 'label': date.strftime('%Y-%m-%d')}]) 140 dlg.Destroy() 141 142 return matches
143 #============================================================
144 -class cDateInputPhraseWheel(gmPhraseWheel.cPhraseWheel):
145
146 - def __init__(self, *args, **kwargs):
147 148 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs) 149 150 self.matcher = cDateMatchProvider() 151 self.phrase_separators = None 152 153 self.static_tooltip_extra = _('<ALT-C>: pick from calendar')
154 #-------------------------------------------------------- 155 # internal helpers 156 #--------------------------------------------------------
157 - def __text2timestamp(self, val=None):
158 159 if val is None: 160 val = self.GetValue().strip() 161 162 success, matches = self.matcher.getMatchesByPhrase(val) 163 164 if len(matches) == 1: 165 return matches[0]['data'] 166 167 return None
168 #--------------------------------------------------------
169 - def __pick_from_calendar(self):
170 dlg = cCalendarDatePickerDlg(self) 171 # FIXME: show below parent 172 dlg.CentreOnScreen() 173 decision = dlg.ShowModal() 174 date = dlg.cal.Date 175 dlg.Destroy() 176 177 if decision != wx.ID_OK: 178 return 179 180 if date is None: 181 return 182 183 if not date.IsValid(): 184 return 185 186 date = gmDateTime.wxDate2py_dt(wxDate = date) 187 self.SetText(value = date.strftime('%Y-%m-%d'), data = date, suppress_smarts = True)
188 #-------------------------------------------------------- 189 # phrasewheel internal API 190 #--------------------------------------------------------
191 - def _on_lose_focus(self, event):
192 # are we valid ? 193 if self.data is None: 194 # no, so try 195 self.data = self.__text2timestamp() 196 197 # let the base class do its thing 198 super(self.__class__, self)._on_lose_focus(event)
199 #--------------------------------------------------------
201 data = self._picklist.GetSelectedItemData() 202 if data is not None: 203 return data.strftime('%Y-%m-%d') 204 return self._picklist.get_selected_item_label()
205 #--------------------------------------------------------
206 - def _on_key_down(self, event):
207 208 # <ALT-C> / <ALT-K> -> calendar 209 if event.AltDown() is True: 210 char = unichr(event.GetUnicodeKey()) 211 if char in u'ckCK': 212 self.__pick_from_calendar() 213 return 214 215 super(self.__class__, self)._on_key_down(event)
216 #--------------------------------------------------------
217 - def _get_data_tooltip(self):
218 if self.data is None: 219 return u'' 220 221 return self.data.strftime('%A, %d. %B %Y (%x)').decode(gmI18N.get_encoding())
222 #-------------------------------------------------------- 223 # external API 224 #--------------------------------------------------------
225 - def SetValue(self, value):
226 227 if isinstance(value, pyDT.datetime): 228 self.SetText(data = value, suppress_smarts = True) 229 return 230 231 if value is None: 232 value = u'' 233 234 super(self.__class__, self).SetValue(value)
235 #--------------------------------------------------------
236 - def SetText(self, value=u'', data=None, suppress_smarts=False):
237 238 if data is not None: 239 if isinstance(data, gmDateTime.cFuzzyTimestamp): 240 data = data.timestamp 241 if value.strip() == u'': 242 value = data.strftime('%Y-%m-%d') 243 244 super(self.__class__, self).SetText(value = value, data = data, suppress_smarts = suppress_smarts)
245 #--------------------------------------------------------
246 - def SetData(self, data=None):
247 if data is None: 248 gmPhraseWheel.cPhraseWheel.SetText(self, u'', None) 249 else: 250 if isinstance(data, gmDateTime.cFuzzyTimestamp): 251 data = data.timestamp 252 super(self.__class__, self).SetText(value = data.strftime('%Y-%m-%d'), data = data)
253 #--------------------------------------------------------
254 - def GetData(self):
255 if self.data is None: 256 self.data = self.__text2timestamp() 257 258 return super(self.__class__, self).GetData()
259 #--------------------------------------------------------
260 - def is_valid_timestamp(self, allow_empty=True):
261 if self.data is not None: 262 self.display_as_valid(True) 263 return True 264 265 if self.GetValue().strip() == u'': 266 if allow_empty: 267 self.display_as_valid(True) 268 return True 269 else: 270 self.display_as_valid(False) 271 return False 272 273 # skip showing calendar on '*' from here 274 if self.GetValue().strip() == u'*': 275 self.display_as_valid(False) 276 return False 277 278 self.data = self.__text2timestamp() 279 if self.data is None: 280 self.display_as_valid(False) 281 return False 282 283 self.display_as_valid(True) 284 return True
285 #-------------------------------------------------------- 286 # properties 287 #--------------------------------------------------------
288 - def _get_date(self):
289 return self.data
290
291 - def _set_date(self, date):
292 self.data = date
293 294 date = property(_get_date, _set_date)
295 #============================================================
296 -class cMatchProvider_FuzzyTimestamp(gmMatchProvider.cMatchProvider):
297 - def __init__(self):
298 self.__allow_past = 1 299 self.__shifting_base = None 300 301 gmMatchProvider.cMatchProvider.__init__(self) 302 303 self.setThresholds(aPhrase = 1, aWord = 998, aSubstring = 999) 304 self.word_separators = None
305 # self.ignored_chars("""[?!."'\\(){}\[\]<>~#*$%^_]+""") 306 #-------------------------------------------------------- 307 # external API 308 #-------------------------------------------------------- 309 #-------------------------------------------------------- 310 # base class API 311 #-------------------------------------------------------- 312 # internal matching algorithms 313 # 314 # if we end up here: 315 # - aFragment will not be "None" 316 # - aFragment will be lower case 317 # - we _do_ deliver matches (whether we find any is a different story) 318 #--------------------------------------------------------
319 - def getMatchesByPhrase(self, aFragment):
320 """Return matches for aFragment at start of phrases.""" 321 self.__now = mxDT.now() 322 matches = gmDateTime.str2fuzzy_timestamp_matches(aFragment.strip()) 323 if len(matches) > 0: 324 return (True, matches) 325 else: 326 return (False, [])
327 #--------------------------------------------------------
328 - def getMatchesByWord(self, aFragment):
329 """Return matches for aFragment at start of words inside phrases.""" 330 return self.getMatchesByPhrase(aFragment)
331 #--------------------------------------------------------
332 - def getMatchesBySubstr(self, aFragment):
333 """Return matches for aFragment as a true substring.""" 334 return self.getMatchesByPhrase(aFragment)
335 #--------------------------------------------------------
336 - def getAllMatches(self):
337 """Return all items.""" 338 # FIXME: popup calendar to pick from 339 return (False, [])
340 #==================================================
341 -class cFuzzyTimestampInput(gmPhraseWheel.cPhraseWheel):
342
343 - def __init__(self, *args, **kwargs):
344 345 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs) 346 347 self.matcher = cMatchProvider_FuzzyTimestamp() 348 self.phrase_separators = None 349 self.selection_only = True 350 self.selection_only_error_msg = _('Cannot interpret input as timestamp.')
351 #-------------------------------------------------------- 352 # internal helpers 353 #--------------------------------------------------------
354 - def __text2timestamp(self, val=None):
355 356 if val is None: 357 val = self.GetValue().strip() 358 359 success, matches = self.matcher.getMatchesByPhrase(val) 360 if len(matches) == 1: 361 return matches[0]['data'] 362 363 return None
364 #-------------------------------------------------------- 365 # phrasewheel internal API 366 #--------------------------------------------------------
367 - def _on_lose_focus(self, event):
368 # are we valid ? 369 if self.data is None: 370 # no, so try 371 self.data = self.__text2timestamp() 372 373 # let the base class do its thing 374 gmPhraseWheel.cPhraseWheel._on_lose_focus(self, event)
375 #--------------------------------------------------------
377 data = self._picklist.GetSelectedItemData() 378 if data is not None: 379 return data.format_accurately() 380 return self._picklist.get_selected_item_label()
381 #-------------------------------------------------------- 382 # external API 383 #--------------------------------------------------------
384 - def SetText(self, value=u'', data=None, suppress_smarts=False):
385 386 if data is not None: 387 if isinstance(data, pyDT.datetime): 388 data = gmDateTime.cFuzzyTimestamp(timestamp=data) 389 if value.strip() == u'': 390 value = data.format_accurately() 391 392 gmPhraseWheel.cPhraseWheel.SetText(self, value = value, data = data, suppress_smarts = suppress_smarts)
393 #--------------------------------------------------------
394 - def SetData(self, data=None):
395 if data is None: 396 gmPhraseWheel.cPhraseWheel.SetText(self, u'', None) 397 else: 398 if isinstance(data, pyDT.datetime): 399 data = gmDateTime.cFuzzyTimestamp(timestamp=data) 400 gmPhraseWheel.cPhraseWheel.SetText(self, value = data.format_accurately(), data = data)
401 #--------------------------------------------------------
402 - def is_valid_timestamp(self):
403 if self.data is not None: 404 return True 405 406 # skip empty value 407 if self.GetValue().strip() == u'': 408 return True 409 410 self.data = self.__text2timestamp() 411 if self.data is None: 412 return False 413 414 return True
415 #==================================================
416 -class cTimeInput(wx.TextCtrl):
417 - def __init__(self, parent, *args, **kwargs):
418 if len(args) < 2: 419 if not kwargs.has_key('value'): 420 kwargs['value'] = _('enter time here') 421 wx.TextCtrl.__init__( 422 self, 423 parent, 424 *args, 425 **kwargs 426 )
427 #================================================== 428 #class cDateInputCtrl(wx.DatePickerCtrl): 429 # 430 # #---------------------------------------------- 431 # def SetValue(self, value): 432 # """Set either datetime.datetime or wx.DateTime""" 433 # 434 # if isinstance(value, (pyDT.date, pyDT.datetime)): 435 # value = gmDateTime.py_dt2wxDate(py_dt = value, wx = wx) 436 # 437 # elif value is None: 438 # value = wx.DefaultDateTime 439 # 440 # wx.DatePickerCtrl.SetValue(self, value) 441 # #---------------------------------------------- 442 # def GetValue(self, as_pydt=False, invalid_as_none=False): 443 # """Returns datetime.datetime values""" 444 # 445 # # datepicker can fail to pick up user changes by keyboard until 446 # # it has lost focus, so do that but also set the focus back to us, 447 # # now, this is a side-effect (after .GetValue focus will be 448 # # here) but at least it is predictable ... 449 # self.Navigate() 450 # self.SetFocus() 451 # value = wx.DatePickerCtrl.GetValue(self) 452 # 453 # if value is None: 454 # return None 455 # 456 # # manage null dates (useful when wx.DP_ALLOWNONE is set) 457 # if not value.IsValid(): 458 # if invalid_as_none: 459 # return None 460 # else: 461 # return value 462 # 463 # self.SetBackgroundColour(gmPhraseWheel.color_prw_valid) 464 # self.Refresh() 465 # 466 # if not as_pydt: 467 # return value 468 # 469 # return gmDateTime.wxDate2py_dt(value) 470 # #---------------------------------------------- 471 # # def convenience wrapper 472 # #---------------------------------------------- 473 # def is_valid_timestamp(self, allow_none=True, invalid_as_none=False): 474 # val = self.GetValue(as_pydt = False, invalid_as_none = invalid_as_none) 475 # 476 # if val is None: 477 # if allow_none: 478 # valid = True 479 # else: 480 # valid = False 481 # else: 482 # valid = val.IsValid() 483 # 484 # if valid: 485 # self.SetBackgroundColour(gmPhraseWheel.color_prw_valid) 486 # else: 487 # self.SetBackgroundColour(gmPhraseWheel.color_prw_invalid) 488 # 489 # self.Refresh() 490 # return valid 491 # #---------------------------------------------- 492 # def get_pydt(self): 493 # return self.GetValue(as_pydt = True) 494 # #---------------------------------------------- 495 # def display_as_valid(self, valid=True): 496 # if valid is True: 497 # self.SetBackgroundColour(gmPhraseWheel.color_prw_valid) 498 # else: 499 # self.SetBackgroundColour(gmPhraseWheel.color_prw_invalid) 500 # self.Refresh() 501 #================================================== 502 # main 503 #-------------------------------------------------- 504 if __name__ == '__main__': 505 506 if len(sys.argv) < 2: 507 sys.exit() 508 509 if sys.argv[2] != 'test': 510 sys.exit() 511 512 gmI18N.activate_locale() 513 gmI18N.install_domain(domain='gnumed') 514 gmDateTime.init() 515 516 #----------------------------------------------------
517 - def test_cli():
518 mp = cMatchProvider_FuzzyTimestamp() 519 mp.word_separators = None 520 mp.setThresholds(aWord = 998, aSubstring = 999) 521 val = None 522 while val != 'exit': 523 print "************************************" 524 val = raw_input('Enter date fragment: ') 525 found, matches = mp.getMatches(aFragment=val) 526 for match in matches: 527 print match['label'] 528 print match['data'] 529 print "---------------"
530 #--------------------------------------------------------
531 - def test_gui():
532 app = wx.PyWidgetTester(size = (200, 300)) 533 app.SetWidget(cFuzzyTimestampInput, id=-1, size=(180,20), pos=(10,20)) 534 app.MainLoop()
535 # #-------------------------------------------------------- 536 # def test_picker(): 537 # app = wx.PyWidgetTester(size = (200, 300)) 538 # app.SetWidget(cDateInputCtrl, id=-1, size=(180,20), pos=(10,20)) 539 # app.MainLoop() 540 #-------------------------------------------------------- 541 #test_cli() 542 #test_gui() 543 test_picker() 544 545 #================================================== 546