| Home | Trees | Indices | Help |
|
|---|
|
|
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 #============================================================
78
80
81 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
82 self.phrase_separators = None
83 self.display_accuracy = None
84 #--------------------------------------------------------
85 # phrasewheel internal API
86 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
161 if len(self._data) == 0:
162 self._set_data_to_first_match()
163
164 return super(cIntervalPhraseWheel, self).GetData()
165
166 #============================================================
168 """Shows a calendar control from which the user can pick a date."""
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 #-----------------------------------------------------------
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 #============================================================
218 """Turns strings into candidate dates.
219
220 Matching on "all" (*, '') will pop up a calendar :-)
221 """
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 #--------------------------------------------------------
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 #--------------------------------------------------------
279 """Return matches for aFragment at start of words inside phrases."""
280 return self.getMatchesByPhrase(aFragment)
281 #--------------------------------------------------------
283 """Return matches for aFragment as a true substring."""
284 return self.getMatchesByPhrase(aFragment)
285 #--------------------------------------------------------
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 #============================================================
315
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
473 if data is None:
474 gmPhraseWheel.cPhraseWheel.SetText(self, '', None)
475 return
476 self.SetText(data = data)
477
478 #--------------------------------------------------------
480 if len(self._data) == 0:
481 self._set_data_to_first_match()
482
483 return super(self.__class__, self).GetData()
484
485 #--------------------------------------------------------
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 #--------------------------------------------------------
519 return self.GetData()
520
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 #============================================================
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 #--------------------------------------------------------
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 #--------------------------------------------------------
575 """Return matches for aFragment at start of words inside phrases."""
576 return self.getMatchesByPhrase(aFragment)
577 #--------------------------------------------------------
579 """Return matches for aFragment as a true substring."""
580 return self.getMatchesByPhrase(aFragment)
581 #--------------------------------------------------------
585
586 #==================================================
588
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #--------------------------------------------------------
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 #----------------------------------------------------
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 #--------------------------------------------------------
773 app = wx.PyWidgetTester(size = (300, 40))
774 app.SetWidget(cFuzzyTimestampInput, id=-1, size=(180,20), pos=(10,20))
775 app.MainLoop()
776 #--------------------------------------------------------
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
| Home | Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Fri Jan 25 02:55:27 2019 | http://epydoc.sourceforge.net |