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 if valid is True:
244 self.SetBackgroundColour(self.__my_startup_color)
245 elif valid is False:
246 if partially_invalid:
247 self.SetBackgroundColour(color_prw_partially_invalid)
248 else:
249 self.SetBackgroundColour(color_prw_invalid)
250 else:
251 raise ValueError(u'<valid> must be True or False')
252 self.Refresh()
253
255 if disabled is True:
256 self.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_BACKGROUND))
257 elif disabled is False:
258 self.SetBackgroundColour(color_prw_valid)
259 else:
260 raise ValueError(u'<disabled> must be True or False')
261 self.Refresh()
262
263
264
266 """Add a callback for invocation when a picklist item is selected.
267
268 The callback will be invoked whenever an item is selected
269 from the picklist. The associated data is passed in as
270 a single parameter. Callbacks must be able to cope with
271 None as the data parameter as that is sent whenever the
272 user changes a previously selected value.
273 """
274 if not callable(callback):
275 raise ValueError('[add_callback_on_selection]: ignoring callback [%s], it is not callable' % callback)
276
277 self._on_selection_callbacks.append(callback)
278
280 """Add a callback for invocation when getting focus."""
281 if not callable(callback):
282 raise ValueError('[add_callback_on_set_focus]: ignoring callback [%s] - not callable' % callback)
283
284 self._on_set_focus_callbacks.append(callback)
285
287 """Add a callback for invocation when losing focus."""
288 if not callable(callback):
289 raise ValueError('[add_callback_on_lose_focus]: ignoring callback [%s] - not callable' % callback)
290
291 self._on_lose_focus_callbacks.append(callback)
292
294 """Add a callback for invocation when the content is modified."""
295 if not callable(callback):
296 raise ValueError('[add_callback_on_modified]: ignoring callback [%s] - not callable' % callback)
297
298 self._on_modified_callbacks.append(callback)
299
300
301
302 - def set_context(self, context=None, val=None):
303 if self.matcher is not None:
304 self.matcher.set_context(context=context, val=val)
305
306 - def unset_context(self, context=None):
307 if self.matcher is not None:
308 self.matcher.unset_context(context=context)
309
310
311
313
314 try:
315 import enchant
316 except ImportError:
317 self.speller = None
318 return False
319
320 try:
321 self.speller = enchant.DictWithPWL(None, os.path.expanduser(os.path.join('~', '.gnumed', 'spellcheck', 'wordlist.pwl')))
322 except enchant.DictNotFoundError:
323 self.speller = None
324 return False
325
326 return True
327
329 if self.speller is None:
330 return None
331
332
333 last_word = self.__speller_word_separators.split(val)[-1]
334 if last_word.strip() == u'':
335 return None
336
337 try:
338 suggestions = self.speller.suggest(last_word)
339 except:
340 _log.exception('had to disable (enchant) spell checker')
341 self.speller = None
342 return None
343
344 if len(suggestions) == 0:
345 return None
346
347 input2match_without_last_word = val[:val.rindex(last_word)]
348 return [ input2match_without_last_word + suggestion for suggestion in suggestions ]
349
355
357 return self.__speller_word_separators.pattern
358
359 speller_word_separators = property(_get_speller_word_separators, _set_speller_word_separators)
360
361
362
363
364
366 szr_dropdown = None
367 try:
368
369 self.__dropdown_needs_relative_position = False
370 self._picklist_dropdown = wx.PopupWindow(parent)
371 list_parent = self._picklist_dropdown
372 self.__use_fake_popup = False
373 except NotImplementedError:
374 self.__use_fake_popup = True
375
376
377 add_picklist_to_sizer = True
378 szr_dropdown = wx.BoxSizer(wx.VERTICAL)
379
380
381 self.__dropdown_needs_relative_position = False
382 self._picklist_dropdown = wx.MiniFrame (
383 parent = parent,
384 id = -1,
385 style = wx.SIMPLE_BORDER | wx.FRAME_FLOAT_ON_PARENT | wx.FRAME_NO_TASKBAR | wx.POPUP_WINDOW
386 )
387 scroll_win = wx.ScrolledWindow(parent = self._picklist_dropdown, style = wx.NO_BORDER)
388 scroll_win.SetSizer(szr_dropdown)
389 list_parent = scroll_win
390
391
392
393
394
395
396
397 self.__mac_log('dropdown parent: %s' % self._picklist_dropdown.GetParent())
398
399 self._picklist = cPhraseWheelListCtrl (
400 list_parent,
401 style = wx.LC_NO_HEADER
402 )
403 self._picklist.InsertColumn(0, u'')
404
405 if szr_dropdown is not None:
406 szr_dropdown.Add(self._picklist, 1, wx.EXPAND)
407
408 self._picklist_dropdown.Hide()
409
411 """Display the pick list if useful."""
412
413 self._picklist_dropdown.Hide()
414
415 if not self._has_focus:
416 return
417
418 if len(self._current_match_candidates) == 0:
419 return
420
421
422
423 if len(self._current_match_candidates) == 1:
424 candidate = self._current_match_candidates[0]
425 if candidate['field_label'] == input2match:
426 self._update_data_from_picked_item(candidate)
427 return
428
429
430 dropdown_size = self._picklist_dropdown.GetSize()
431 border_width = 4
432 extra_height = 25
433
434 rows = len(self._current_match_candidates)
435 if rows < 2:
436 rows = 2
437 if rows > 20:
438 rows = 20
439 self.__mac_log('dropdown needs rows: %s' % rows)
440 pw_size = self.GetSize()
441 dropdown_size.SetHeight (
442 (pw_size.height * rows)
443 + border_width
444 + extra_height
445 )
446
447 dropdown_size.SetWidth(min (
448 self.Size.width * 2,
449 self.Parent.Size.width
450 ))
451
452
453 (pw_x_abs, pw_y_abs) = self.ClientToScreenXY(0,0)
454 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)))
455 dropdown_new_x = pw_x_abs
456 dropdown_new_y = pw_y_abs + pw_size.height
457 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)))
458 self.__mac_log('desired dropdown size: %s' % dropdown_size)
459
460
461 if (dropdown_new_y + dropdown_size.height) > self._screenheight:
462 self.__mac_log('dropdown extends offscreen (screen max y: %s)' % self._screenheight)
463 max_height = self._screenheight - dropdown_new_y - 4
464 self.__mac_log('max dropdown height would be: %s' % max_height)
465 if max_height > ((pw_size.height * 2) + 4):
466 dropdown_size.SetHeight(max_height)
467 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)))
468 self.__mac_log('possible dropdown size: %s' % dropdown_size)
469
470
471 self._picklist_dropdown.SetSize(dropdown_size)
472 self._picklist.SetSize(self._picklist_dropdown.GetClientSize())
473 self.__mac_log('pick list size set to: %s' % self._picklist_dropdown.GetSize())
474 if self.__dropdown_needs_relative_position:
475 dropdown_new_x, dropdown_new_y = self._picklist_dropdown.GetParent().ScreenToClientXY(dropdown_new_x, dropdown_new_y)
476 self._picklist_dropdown.MoveXY(dropdown_new_x, dropdown_new_y)
477
478
479 self._picklist.Select(0)
480
481
482 self._picklist_dropdown.Show(True)
483
484
485
486
487
488
489
490
491
492
493
495 """Hide the pick list."""
496 self._picklist_dropdown.Hide()
497
499 """Mark the given picklist row as selected."""
500 if old_row_idx is not None:
501 pass
502 self._picklist.Select(new_row_idx)
503 self._picklist.EnsureVisible(new_row_idx)
504
506 """Get string to display in the field for the given picklist item."""
507 if item is None:
508 item = self._picklist.get_selected_item()
509 try:
510 return item['field_label']
511 except KeyError:
512 pass
513 try:
514 return item['list_label']
515 except KeyError:
516 pass
517 try:
518 return item['label']
519 except KeyError:
520 return u'<no field_*/list_*/label in item>'
521
522
524 """Update the display to show item strings."""
525
526 display_string = self._picklist_item2display_string(item = item)
527 self.suppress_text_update_smarts = True
528 super(cPhraseWheelBase, self).SetValue(display_string)
529
530 self.SetInsertionPoint(self.GetLastPosition())
531 return
532
533
534
536 raise NotImplementedError('[%s]: fragment extraction not implemented' % self.__class__.__name__)
537
539 """Get candidates matching the currently typed input."""
540
541
542 self._current_match_candidates = []
543 if self.matcher is not None:
544 matched, self._current_match_candidates = self.matcher.getMatches(val)
545 self._picklist.SetItems(self._current_match_candidates)
546
547
548
549
550
551 if len(self._current_match_candidates) == 0:
552 suggestions = self._get_suggestions_from_spell_checker(val)
553 if suggestions is not None:
554 self._current_match_candidates = [
555 {'list_label': suggestion, 'field_label': suggestion, 'data': None}
556 for suggestion in suggestions
557 ]
558 self._picklist.SetItems(self._current_match_candidates)
559
560
561
565
611
613 return self.__static_tt_extra
614
616 self.__static_tt_extra = tt
617
618 static_tooltip_extra = property(_get_static_tt_extra, _set_static_tt_extra)
619
620
621
623 wx.EVT_KEY_DOWN (self, self._on_key_down)
624 wx.EVT_SET_FOCUS(self, self._on_set_focus)
625 wx.EVT_KILL_FOCUS(self, self._on_lose_focus)
626 wx.EVT_TEXT(self, self.GetId(), self._on_text_update)
627 self._picklist.Bind(wx.EVT_LEFT_DCLICK, self._on_list_item_selected)
628
630 """Is called when a key is pressed."""
631
632 keycode = event.GetKeyCode()
633
634 if keycode == wx.WXK_DOWN:
635 self.__on_cursor_down()
636 return
637
638 if keycode == wx.WXK_UP:
639 self.__on_cursor_up()
640 return
641
642 if keycode == wx.WXK_RETURN:
643 self._on_enter()
644 return
645
646 if keycode == wx.WXK_TAB:
647 if event.ShiftDown():
648 self.Navigate(flags = wx.NavigationKeyEvent.IsBackward)
649 return
650 self.__on_tab()
651 self.Navigate(flags = wx.NavigationKeyEvent.IsForward)
652 return
653
654
655 if keycode in [wx.WXK_SHIFT, wx.WXK_BACK, wx.WXK_DELETE, wx.WXK_LEFT, wx.WXK_RIGHT]:
656 pass
657
658
659 elif not self.__char_is_allowed(char = unichr(event.GetUnicodeKey())):
660 wx.Bell()
661
662 return
663
664 event.Skip()
665 return
666
668
669 self._has_focus = True
670 event.Skip()
671
672 self.__non_edit_font = self.GetFont()
673 edit_font = self.GetFont()
674 edit_font.SetPointSize(pointSize = self.__non_edit_font.GetPointSize() + 1)
675 self.SetFont(edit_font)
676 self.Refresh()
677
678
679 for callback in self._on_set_focus_callbacks:
680 callback()
681
682 self.__timer.Start(oneShot = True, milliseconds = self.picklist_delay)
683 return True
684
686 """Do stuff when leaving the control.
687
688 The user has had her say, so don't second guess
689 intentions but do report error conditions.
690 """
691 self._has_focus = False
692
693 self.__timer.Stop()
694 self._hide_picklist()
695 self.SetSelection(1,1)
696 self.SetFont(self.__non_edit_font)
697 self.Refresh()
698
699 is_valid = True
700
701
702
703
704 self._set_data_to_first_match()
705
706
707 if self.__final_regex.match(self.GetValue().strip()) is None:
708 gmDispatcher.send(signal = 'statustext', msg = self.final_regex_error_msg)
709 is_valid = False
710
711 self.display_as_valid(valid = is_valid)
712
713
714 for callback in self._on_lose_focus_callbacks:
715 callback()
716
717 event.Skip()
718 return True
719
721 """Gets called when user selected a list item."""
722
723 self._hide_picklist()
724
725 item = self._picklist.get_selected_item()
726
727 if item is None:
728 self.display_as_valid(valid = True)
729 return
730
731 self._update_display_from_picked_item(item)
732 self._update_data_from_picked_item(item)
733 self.MarkDirty()
734
735
736 for callback in self._on_selection_callbacks:
737 callback(self._data)
738
739 if self.navigate_after_selection:
740 self.Navigate()
741
742 return
743
744 - def _on_text_update (self, event):
745 """Internal handler for wx.EVT_TEXT.
746
747 Called when text was changed by user or by SetValue().
748 """
749 if self.suppress_text_update_smarts:
750 self.suppress_text_update_smarts = False
751 return
752
753 self._adjust_data_after_text_update()
754 self._current_match_candidates = []
755
756 val = self.GetValue().strip()
757 ins_point = self.GetInsertionPoint()
758
759
760
761 if val == u'':
762 self._hide_picklist()
763 self.__timer.Stop()
764 else:
765 new_val = gmTools.capitalize(text = val, mode = self.capitalisation_mode)
766 if new_val != val:
767 self.suppress_text_update_smarts = True
768 super(cPhraseWheelBase, self).SetValue(new_val)
769 if ins_point > len(new_val):
770 self.SetInsertionPointEnd()
771 else:
772 self.SetInsertionPoint(ins_point)
773
774
775
776 self.__timer.Start(oneShot = True, milliseconds = self.picklist_delay)
777
778
779 for callback in self._on_modified_callbacks:
780 callback()
781
782 return
783
784
785
787 """Called when the user pressed <ENTER>."""
788 if self._picklist_dropdown.IsShown():
789 self._on_list_item_selected()
790 else:
791
792 self.Navigate()
793
795
796 if self._picklist_dropdown.IsShown():
797 idx_selected = self._picklist.GetFirstSelected()
798 if idx_selected < (len(self._current_match_candidates) - 1):
799 self._select_picklist_row(idx_selected + 1, idx_selected)
800 return
801
802
803
804
805
806 self.__timer.Stop()
807 if self.GetValue().strip() == u'':
808 val = u'*'
809 else:
810 val = self._extract_fragment_to_match_on()
811 self._update_candidates_in_picklist(val = val)
812 self._show_picklist(input2match = val)
813
815 if self._picklist_dropdown.IsShown():
816 selected = self._picklist.GetFirstSelected()
817 if selected > 0:
818 self._select_picklist_row(selected-1, selected)
819 else:
820
821 pass
822
824 """Under certain circumstances take special action on <TAB>.
825
826 returns:
827 True: <TAB> was handled
828 False: <TAB> was not handled
829
830 -> can be used to decide whether to do further <TAB> handling outside this class
831 """
832
833 if not self._picklist_dropdown.IsShown():
834 return False
835
836
837 if len(self._current_match_candidates) != 1:
838 return False
839
840
841 if not self.selection_only:
842 return False
843
844
845 self._select_picklist_row(new_row_idx = 0)
846 self._on_list_item_selected()
847
848 return True
849
850
851
853 self.__timer = _cPRWTimer()
854 self.__timer.callback = self._on_timer_fired
855
856 self.__timer.Stop()
857
859 """Callback for delayed match retrieval timer.
860
861 if we end up here:
862 - delay has passed without user input
863 - the value in the input field has not changed since the timer started
864 """
865
866 val = self._extract_fragment_to_match_on()
867 self._update_candidates_in_picklist(val = val)
868
869
870
871
872
873
874 wx.CallAfter(self._show_picklist, input2match = val)
875
876
877
879 if self.__use_fake_popup:
880 _log.debug(msg)
881
883
884 if self.accepted_chars is None:
885 return True
886 return (self.__accepted_chars.match(char) is not None)
887
893
895 if self.__accepted_chars is None:
896 return None
897 return self.__accepted_chars.pattern
898
899 accepted_chars = property(_get_accepted_chars, _set_accepted_chars)
900
902 self.__final_regex = regex.compile(final_regex, flags = regex.LOCALE | regex.UNICODE)
903
905 return self.__final_regex.pattern
906
907 final_regex = property(_get_final_regex, _set_final_regex)
908
910 self.__final_regex_error_msg = msg % self.final_regex
911
913 return self.__final_regex_error_msg
914
915 final_regex_error_msg = property(_get_final_regex_error_msg, _set_final_regex_error_msg)
916
917
918
921
923 self.data = {item['field_label']: item}
924
926 raise NotImplementedError('[%s]: _dictify_data()' % self.__class__.__name__)
927
929 raise NotImplementedError('[%s]: cannot adjust data after text update' % self.__class__.__name__)
930
935
937 raise NotImplementedError('[%s]: cannot create data object' % self.__class__.__name__)
938
941
943 self._data = data
944 self.__recalculate_tooltip()
945
946 data = property(_get_data, _set_data)
947
948
949
950
951
952
953
954
955
956
957
958
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
1017
1018 - def GetData(self, can_create=False, as_instance=False):
1019
1020 super(cPhraseWheel, self).GetData(can_create = can_create)
1021
1022 if len(self._data) > 0:
1023 if as_instance:
1024 return self._data2instance()
1025
1026 if len(self._data) == 0:
1027 return None
1028
1029 return self._data.values()[0]['data']
1030
1032 """Set the data and thereby set the value, too. if possible.
1033
1034 If you call SetData() you better be prepared
1035 doing a scan of the entire potential match space.
1036
1037 The whole thing will only work if data is found
1038 in the match space anyways.
1039 """
1040
1041 self._update_candidates_in_picklist(u'*')
1042
1043
1044 if self.selection_only:
1045
1046 if len(self._current_match_candidates) == 0:
1047 return False
1048
1049
1050 for candidate in self._current_match_candidates:
1051 if candidate['data'] == data:
1052 super(cPhraseWheel, self).SetText (
1053 value = candidate['field_label'],
1054 data = data,
1055 suppress_smarts = True
1056 )
1057 return True
1058
1059
1060 if self.selection_only:
1061 self.display_as_valid(valid = False)
1062 return False
1063
1064 self.data = self._dictify_data(data = data)
1065 self.display_as_valid(valid = True)
1066 return True
1067
1068
1069
1071
1072
1073
1074
1075 if len(self._data) > 0:
1076 self._picklist_dropdown.Hide()
1077 return
1078
1079 return super(cPhraseWheel, self)._show_picklist(input2match = input2match)
1080
1082
1083 if len(self._data) > 0:
1084 return True
1085
1086
1087 val = self.GetValue().strip()
1088 if val == u'':
1089 return True
1090
1091
1092 self._update_candidates_in_picklist(val = val)
1093 for candidate in self._current_match_candidates:
1094 if candidate['field_label'] == val:
1095 self.data = {candidate['field_label']: candidate}
1096 self.MarkDirty()
1097 return True
1098
1099
1100 if self.selection_only:
1101 gmDispatcher.send(signal = 'statustext', msg = self.selection_only_error_msg)
1102 is_valid = False
1103 return False
1104
1105 return True
1106
1109
1112
1118
1120
1129
1130 - def GetData(self, can_create=False, as_instance=False):
1131
1132 super(cMultiPhraseWheel, self).GetData(can_create = can_create)
1133
1134 if len(self._data) > 0:
1135 if as_instance:
1136 return self._data2instance()
1137
1138 return self._data.values()
1139
1141 self.speller = None
1142 return True
1143
1145
1146 data_dict = {}
1147
1148 for item in data_items:
1149 try:
1150 list_label = item['list_label']
1151 except KeyError:
1152 list_label = item['label']
1153 try:
1154 field_label = item['field_label']
1155 except KeyError:
1156 field_label = list_label
1157 data_dict[field_label] = {'data': item['data'], 'list_label': list_label, 'field_label': field_label}
1158
1159 return data_dict
1160
1161
1162
1165
1167
1168 new_data = {}
1169
1170
1171 for displayed_label in self.displayed_strings:
1172 try:
1173 new_data[displayed_label] = self._data[displayed_label]
1174 except KeyError:
1175
1176
1177 pass
1178
1179 self.data = new_data
1180
1182
1183 cursor_pos = self.GetInsertionPoint()
1184
1185 entire_input = self.GetValue()
1186 if self.__phrase_separators.search(entire_input) is None:
1187 self.left_part = u''
1188 self.right_part = u''
1189 return self.GetValue().strip()
1190
1191 string_left_of_cursor = entire_input[:cursor_pos]
1192 string_right_of_cursor = entire_input[cursor_pos:]
1193
1194 left_parts = [ lp.strip() for lp in self.__phrase_separators.split(string_left_of_cursor) ]
1195 if len(left_parts) == 0:
1196 self.left_part = u''
1197 else:
1198 self.left_part = u'%s%s ' % (
1199 (u'%s ' % self.__phrase_separators.pattern[0]).join(left_parts[:-1]),
1200 self.__phrase_separators.pattern[0]
1201 )
1202
1203 right_parts = [ rp.strip() for rp in self.__phrase_separators.split(string_right_of_cursor) ]
1204 self.right_part = u'%s %s' % (
1205 self.__phrase_separators.pattern[0],
1206 (u'%s ' % self.__phrase_separators.pattern[0]).join(right_parts[1:])
1207 )
1208
1209 val = (left_parts[-1] + right_parts[0]).strip()
1210 return val
1211
1213 val = (u'%s%s%s' % (
1214 self.left_part,
1215 self._picklist_item2display_string(item = item),
1216 self.right_part
1217 )).lstrip().lstrip(';').strip()
1218 self.suppress_text_update_smarts = True
1219 super(cMultiPhraseWheel, self).SetValue(val)
1220
1221 item_end = val.index(item['field_label']) + len(item['field_label'])
1222 self.SetInsertionPoint(item_end)
1223 return
1224
1226
1227
1228 self._data[item['field_label']] = item
1229
1230
1231 field_labels = [ p.strip() for p in self.__phrase_separators.split(self.GetValue().strip()) ]
1232 new_data = {}
1233
1234
1235 for field_label in field_labels:
1236 try:
1237 new_data[field_label] = self._data[field_label]
1238 except KeyError:
1239
1240
1241 pass
1242
1243 self.data = new_data
1244
1251
1252
1253
1255 """Set phrase separators.
1256
1257 - must be a valid regular expression pattern
1258
1259 input is split into phrases at boundaries defined by
1260 this regex and matching is performed on the phrase
1261 the cursor is in only,
1262
1263 after selection from picklist phrase_separators[0] is
1264 added to the end of the match in the PRW
1265 """
1266 self.__phrase_separators = regex.compile(phrase_separators, flags = regex.LOCALE | regex.UNICODE)
1267
1269 return self.__phrase_separators.pattern
1270
1271 phrase_separators = property(_get_phrase_separators, _set_phrase_separators)
1272
1274 return [ p.strip() for p in self.__phrase_separators.split(self.GetValue().strip()) if p.strip() != u'' ]
1275
1276 displayed_strings = property(_get_displayed_strings, lambda x:x)
1277
1278
1279
1280 if __name__ == '__main__':
1281
1282 if len(sys.argv) < 2:
1283 sys.exit()
1284
1285 if sys.argv[1] != u'test':
1286 sys.exit()
1287
1288 from Gnumed.pycommon import gmI18N
1289 gmI18N.activate_locale()
1290 gmI18N.install_domain(domain='gnumed')
1291
1292 from Gnumed.pycommon import gmPG2, gmMatchProvider
1293
1294 prw = None
1295
1297 print "got focus:"
1298 print "value:", prw.GetValue()
1299 print "data :", prw.GetData()
1300 return True
1301
1303 print "lost focus:"
1304 print "value:", prw.GetValue()
1305 print "data :", prw.GetData()
1306 return True
1307
1309 print "modified:"
1310 print "value:", prw.GetValue()
1311 print "data :", prw.GetData()
1312 return True
1313
1315 print "selected:"
1316 print "value:", prw.GetValue()
1317 print "data :", prw.GetData()
1318 return True
1319
1320
1322 app = wx.PyWidgetTester(size = (200, 50))
1323
1324 items = [ {'data': 1, 'list_label': "Bloggs", 'field_label': "Bloggs", 'weight': 0},
1325 {'data': 2, 'list_label': "Baker", 'field_label': "Baker", 'weight': 0},
1326 {'data': 3, 'list_label': "Jones", 'field_label': "Jones", 'weight': 0},
1327 {'data': 4, 'list_label': "Judson", 'field_label': "Judson", 'weight': 0},
1328 {'data': 5, 'list_label': "Jacobs", 'field_label': "Jacobs", 'weight': 0},
1329 {'data': 6, 'list_label': "Judson-Jacobs", 'field_label': "Judson-Jacobs", 'weight': 0}
1330 ]
1331
1332 mp = gmMatchProvider.cMatchProvider_FixedList(items)
1333
1334 mp.word_separators = '[ \t=+&:@]+'
1335 global prw
1336 prw = cPhraseWheel(parent = app.frame, id = -1)
1337 prw.matcher = mp
1338 prw.capitalisation_mode = gmTools.CAPS_NAMES
1339 prw.add_callback_on_set_focus(callback=display_values_set_focus)
1340 prw.add_callback_on_modified(callback=display_values_modified)
1341 prw.add_callback_on_lose_focus(callback=display_values_lose_focus)
1342 prw.add_callback_on_selection(callback=display_values_selected)
1343
1344 app.frame.Show(True)
1345 app.MainLoop()
1346
1347 return True
1348
1350 print "Do you want to test the database connected phrase wheel ?"
1351 yes_no = raw_input('y/n: ')
1352 if yes_no != 'y':
1353 return True
1354
1355 gmPG2.get_connection()
1356 query = u"""SELECT code, code || ': ' || _(name), _(name) FROM dem.country WHERE _(name) %(fragment_condition)s"""
1357 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query])
1358 app = wx.PyWidgetTester(size = (400, 50))
1359 global prw
1360
1361 prw = cMultiPhraseWheel(parent = app.frame, id = -1)
1362 prw.matcher = mp
1363
1364 app.frame.Show(True)
1365 app.MainLoop()
1366
1367 return True
1368
1370 gmPG2.get_connection()
1371 query = u"""
1372 select
1373 pk_identity,
1374 firstnames || ' ' || lastnames || ', ' || to_char(dob, 'YYYY-MM-DD'),
1375 firstnames || ' ' || lastnames
1376 from
1377 dem.v_basic_person
1378 where
1379 firstnames || lastnames %(fragment_condition)s
1380 """
1381 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query])
1382 app = wx.PyWidgetTester(size = (500, 50))
1383 global prw
1384 prw = cPhraseWheel(parent = app.frame, id = -1)
1385 prw.matcher = mp
1386 prw.selection_only = True
1387
1388 app.frame.Show(True)
1389 app.MainLoop()
1390
1391 return True
1392
1410
1411
1412
1413
1414 test_prw_patients()
1415
1416
1417