| 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.DestroyLater()
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.DestroyLater()
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 if isinstance(date, pyDT.datetime):
423 date = pyDT.date(date.year, date.month, date.day)
424 today = pyDT.date.today()
425 if date > today:
426 intv = date - today
427 return _('%s\n (a %s in %s)') % (
428 date.strftime(format = '%B %d %Y -- %x'),
429 date.strftime(format = '%A'),
430 gmDateTime.format_interval(interval = intv, accuracy_wanted = gmDateTime.acc_days, verbose = True)
431 )
432
433 if date < today:
434 intv = today - date
435 return _('%s\n (a %s %s ago)') % (
436 gmDateTime.pydt_strftime(date, format = '%B %d %Y -- %x'),
437 gmDateTime.pydt_strftime(date, format = '%A'),
438 gmDateTime.format_interval(interval = intv, accuracy_wanted = gmDateTime.acc_days, verbose = True)
439 )
440
441 return _('today (%s): %s') % (
442 gmDateTime.pydt_strftime(date, format = '%A'),
443 gmDateTime.pydt_strftime(date, format = '%B %d %Y -- %x')
444 )
445
446 #--------------------------------------------------------
447 # external API
448 #--------------------------------------------------------
450
451 if isinstance(value, pyDT.datetime):
452 date = value.replace (
453 hour = 11,
454 minute = 11,
455 second = 11,
456 microsecond = 111111
457 )
458 self.SetText(data = date, suppress_smarts = True)
459 return
460
461 if value is None:
462 value = ''
463
464 super().SetValue(value)
465
466 #--------------------------------------------------------
468
469 if data is not None:
470 if isinstance(data, gmDateTime.cFuzzyTimestamp):
471 data = data.timestamp.replace (
472 hour = 11,
473 minute = 11,
474 second = 11,
475 microsecond = 111111
476 )
477 if value.strip() == '':
478 value = gmDateTime.pydt_strftime(data, format = '%Y-%m-%d', accuracy = gmDateTime.acc_days)
479
480 super().SetText(value = value, data = data, suppress_smarts = suppress_smarts)
481
482 #--------------------------------------------------------
484 if data is None:
485 gmPhraseWheel.cPhraseWheel.SetText(self, '', None)
486 return
487 self.SetText(data = data)
488
489 #--------------------------------------------------------
491 if len(self._data) == 0:
492 self._set_data_to_first_match()
493
494 return super(self.__class__, self).GetData()
495
496 #--------------------------------------------------------
498 if len(self._data) > 0:
499 self.display_as_valid(True)
500 return True
501
502 if self.GetValue().strip() == '':
503 if empty_is_valid:
504 self.display_as_valid(True)
505 return True
506 else:
507 self.display_as_valid(False)
508 return False
509
510 # skip showing calendar on '*' from here
511 if self.GetValue().strip() == '*':
512 self.display_as_valid(False)
513 return False
514
515 # try to auto-snap to first match
516 self._set_data_to_first_match()
517 if len(self._data) == 0:
518 self.display_as_valid(False)
519 return False
520
521 date = self.GetData()
522 self.SetValue(gmDateTime.pydt_strftime(date, format = '%Y-%m-%d', accuracy = gmDateTime.acc_days))#, none_str = u'')
523 self.display_as_valid(True)
524 return True
525
526 #--------------------------------------------------------
527 # properties
528 #--------------------------------------------------------
530 return self.GetData()
531
534 # val = gmDateTime.pydt_strftime(date, format = '%Y-%m-%d', accuracy = gmDateTime.acc_days)
535 # self.data = date.replace (
536 # hour = 11,
537 # minute = 11,
538 # second = 11,
539 # microsecond = 111111
540 # )
541
542 date = property(_get_date, _set_date)
543
544 #============================================================
547 self.__allow_past = 1
548 self.__shifting_base = None
549
550 gmMatchProvider.cMatchProvider.__init__(self)
551
552 self.setThresholds(aPhrase = 1, aWord = 998, aSubstring = 999)
553 self.word_separators = None
554 # self.ignored_chars("""[?!."'\\(){}\[\]<>~#*$%^_]+""")
555 #--------------------------------------------------------
556 # external API
557 #--------------------------------------------------------
558 #--------------------------------------------------------
559 # base class API
560 #--------------------------------------------------------
561 # internal matching algorithms
562 #
563 # if we end up here:
564 # - aFragment will not be "None"
565 # - aFragment will be lower case
566 # - we _do_ deliver matches (whether we find any is a different story)
567 #--------------------------------------------------------
569 """Return matches for aFragment at start of phrases."""
570 matches = gmDateTime.str2fuzzy_timestamp_matches(aFragment.strip())
571
572 if len(matches) == 0:
573 return (False, [])
574
575 items = []
576 for match in matches:
577 items.append ({
578 'data': match['data'],
579 'field_label': match['label'],
580 'list_label': match['label']
581 })
582
583 return (True, items)
584 #--------------------------------------------------------
586 """Return matches for aFragment at start of words inside phrases."""
587 return self.getMatchesByPhrase(aFragment)
588 #--------------------------------------------------------
590 """Return matches for aFragment as a true substring."""
591 return self.getMatchesByPhrase(aFragment)
592 #--------------------------------------------------------
596
597 #==================================================
599
601
602 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
603
604 self.matcher = cMatchProvider_FuzzyTimestamp()
605 self.phrase_separators = None
606 self.selection_only = True
607 self.selection_only_error_msg = _('Cannot interpret input as timestamp.')
608 self.display_accuracy = None
609
610 self.__weekday_keys = [wx.WXK_F1, wx.WXK_F2, wx.WXK_F3, wx.WXK_F4, wx.WXK_F5, wx.WXK_F6, wx.WXK_F7]
611
612 #--------------------------------------------------------
613 # internal helpers
614 #--------------------------------------------------------
616 if val is None:
617 val = self.GetValue()
618 val = val.strip()
619 if val == '':
620 return None
621 success, matches = self.matcher.getMatchesByPhrase(val)
622 if len(matches) == 1:
623 return matches[0]['data']
624 return None
625
626 #--------------------------------------------------------
628 self.is_valid_timestamp(empty_is_valid = True)
629 dt = self.GetData()
630 if dt is not None:
631 dt = dt.timestamp
632 target_date = gmDateTime.get_date_of_weekday_in_week_of_date(weekday, base_dt = dt)
633 self.SetText(data = target_date, suppress_smarts = True)
634
635 #--------------------------------------------------------
636 # phrasewheel internal API
637 #--------------------------------------------------------
639 # are we valid ?
640 if self.data is None:
641 # no, so try
642 date = self.__text2timestamp()
643 if date is not None:
644 self.SetValue(value = date.format_accurately(accuracy = self.display_accuracy))
645 self.data = date
646
647 # let the base class do its thing
648 gmPhraseWheel.cPhraseWheel._on_lose_focus(self, event)
649
650 #--------------------------------------------------------
652 data = item['data']
653 if data is not None:
654 return data.format_accurately(accuracy = self.display_accuracy)
655 return item['field_label']
656
657 #--------------------------------------------------------
659
660 if event.GetUnicodeKey() == wx.WXK_NONE:
661 key = event.GetKeyCode()
662 if key in self.__weekday_keys:
663 self.__pick_from_weekday(self.__weekday_keys.index(key))
664 return
665
666 # # <ALT-C> / <ALT-K> -> calendar
667 # if event.AltDown() is True:
668 # char = chr(event.GetUnicodeKey())
669 # if char in 'ckCK':
670 # self.__pick_from_calendar()
671 # return
672
673 super()._on_key_down(event)
674
675 #--------------------------------------------------------
677 if len(self._data) == 0:
678 return ''
679
680 date = self.GetData()
681 # if match provider only provided completions
682 # but not a full date with it
683 if date is None:
684 return ''
685 ts = date.timestamp
686 now = gmDateTime.pydt_now_here()
687 if ts > now:
688 intv = ts - now
689 template = _('%s\n %s\n in %s')
690 else:
691 intv = now - ts
692 template = _('%s\n%s\n%s ago')
693 txt = template % (
694 date.format_accurately(self.display_accuracy),
695 gmDateTime.pydt_strftime (
696 ts,
697 format = '%A, %B-%d %Y (%c)',
698 ),
699 gmDateTime.format_interval (
700 interval = intv,
701 accuracy_wanted = gmDateTime.acc_days,
702 verbose = True
703 )
704 )
705 return txt
706
707 #--------------------------------------------------------
708 # external API
709 #--------------------------------------------------------
711
712 if data is not None:
713 if isinstance(data, pyDT.datetime):
714 data = gmDateTime.cFuzzyTimestamp(timestamp = data)
715 if value.strip() == '':
716 value = data.format_accurately(accuracy = self.display_accuracy)
717
718 super().SetText(value = value, data = data, suppress_smarts = suppress_smarts)
719
720 #--------------------------------------------------------
722 if data is None:
723 gmPhraseWheel.cPhraseWheel.SetText(self, '', None)
724 else:
725 if isinstance(data, pyDT.datetime):
726 data = gmDateTime.cFuzzyTimestamp(timestamp=data)
727 gmPhraseWheel.cPhraseWheel.SetText(self, value = data.format_accurately(accuracy = self.display_accuracy), data = data)
728
729 #--------------------------------------------------------
731 if self.GetData() is not None:
732 return True
733
734 # skip empty value
735 if self.GetValue().strip() == '':
736 if empty_is_valid:
737 return True
738 return False
739
740 date = self.__text2timestamp()
741 if date is None:
742 return False
743
744 self.SetText (
745 value = date.format_accurately(accuracy = self.display_accuracy),
746 data = date,
747 suppress_smarts = True
748 )
749
750 return True
751
752 #==================================================
753 # main
754 #--------------------------------------------------
755 if __name__ == '__main__':
756
757 if len(sys.argv) < 2:
758 sys.exit()
759
760 if sys.argv[1] != 'test':
761 sys.exit()
762
763 gmI18N.activate_locale()
764 gmI18N.install_domain(domain='gnumed')
765 gmDateTime.init()
766
767 #----------------------------------------------------
769 mp = cMatchProvider_FuzzyTimestamp()
770 mp.word_separators = None
771 mp.setThresholds(aWord = 998, aSubstring = 999)
772 val = None
773 while val != 'exit':
774 print("************************************")
775 val = input('Enter date fragment ("exit" to quit): ')
776 found, matches = mp.getMatches(aFragment=val)
777 for match in matches:
778 #print match
779 print(match['label'])
780 print(match['data'])
781 print("---------------")
782 #--------------------------------------------------------
784 app = wx.PyWidgetTester(size = (300, 40))
785 app.SetWidget(cFuzzyTimestampInput, id=-1, size=(180,20), pos=(10,20))
786 app.MainLoop()
787 #--------------------------------------------------------
789 app = wx.PyWidgetTester(size = (300, 40))
790 app.SetWidget(cDateInputPhraseWheel, id=-1, size=(180,20), pos=(10,20))
791 app.MainLoop()
792 #--------------------------------------------------------
793 #test_cli()
794 #test_fuzzy_picker()
795 test_picker()
796
797 #==================================================
798
| Home | Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Thu Jul 23 01:55:31 2020 | http://epydoc.sourceforge.net |