1 """GNUmed phrasewheel.
2
3 A class, extending wx.TextCtrl, which has a drop-down pick list,
4 automatically filled based on the inital letters typed. Based on the
5 interface of Richard Terry's Visual Basic client
6
7 This is based on seminal work by Ian Haywood <ihaywood@gnu.org>
8 """
9
10 __author__ = "K.Hilbert <Karsten.Hilbert@gmx.net>, I.Haywood, S.J.Tan <sjtan@bigpond.com>"
11 __license__ = "GPL"
12
13
14 import string, types, time, sys, re as regex, os.path
15
16
17
18 import wx
19 import wx.lib.mixins.listctrl as listmixins
20
21
22
23 if __name__ == '__main__':
24 sys.path.insert(0, '../../')
25 from Gnumed.pycommon import gmTools
26 from Gnumed.pycommon import gmDispatcher
27
28
29 import logging
30 _log = logging.getLogger('macosx')
31
32
33 color_prw_invalid = 'pink'
34 color_prw_partially_invalid = 'yellow'
35 color_prw_valid = None
36
37
38 default_phrase_separators = r';+'
39 default_spelling_word_separators = r'[\W\d_]+'
40
41
42 NUMERIC = '0-9'
43 ALPHANUMERIC = 'a-zA-Z0-9'
44 EMAIL_CHARS = "a-zA-Z0-9\-_@\."
45 WEB_CHARS = "a-zA-Z0-9\.\-_/:"
46
47
48 _timers = []
49
51 """It can be useful to call this early from your shutdown code to avoid hangs on Notify()."""
52 global _timers
53 _log.info('shutting down %s pending timers', len(_timers))
54 for timer in _timers:
55 _log.debug('timer [%s]', timer)
56 timer.Stop()
57 _timers = []
58
60
62 wx.Timer.__init__(self, *args, **kwargs)
63 self.callback = lambda x:x
64 global _timers
65 _timers.append(self)
66
69
70
72
74 try:
75 kwargs['style'] = kwargs['style'] | wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.SIMPLE_BORDER
76 except: pass
77 wx.ListCtrl.__init__(self, *args, **kwargs)
78 listmixins.ListCtrlAutoWidthMixin.__init__(self)
79
81 self.DeleteAllItems()
82 self.__data = items
83 pos = len(items) + 1
84 for item in items:
85 row_num = self.InsertStringItem(pos, label=item['list_label'])
86
88 sel_idx = self.GetFirstSelected()
89 if sel_idx == -1:
90 return None
91 return self.__data[sel_idx]['data']
92
94 sel_idx = self.GetFirstSelected()
95 if sel_idx == -1:
96 return None
97 return self.__data[sel_idx]
98
100 sel_idx = self.GetFirstSelected()
101 if sel_idx == -1:
102 return None
103 return self.__data[sel_idx]['list_label']
104
105
106
108 """Widget for smart guessing of user fields, after Richard Terry's interface.
109
110 - VB implementation by Richard Terry
111 - Python port by Ian Haywood for GNUmed
112 - enhanced by Karsten Hilbert for GNUmed
113 - enhanced by Ian Haywood for aumed
114 - enhanced by Karsten Hilbert for GNUmed
115
116 @param matcher: a class used to find matches for the current input
117 @type matcher: a L{match provider<Gnumed.pycommon.gmMatchProvider.cMatchProvider>}
118 instance or C{None}
119
120 @param selection_only: whether free-text can be entered without associated data
121 @type selection_only: boolean
122
123 @param capitalisation_mode: how to auto-capitalize input, valid values
124 are found in L{capitalize()<Gnumed.pycommon.gmTools.capitalize>}
125 @type capitalisation_mode: integer
126
127 @param accepted_chars: a regex pattern defining the characters
128 acceptable in the input string, if None no checking is performed
129 @type accepted_chars: None or a string holding a valid regex pattern
130
131 @param final_regex: when the control loses focus the input is
132 checked against this regular expression
133 @type final_regex: a string holding a valid regex pattern
134
135 @param navigate_after_selection: whether or not to immediately
136 navigate to the widget next-in-tab-order after selecting an
137 item from the dropdown picklist
138 @type navigate_after_selection: boolean
139
140 @param speller: if not None used to spellcheck the current input
141 and to retrieve suggested replacements/completions
142 @type speller: None or a L{enchant Dict<enchant>} descendant
143
144 @param picklist_delay: this much time of user inactivity must have
145 passed before the input related smarts kick in and the drop
146 down pick list is shown
147 @type picklist_delay: integer (milliseconds)
148 """
149 - def __init__ (self, parent=None, id=-1, *args, **kwargs):
150
151
152 self.matcher = None
153 self.selection_only = False
154 self.selection_only_error_msg = _('You must select a value from the picklist or type an exact match.')
155 self.capitalisation_mode = gmTools.CAPS_NONE
156 self.accepted_chars = None
157 self.final_regex = '.*'
158 self.final_regex_error_msg = _('The content is invalid. It must match the regular expression: [%%s]. <%s>') % self.__class__.__name__
159 self.navigate_after_selection = False
160 self.speller = None
161 self.speller_word_separators = default_spelling_word_separators
162 self.picklist_delay = 150
163
164
165 self._has_focus = False
166 self._current_match_candidates = []
167 self._screenheight = wx.SystemSettings.GetMetric(wx.SYS_SCREEN_Y)
168 self.suppress_text_update_smarts = False
169
170 self.__static_tt = None
171 self.__static_tt_extra = None
172
173
174 self._data = {}
175
176 self._on_selection_callbacks = []
177 self._on_lose_focus_callbacks = []
178 self._on_set_focus_callbacks = []
179 self._on_modified_callbacks = []
180
181 try:
182 kwargs['style'] = kwargs['style'] | wx.TE_PROCESS_TAB | wx.TE_PROCESS_ENTER
183 except KeyError:
184 kwargs['style'] = wx.TE_PROCESS_TAB | wx.TE_PROCESS_ENTER
185 super(cPhraseWheelBase, self).__init__(parent, id, **kwargs)
186
187 self.__my_startup_color = self.GetBackgroundColour()
188 self.__non_edit_font = self.GetFont()
189 global color_prw_valid
190 if color_prw_valid is None:
191 color_prw_valid = wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW)
192
193 self.__init_dropdown(parent = parent)
194 self.__register_events()
195 self.__init_timer()
196
197
198
199 - def GetData(self, can_create=False):
200 """Retrieve the data associated with the displayed string(s).
201
202 - self._create_data() must set self.data if possible (/successful)
203 """
204 if len(self._data) == 0:
205 if can_create:
206 self._create_data()
207
208 return self._data
209
210 - def SetText(self, value=u'', data=None, suppress_smarts=False):
211
212 if value is None:
213 value = u''
214
215 if (value == u'') and (data is None):
216 self._data = {}
217 super(cPhraseWheelBase, self).SetValue(value)
218 return
219
220 self.suppress_text_update_smarts = suppress_smarts
221
222 if data is not None:
223 self.suppress_text_update_smarts = True
224 self.data = self._dictify_data(data = data, value = value)
225 super(cPhraseWheelBase, self).SetValue(value)
226 self.display_as_valid(valid = True)
227
228
229 if len(self._data) > 0:
230 return True
231
232
233 if value == u'':
234
235 if not self.selection_only:
236 return True
237
238 if not self._set_data_to_first_match():
239
240 if self.selection_only:
241 self.display_as_valid(valid = False)
242 return False
243
244 return True
245
247 raise NotImplementedError('[%s]: set_from_instance()' % self.__class__.__name__)
248
250 raise NotImplementedError('[%s]: set_from_pk()' % self.__class__.__name__)
251
253 if valid is True:
254 self.SetBackgroundColour(self.__my_startup_color)
255 elif valid is False:
256 if partially_invalid:
257 self.SetBackgroundColour(color_prw_partially_invalid)
258 else:
259 self.SetBackgroundColour(color_prw_invalid)
260 else:
261 raise ValueError(u'<valid> must be True or False')
262 self.Refresh()
263
265 if disabled is True:
266 self.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_BACKGROUND))
267 elif disabled is False:
268 self.SetBackgroundColour(color_prw_valid)
269 else:
270 raise ValueError(u'<disabled> must be True or False')
271 self.Refresh()
272
273
274
276 """Add a callback for invocation when a picklist item is selected.
277
278 The callback will be invoked whenever an item is selected
279 from the picklist. The associated data is passed in as
280 a single parameter. Callbacks must be able to cope with
281 None as the data parameter as that is sent whenever the
282 user changes a previously selected value.
283 """
284 if not callable(callback):
285 raise ValueError('[add_callback_on_selection]: ignoring callback [%s], it is not callable' % callback)
286
287 self._on_selection_callbacks.append(callback)
288
290 """Add a callback for invocation when getting focus."""
291 if not callable(callback):
292 raise ValueError('[add_callback_on_set_focus]: ignoring callback [%s] - not callable' % callback)
293
294 self._on_set_focus_callbacks.append(callback)
295
297 """Add a callback for invocation when losing focus."""
298 if not callable(callback):
299 raise ValueError('[add_callback_on_lose_focus]: ignoring callback [%s] - not callable' % callback)
300
301 self._on_lose_focus_callbacks.append(callback)
302
304 """Add a callback for invocation when the content is modified.
305
306 This callback will NOT be passed any values.
307 """
308 if not callable(callback):
309 raise ValueError('[add_callback_on_modified]: ignoring callback [%s] - not callable' % callback)
310
311 self._on_modified_callbacks.append(callback)
312
313
314
315 - def set_context(self, context=None, val=None):
316 if self.matcher is not None:
317 self.matcher.set_context(context=context, val=val)
318
319 - def unset_context(self, context=None):
320 if self.matcher is not None:
321 self.matcher.unset_context(context=context)
322
323
324
326
327 try:
328 import enchant
329 except ImportError:
330 self.speller = None
331 return False
332
333 try:
334 self.speller = enchant.DictWithPWL(None, os.path.expanduser(os.path.join('~', '.gnumed', 'spellcheck', 'wordlist.pwl')))
335 except enchant.DictNotFoundError:
336 self.speller = None
337 return False
338
339 return True
340
342 if self.speller is None:
343 return None
344
345
346 last_word = self.__speller_word_separators.split(val)[-1]
347 if last_word.strip() == u'':
348 return None
349
350 try:
351 suggestions = self.speller.suggest(last_word)
352 except:
353 _log.exception('had to disable (enchant) spell checker')
354 self.speller = None
355 return None
356
357 if len(suggestions) == 0:
358 return None
359
360 input2match_without_last_word = val[:val.rindex(last_word)]
361 return [ input2match_without_last_word + suggestion for suggestion in suggestions ]
362
368
370 return self.__speller_word_separators.pattern
371
372 speller_word_separators = property(_get_speller_word_separators, _set_speller_word_separators)
373
374
375
376
377
379 szr_dropdown = None
380 try:
381
382 self.__dropdown_needs_relative_position = False
383 self._picklist_dropdown = wx.PopupWindow(parent)
384 list_parent = self._picklist_dropdown
385 self.__use_fake_popup = False
386 except NotImplementedError:
387 self.__use_fake_popup = True
388
389
390 add_picklist_to_sizer = True
391 szr_dropdown = wx.BoxSizer(wx.VERTICAL)
392
393
394 self.__dropdown_needs_relative_position = False
395 self._picklist_dropdown = wx.MiniFrame (
396 parent = parent,
397 id = -1,
398 style = wx.SIMPLE_BORDER | wx.FRAME_FLOAT_ON_PARENT | wx.FRAME_NO_TASKBAR | wx.POPUP_WINDOW
399 )
400 scroll_win = wx.ScrolledWindow(parent = self._picklist_dropdown, style = wx.NO_BORDER)
401 scroll_win.SetSizer(szr_dropdown)
402 list_parent = scroll_win
403
404
405
406
407
408
409
410 self.__mac_log('dropdown parent: %s' % self._picklist_dropdown.GetParent())
411
412 self._picklist = cPhraseWheelListCtrl (
413 list_parent,
414 style = wx.LC_NO_HEADER
415 )
416 self._picklist.InsertColumn(0, u'')
417
418 if szr_dropdown is not None:
419 szr_dropdown.Add(self._picklist, 1, wx.EXPAND)
420
421 self._picklist_dropdown.Hide()
422
424 """Display the pick list if useful."""
425
426 self._picklist_dropdown.Hide()
427
428 if not self._has_focus:
429 return
430
431 if len(self._current_match_candidates) == 0:
432 return
433
434
435
436 if len(self._current_match_candidates) == 1:
437 candidate = self._current_match_candidates[0]
438 if candidate['field_label'] == input2match:
439 self._update_data_from_picked_item(candidate)
440 return
441
442
443 dropdown_size = self._picklist_dropdown.GetSize()
444 border_width = 4
445 extra_height = 25
446
447 rows = len(self._current_match_candidates)
448 if rows < 2:
449 rows = 2
450 if rows > 20:
451 rows = 20
452 self.__mac_log('dropdown needs rows: %s' % rows)
453 pw_size = self.GetSize()
454 dropdown_size.SetHeight (
455 (pw_size.height * rows)
456 + border_width
457 + extra_height
458 )
459
460 dropdown_size.SetWidth(min (
461 self.Size.width * 2,
462 self.Parent.Size.width
463 ))
464
465
466 (pw_x_abs, pw_y_abs) = self.ClientToScreenXY(0,0)
467 self.__mac_log('phrasewheel position (on screen): x:%s-%s, y:%s-%s' % (pw_x_abs, (pw_x_abs+pw_size.width), pw_y_abs, (pw_y_abs+pw_size.height)))
468 dropdown_new_x = pw_x_abs
469 dropdown_new_y = pw_y_abs + pw_size.height
470 self.__mac_log('desired dropdown position (on screen): x:%s-%s, y:%s-%s' % (dropdown_new_x, (dropdown_new_x+dropdown_size.width), dropdown_new_y, (dropdown_new_y+dropdown_size.height)))
471 self.__mac_log('desired dropdown size: %s' % dropdown_size)
472
473
474 if (dropdown_new_y + dropdown_size.height) > self._screenheight:
475 self.__mac_log('dropdown extends offscreen (screen max y: %s)' % self._screenheight)
476 max_height = self._screenheight - dropdown_new_y - 4
477 self.__mac_log('max dropdown height would be: %s' % max_height)
478 if max_height > ((pw_size.height * 2) + 4):
479 dropdown_size.SetHeight(max_height)
480 self.__mac_log('possible dropdown position (on screen): x:%s-%s, y:%s-%s' % (dropdown_new_x, (dropdown_new_x+dropdown_size.width), dropdown_new_y, (dropdown_new_y+dropdown_size.height)))
481 self.__mac_log('possible dropdown size: %s' % dropdown_size)
482
483
484 self._picklist_dropdown.SetSize(dropdown_size)
485 self._picklist.SetSize(self._picklist_dropdown.GetClientSize())
486 self.__mac_log('pick list size set to: %s' % self._picklist_dropdown.GetSize())
487 if self.__dropdown_needs_relative_position:
488 dropdown_new_x, dropdown_new_y = self._picklist_dropdown.GetParent().ScreenToClientXY(dropdown_new_x, dropdown_new_y)
489 self._picklist_dropdown.MoveXY(dropdown_new_x, dropdown_new_y)
490
491
492 self._picklist.Select(0)
493
494
495 self._picklist_dropdown.Show(True)
496
497
498
499
500
501
502
503
504
505
506
508 """Hide the pick list."""
509 self._picklist_dropdown.Hide()
510
512 """Mark the given picklist row as selected."""
513 if old_row_idx is not None:
514 pass
515 self._picklist.Select(new_row_idx)
516 self._picklist.EnsureVisible(new_row_idx)
517
519 """Get string to display in the field for the given picklist item."""
520 if item is None:
521 item = self._picklist.get_selected_item()
522 try:
523 return item['field_label']
524 except KeyError:
525 pass
526 try:
527 return item['list_label']
528 except KeyError:
529 pass
530 try:
531 return item['label']
532 except KeyError:
533 return u'<no field_*/list_*/label in item>'
534
535
537 """Update the display to show item strings."""
538
539 display_string = self._picklist_item2display_string(item = item)
540 self.suppress_text_update_smarts = True
541 super(cPhraseWheelBase, self).SetValue(display_string)
542
543 self.SetInsertionPoint(self.GetLastPosition())
544 return
545
546
547
549 raise NotImplementedError('[%s]: fragment extraction not implemented' % self.__class__.__name__)
550
552 """Get candidates matching the currently typed input."""
553
554
555 self._current_match_candidates = []
556 if self.matcher is not None:
557 matched, self._current_match_candidates = self.matcher.getMatches(val)
558 self._picklist.SetItems(self._current_match_candidates)
559
560
561
562
563
564 if len(self._current_match_candidates) == 0:
565 suggestions = self._get_suggestions_from_spell_checker(val)
566 if suggestions is not None:
567 self._current_match_candidates = [
568 {'list_label': suggestion, 'field_label': suggestion, 'data': None}
569 for suggestion in suggestions
570 ]
571 self._picklist.SetItems(self._current_match_candidates)
572
573
574
580
626
628 return self.__static_tt_extra
629
631 self.__static_tt_extra = tt
632
633 static_tooltip_extra = property(_get_static_tt_extra, _set_static_tt_extra)
634
635
636
638 wx.EVT_KEY_DOWN (self, self._on_key_down)
639 wx.EVT_SET_FOCUS(self, self._on_set_focus)
640 wx.EVT_KILL_FOCUS(self, self._on_lose_focus)
641 wx.EVT_TEXT(self, self.GetId(), self._on_text_update)
642 self._picklist.Bind(wx.EVT_LEFT_DCLICK, self._on_list_item_selected)
643
645 """Is called when a key is pressed."""
646
647 keycode = event.GetKeyCode()
648
649 if keycode == wx.WXK_DOWN:
650 self.__on_cursor_down()
651 return
652
653 if keycode == wx.WXK_UP:
654 self.__on_cursor_up()
655 return
656
657 if keycode == wx.WXK_RETURN:
658 self._on_enter()
659 return
660
661 if keycode == wx.WXK_TAB:
662 if event.ShiftDown():
663 self.Navigate(flags = wx.NavigationKeyEvent.IsBackward)
664 return
665 self.__on_tab()
666 self.Navigate(flags = wx.NavigationKeyEvent.IsForward)
667 return
668
669
670 if keycode in [wx.WXK_SHIFT, wx.WXK_BACK, wx.WXK_DELETE, wx.WXK_LEFT, wx.WXK_RIGHT]:
671 pass
672
673
674 elif not self.__char_is_allowed(char = unichr(event.GetUnicodeKey())):
675 wx.Bell()
676
677 return
678
679 event.Skip()
680 return
681
683
684 self._has_focus = True
685 event.Skip()
686
687
688
689 edit_font = wx.FontFromNativeInfo(self.__non_edit_font.NativeFontInfo)
690 edit_font.SetPointSize(pointSize = edit_font.GetPointSize() + 1)
691 self.SetFont(edit_font)
692 self.Refresh()
693
694
695 for callback in self._on_set_focus_callbacks:
696 callback()
697
698 self.__timer.Start(oneShot = True, milliseconds = self.picklist_delay)
699 return True
700
702 """Do stuff when leaving the control.
703
704 The user has had her say, so don't second guess
705 intentions but do report error conditions.
706 """
707 event.Skip()
708 self._has_focus = False
709 self.__timer.Stop()
710 self._hide_picklist()
711 wx.CallAfter(self.__on_lost_focus)
712 return True
713
715 self.SetSelection(1,1)
716 self.SetFont(self.__non_edit_font)
717
718
719 is_valid = True
720
721
722
723
724 self._set_data_to_first_match()
725
726
727 if self.__final_regex.match(self.GetValue().strip()) is None:
728 gmDispatcher.send(signal = 'statustext', msg = self.final_regex_error_msg)
729 is_valid = False
730
731 self.display_as_valid(valid = is_valid)
732
733
734 for callback in self._on_lose_focus_callbacks:
735 callback()
736
738 """Gets called when user selected a list item."""
739
740 self._hide_picklist()
741
742 item = self._picklist.get_selected_item()
743
744 if item is None:
745 self.display_as_valid(valid = True)
746 return
747
748 self._update_display_from_picked_item(item)
749 self._update_data_from_picked_item(item)
750 self.MarkDirty()
751
752
753 for callback in self._on_selection_callbacks:
754 callback(self._data)
755
756 if self.navigate_after_selection:
757 self.Navigate()
758
759 return
760
761 - def _on_text_update (self, event):
762 """Internal handler for wx.EVT_TEXT.
763
764 Called when text was changed by user or by SetValue().
765 """
766 if self.suppress_text_update_smarts:
767 self.suppress_text_update_smarts = False
768 return
769
770 self._adjust_data_after_text_update()
771 self._current_match_candidates = []
772
773 val = self.GetValue().strip()
774 ins_point = self.GetInsertionPoint()
775
776
777
778 if val == u'':
779 self._hide_picklist()
780 self.__timer.Stop()
781 else:
782 new_val = gmTools.capitalize(text = val, mode = self.capitalisation_mode)
783 if new_val != val:
784 self.suppress_text_update_smarts = True
785 super(cPhraseWheelBase, self).SetValue(new_val)
786 if ins_point > len(new_val):
787 self.SetInsertionPointEnd()
788 else:
789 self.SetInsertionPoint(ins_point)
790
791
792
793 self.__timer.Start(oneShot = True, milliseconds = self.picklist_delay)
794
795
796 for callback in self._on_modified_callbacks:
797 callback()
798
799 return
800
801
802
804 """Called when the user pressed <ENTER>."""
805 if self._picklist_dropdown.IsShown():
806 self._on_list_item_selected()
807 else:
808
809 self.Navigate()
810
812
813 if self._picklist_dropdown.IsShown():
814 idx_selected = self._picklist.GetFirstSelected()
815 if idx_selected < (len(self._current_match_candidates) - 1):
816 self._select_picklist_row(idx_selected + 1, idx_selected)
817 return
818
819
820
821
822
823 self.__timer.Stop()
824 if self.GetValue().strip() == u'':
825 val = u'*'
826 else:
827 val = self._extract_fragment_to_match_on()
828 self._update_candidates_in_picklist(val = val)
829 self._show_picklist(input2match = val)
830
832 if self._picklist_dropdown.IsShown():
833 selected = self._picklist.GetFirstSelected()
834 if selected > 0:
835 self._select_picklist_row(selected-1, selected)
836 else:
837
838 pass
839
841 """Under certain circumstances take special action on <TAB>.
842
843 returns:
844 True: <TAB> was handled
845 False: <TAB> was not handled
846
847 -> can be used to decide whether to do further <TAB> handling outside this class
848 """
849
850 if not self._picklist_dropdown.IsShown():
851 return False
852
853
854 if len(self._current_match_candidates) != 1:
855 return False
856
857
858 if not self.selection_only:
859 return False
860
861
862 self._select_picklist_row(new_row_idx = 0)
863 self._on_list_item_selected()
864
865 return True
866
867
868
870 self.__timer = _cPRWTimer()
871 self.__timer.callback = self._on_timer_fired
872
873 self.__timer.Stop()
874
876 """Callback for delayed match retrieval timer.
877
878 if we end up here:
879 - delay has passed without user input
880 - the value in the input field has not changed since the timer started
881 """
882
883 val = self._extract_fragment_to_match_on()
884 self._update_candidates_in_picklist(val = val)
885
886
887
888
889
890
891 wx.CallAfter(self._show_picklist, input2match = val)
892
893
894
896 if self.__use_fake_popup:
897 _log.debug(msg)
898
900
901 if self.accepted_chars is None:
902 return True
903 return (self.__accepted_chars.match(char) is not None)
904
910
912 if self.__accepted_chars is None:
913 return None
914 return self.__accepted_chars.pattern
915
916 accepted_chars = property(_get_accepted_chars, _set_accepted_chars)
917
919 self.__final_regex = regex.compile(final_regex, flags = regex.LOCALE | regex.UNICODE)
920
922 return self.__final_regex.pattern
923
924 final_regex = property(_get_final_regex, _set_final_regex)
925
927 self.__final_regex_error_msg = msg % self.final_regex
928
930 return self.__final_regex_error_msg
931
932 final_regex_error_msg = property(_get_final_regex_error_msg, _set_final_regex_error_msg)
933
934
935
938
940 self.data = {item['field_label']: item}
941
943 raise NotImplementedError('[%s]: _dictify_data()' % self.__class__.__name__)
944
946 raise NotImplementedError('[%s]: cannot adjust data after text update' % self.__class__.__name__)
947
952
954 raise NotImplementedError('[%s]: cannot create data object' % self.__class__.__name__)
955
958
960 self._data = data
961 self.__recalculate_tooltip()
962
963 data = property(_get_data, _set_data)
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1034
1035 - def GetData(self, can_create=False, as_instance=False):
1036
1037 super(cPhraseWheel, self).GetData(can_create = can_create)
1038
1039 if len(self._data) > 0:
1040 if as_instance:
1041 return self._data2instance()
1042
1043 if len(self._data) == 0:
1044 return None
1045
1046 return self._data.values()[0]['data']
1047
1049 """Set the data and thereby set the value, too. if possible.
1050
1051 If you call SetData() you better be prepared
1052 doing a scan of the entire potential match space.
1053
1054 The whole thing will only work if data is found
1055 in the match space anyways.
1056 """
1057 if data is None:
1058 self._data = {}
1059 return True
1060
1061
1062 self._update_candidates_in_picklist(u'*')
1063
1064
1065 if self.selection_only:
1066
1067 if len(self._current_match_candidates) == 0:
1068 return False
1069
1070
1071 for candidate in self._current_match_candidates:
1072 if candidate['data'] == data:
1073 super(cPhraseWheel, self).SetText (
1074 value = candidate['field_label'],
1075 data = data,
1076 suppress_smarts = True
1077 )
1078 return True
1079
1080
1081 if self.selection_only:
1082 self.display_as_valid(valid = False)
1083 return False
1084
1085 self.data = self._dictify_data(data = data)
1086 self.display_as_valid(valid = True)
1087 return True
1088
1089
1090
1092
1093
1094
1095
1096 if len(self._data) > 0:
1097 self._picklist_dropdown.Hide()
1098 return
1099
1100 return super(cPhraseWheel, self)._show_picklist(input2match = input2match)
1101
1103
1104 if len(self._data) > 0:
1105 return True
1106
1107
1108 val = self.GetValue().strip()
1109 if val == u'':
1110 return True
1111
1112
1113 self._update_candidates_in_picklist(val = val)
1114 for candidate in self._current_match_candidates:
1115 if candidate['field_label'] == val:
1116 self.data = {candidate['field_label']: candidate}
1117 self.MarkDirty()
1118
1119 for callback in self._on_selection_callbacks:
1120 callback(self._data)
1121 return True
1122
1123
1124 if self.selection_only:
1125 gmDispatcher.send(signal = 'statustext', msg = self.selection_only_error_msg)
1126 is_valid = False
1127 return False
1128
1129 return True
1130
1133
1136
1142
1144
1153
1154 - def GetData(self, can_create=False, as_instance=False):
1155
1156 super(cMultiPhraseWheel, self).GetData(can_create = can_create)
1157
1158 if len(self._data) > 0:
1159 if as_instance:
1160 return self._data2instance()
1161
1162 return self._data.values()
1163
1165 self.speller = None
1166 return True
1167
1169
1170 data_dict = {}
1171
1172 for item in data_items:
1173 try:
1174 list_label = item['list_label']
1175 except KeyError:
1176 list_label = item['label']
1177 try:
1178 field_label = item['field_label']
1179 except KeyError:
1180 field_label = list_label
1181 data_dict[field_label] = {'data': item['data'], 'list_label': list_label, 'field_label': field_label}
1182
1183 return data_dict
1184
1185
1186
1189
1191
1192 new_data = {}
1193
1194
1195 for displayed_label in self.displayed_strings:
1196 try:
1197 new_data[displayed_label] = self._data[displayed_label]
1198 except KeyError:
1199
1200
1201 pass
1202
1203 self.data = new_data
1204
1206
1207 cursor_pos = self.GetInsertionPoint()
1208
1209 entire_input = self.GetValue()
1210 if self.__phrase_separators.search(entire_input) is None:
1211 self.left_part = u''
1212 self.right_part = u''
1213 return self.GetValue().strip()
1214
1215 string_left_of_cursor = entire_input[:cursor_pos]
1216 string_right_of_cursor = entire_input[cursor_pos:]
1217
1218 left_parts = [ lp.strip() for lp in self.__phrase_separators.split(string_left_of_cursor) ]
1219 if len(left_parts) == 0:
1220 self.left_part = u''
1221 else:
1222 self.left_part = u'%s%s ' % (
1223 (u'%s ' % self.__phrase_separators.pattern[0]).join(left_parts[:-1]),
1224 self.__phrase_separators.pattern[0]
1225 )
1226
1227 right_parts = [ rp.strip() for rp in self.__phrase_separators.split(string_right_of_cursor) ]
1228 self.right_part = u'%s %s' % (
1229 self.__phrase_separators.pattern[0],
1230 (u'%s ' % self.__phrase_separators.pattern[0]).join(right_parts[1:])
1231 )
1232
1233 val = (left_parts[-1] + right_parts[0]).strip()
1234 return val
1235
1237 val = (u'%s%s%s' % (
1238 self.left_part,
1239 self._picklist_item2display_string(item = item),
1240 self.right_part
1241 )).lstrip().lstrip(';').strip()
1242 self.suppress_text_update_smarts = True
1243 super(cMultiPhraseWheel, self).SetValue(val)
1244
1245 item_end = val.index(item['field_label']) + len(item['field_label'])
1246 self.SetInsertionPoint(item_end)
1247 return
1248
1250
1251
1252 self._data[item['field_label']] = item
1253
1254
1255 field_labels = [ p.strip() for p in self.__phrase_separators.split(self.GetValue().strip()) ]
1256 new_data = {}
1257
1258
1259 for field_label in field_labels:
1260 try:
1261 new_data[field_label] = self._data[field_label]
1262 except KeyError:
1263
1264
1265 pass
1266
1267 self.data = new_data
1268
1275
1276
1277
1279 """Set phrase separators.
1280
1281 - must be a valid regular expression pattern
1282
1283 input is split into phrases at boundaries defined by
1284 this regex and matching is performed on the phrase
1285 the cursor is in only,
1286
1287 after selection from picklist phrase_separators[0] is
1288 added to the end of the match in the PRW
1289 """
1290 self.__phrase_separators = regex.compile(phrase_separators, flags = regex.LOCALE | regex.UNICODE)
1291
1293 return self.__phrase_separators.pattern
1294
1295 phrase_separators = property(_get_phrase_separators, _set_phrase_separators)
1296
1298 return [ p.strip() for p in self.__phrase_separators.split(self.GetValue().strip()) if p.strip() != u'' ]
1299
1300 displayed_strings = property(_get_displayed_strings, lambda x:x)
1301
1302
1303
1304 if __name__ == '__main__':
1305
1306 if len(sys.argv) < 2:
1307 sys.exit()
1308
1309 if sys.argv[1] != u'test':
1310 sys.exit()
1311
1312 from Gnumed.pycommon import gmI18N
1313 gmI18N.activate_locale()
1314 gmI18N.install_domain(domain='gnumed')
1315
1316 from Gnumed.pycommon import gmPG2, gmMatchProvider
1317
1318 prw = None
1319
1321 print "got focus:"
1322 print "value:", prw.GetValue()
1323 print "data :", prw.GetData()
1324 return True
1325
1327 print "lost focus:"
1328 print "value:", prw.GetValue()
1329 print "data :", prw.GetData()
1330 return True
1331
1333 print "modified:"
1334 print "value:", prw.GetValue()
1335 print "data :", prw.GetData()
1336 return True
1337
1339 print "selected:"
1340 print "value:", prw.GetValue()
1341 print "data :", prw.GetData()
1342 return True
1343
1344
1346 app = wx.PyWidgetTester(size = (200, 50))
1347
1348 items = [ {'data': 1, 'list_label': "Bloggs", 'field_label': "Bloggs", 'weight': 0},
1349 {'data': 2, 'list_label': "Baker", 'field_label': "Baker", 'weight': 0},
1350 {'data': 3, 'list_label': "Jones", 'field_label': "Jones", 'weight': 0},
1351 {'data': 4, 'list_label': "Judson", 'field_label': "Judson", 'weight': 0},
1352 {'data': 5, 'list_label': "Jacobs", 'field_label': "Jacobs", 'weight': 0},
1353 {'data': 6, 'list_label': "Judson-Jacobs", 'field_label': "Judson-Jacobs", 'weight': 0}
1354 ]
1355
1356 mp = gmMatchProvider.cMatchProvider_FixedList(items)
1357
1358 mp.word_separators = '[ \t=+&:@]+'
1359 global prw
1360 prw = cPhraseWheel(parent = app.frame, id = -1)
1361 prw.matcher = mp
1362 prw.capitalisation_mode = gmTools.CAPS_NAMES
1363 prw.add_callback_on_set_focus(callback=display_values_set_focus)
1364 prw.add_callback_on_modified(callback=display_values_modified)
1365 prw.add_callback_on_lose_focus(callback=display_values_lose_focus)
1366 prw.add_callback_on_selection(callback=display_values_selected)
1367
1368 app.frame.Show(True)
1369 app.MainLoop()
1370
1371 return True
1372
1374 print "Do you want to test the database connected phrase wheel ?"
1375 yes_no = raw_input('y/n: ')
1376 if yes_no != 'y':
1377 return True
1378
1379 gmPG2.get_connection()
1380 query = u"""SELECT code, code || ': ' || _(name), _(name) FROM dem.country WHERE _(name) %(fragment_condition)s"""
1381 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query])
1382 app = wx.PyWidgetTester(size = (400, 50))
1383 global prw
1384
1385 prw = cMultiPhraseWheel(parent = app.frame, id = -1)
1386 prw.matcher = mp
1387
1388 app.frame.Show(True)
1389 app.MainLoop()
1390
1391 return True
1392
1394 gmPG2.get_connection()
1395 query = u"""
1396 select
1397 pk_identity,
1398 firstnames || ' ' || lastnames || ', ' || to_char(dob, 'YYYY-MM-DD'),
1399 firstnames || ' ' || lastnames
1400 from
1401 dem.v_basic_person
1402 where
1403 firstnames || lastnames %(fragment_condition)s
1404 """
1405 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query])
1406 app = wx.PyWidgetTester(size = (500, 50))
1407 global prw
1408 prw = cPhraseWheel(parent = app.frame, id = -1)
1409 prw.matcher = mp
1410 prw.selection_only = True
1411
1412 app.frame.Show(True)
1413 app.MainLoop()
1414
1415 return True
1416
1434
1435
1436
1437
1438 test_prw_patients()
1439
1440
1441