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