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