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 """Widget for smart guessing of user fields, after Richard Terry's interface.
73
74 - VB implementation by Richard Terry
75 - Python port by Ian Haywood for GNUmed
76 - enhanced by Karsten Hilbert for GNUmed
77 - enhanced by Ian Haywood for aumed
78 - enhanced by Karsten Hilbert for GNUmed
79
80 @param matcher: a class used to find matches for the current input
81 @type matcher: a L{match provider<Gnumed.pycommon.gmMatchProvider.cMatchProvider>}
82 instance or C{None}
83
84 @param selection_only: whether free-text can be entered without associated data
85 @type selection_only: boolean
86
87 @param capitalisation_mode: how to auto-capitalize input, valid values
88 are found in L{capitalize()<Gnumed.pycommon.gmTools.capitalize>}
89 @type capitalisation_mode: integer
90
91 @param accepted_chars: a regex pattern defining the characters
92 acceptable in the input string, if None no checking is performed
93 @type accepted_chars: None or a string holding a valid regex pattern
94
95 @param final_regex: when the control loses focus the input is
96 checked against this regular expression
97 @type final_regex: a string holding a valid regex pattern
98
99 @param navigate_after_selection: whether or not to immediately
100 navigate to the widget next-in-tab-order after selecting an
101 item from the dropdown picklist
102 @type navigate_after_selection: boolean
103
104 @param speller: if not None used to spellcheck the current input
105 and to retrieve suggested replacements/completions
106 @type speller: None or a L{enchant Dict<enchant>} descendant
107
108 @param picklist_delay: this much time of user inactivity must have
109 passed before the input related smarts kick in and the drop
110 down pick list is shown
111 @type picklist_delay: integer (milliseconds)
112 """
113
115 try:
116 kwargs['style'] = kwargs['style'] | wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.SIMPLE_BORDER
117 except: pass
118 wx.ListCtrl.__init__(self, *args, **kwargs)
119 listmixins.ListCtrlAutoWidthMixin.__init__(self)
120
122 self.DeleteAllItems()
123 self.__data = items
124 pos = len(items) + 1
125 for item in items:
126 row_num = self.InsertStringItem(pos, label=item['list_label'])
127
129 sel_idx = self.GetFirstSelected()
130 if sel_idx == -1:
131 return None
132 return self.__data[sel_idx]['data']
133
135 sel_idx = self.GetFirstSelected()
136 if sel_idx == -1:
137 return None
138 return self.__data[sel_idx]
139
141 sel_idx = self.GetFirstSelected()
142 if sel_idx == -1:
143 return None
144 return self.__data[sel_idx]['list_label']
145
146
147
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
184 except KeyError:
185 kwargs['style'] = wx.TE_PROCESS_TAB
186 super(cPhraseWheelBase, self).__init__(parent, id, **kwargs)
187
188 self.__non_edit_font = self.GetFont()
189 self.__color_valid = self.GetBackgroundColour()
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.__color_valid)
245 elif valid is False:
246 self.SetBackgroundColour(color_prw_invalid)
247 else:
248 raise ValueError(u'<valid> must be True or False')
249 self.Refresh()
250
251
252
254 """Add a callback for invocation when a picklist item is selected.
255
256 The callback will be invoked whenever an item is selected
257 from the picklist. The associated data is passed in as
258 a single parameter. Callbacks must be able to cope with
259 None as the data parameter as that is sent whenever the
260 user changes a previously selected value.
261 """
262 if not callable(callback):
263 raise ValueError('[add_callback_on_selection]: ignoring callback [%s], it is not callable' % callback)
264
265 self._on_selection_callbacks.append(callback)
266
268 """Add a callback for invocation when getting focus."""
269 if not callable(callback):
270 raise ValueError('[add_callback_on_set_focus]: ignoring callback [%s] - not callable' % callback)
271
272 self._on_set_focus_callbacks.append(callback)
273
275 """Add a callback for invocation when losing focus."""
276 if not callable(callback):
277 raise ValueError('[add_callback_on_lose_focus]: ignoring callback [%s] - not callable' % callback)
278
279 self._on_lose_focus_callbacks.append(callback)
280
282 """Add a callback for invocation when the content is modified."""
283 if not callable(callback):
284 raise ValueError('[add_callback_on_modified]: ignoring callback [%s] - not callable' % callback)
285
286 self._on_modified_callbacks.append(callback)
287
288
289
290 - def set_context(self, context=None, val=None):
291 if self.matcher is not None:
292 self.matcher.set_context(context=context, val=val)
293
294 - def unset_context(self, context=None):
295 if self.matcher is not None:
296 self.matcher.unset_context(context=context)
297
298
299
301
302 try:
303 import enchant
304 except ImportError:
305 self.speller = None
306 return False
307
308 try:
309 self.speller = enchant.DictWithPWL(None, os.path.expanduser(os.path.join('~', '.gnumed', 'spellcheck', 'wordlist.pwl')))
310 except enchant.DictNotFoundError:
311 self.speller = None
312 return False
313
314 return True
315
317 if self.speller is None:
318 return None
319
320
321 last_word = self.__speller_word_separators.split(val)[-1]
322 if last_word.strip() == u'':
323 return None
324
325 try:
326 suggestions = self.speller.suggest(last_word)
327 except:
328 _log.exception('had to disable (enchant) spell checker')
329 self.speller = None
330 return None
331
332 if len(suggestions) == 0:
333 return None
334
335 input2match_without_last_word = val[:val.rindex(last_word)]
336 return [ input2match_without_last_word + suggestion for suggestion in suggestions ]
337
343
345 return self.__speller_word_separators.pattern
346
347 speller_word_separators = property(_get_speller_word_separators, _set_speller_word_separators)
348
349
350
351
352
354 szr_dropdown = None
355 try:
356
357 self.__dropdown_needs_relative_position = False
358 self._picklist_dropdown = wx.PopupWindow(parent)
359 list_parent = self._picklist_dropdown
360 self.__use_fake_popup = False
361 except NotImplementedError:
362 self.__use_fake_popup = True
363
364
365 add_picklist_to_sizer = True
366 szr_dropdown = wx.BoxSizer(wx.VERTICAL)
367
368
369 self.__dropdown_needs_relative_position = False
370 self._picklist_dropdown = wx.MiniFrame (
371 parent = parent,
372 id = -1,
373 style = wx.SIMPLE_BORDER | wx.FRAME_FLOAT_ON_PARENT | wx.FRAME_NO_TASKBAR | wx.POPUP_WINDOW
374 )
375 scroll_win = wx.ScrolledWindow(parent = self._picklist_dropdown, style = wx.NO_BORDER)
376 scroll_win.SetSizer(szr_dropdown)
377 list_parent = scroll_win
378
379
380
381
382
383
384
385 self.__mac_log('dropdown parent: %s' % self._picklist_dropdown.GetParent())
386
387 self._picklist = cPhraseWheelListCtrl (
388 list_parent,
389 style = wx.LC_NO_HEADER
390 )
391 self._picklist.InsertColumn(0, u'')
392
393 if szr_dropdown is not None:
394 szr_dropdown.Add(self._picklist, 1, wx.EXPAND)
395
396 self._picklist_dropdown.Hide()
397
399 """Display the pick list if useful."""
400
401 self._picklist_dropdown.Hide()
402
403 if not self._has_focus:
404 return
405
406 if len(self._current_match_candidates) == 0:
407 return
408
409
410
411 if len(self._current_match_candidates) == 1:
412 candidate = self._current_match_candidates[0]
413 if candidate['field_label'] == input2match:
414 self._update_data_from_picked_item(candidate)
415 return
416
417 border_width = 4
418 extra_height = 25
419
420
421 rows = len(self._current_match_candidates)
422 if rows < 2:
423 rows = 2
424 if rows > 20:
425 rows = 20
426 self.__mac_log('dropdown needs rows: %s' % rows)
427 dropdown_size = self._picklist_dropdown.GetSize()
428 pw_size = self.GetSize()
429 dropdown_size.SetWidth(pw_size.width)
430 dropdown_size.SetHeight (
431 (pw_size.height * rows)
432 + border_width
433 + extra_height
434 )
435
436
437 (pw_x_abs, pw_y_abs) = self.ClientToScreenXY(0,0)
438 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)))
439 dropdown_new_x = pw_x_abs
440 dropdown_new_y = pw_y_abs + pw_size.height
441 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)))
442 self.__mac_log('desired dropdown size: %s' % dropdown_size)
443
444
445 if (dropdown_new_y + dropdown_size.height) > self._screenheight:
446 self.__mac_log('dropdown extends offscreen (screen max y: %s)' % self._screenheight)
447 max_height = self._screenheight - dropdown_new_y - 4
448 self.__mac_log('max dropdown height would be: %s' % max_height)
449 if max_height > ((pw_size.height * 2) + 4):
450 dropdown_size.SetHeight(max_height)
451 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)))
452 self.__mac_log('possible dropdown size: %s' % dropdown_size)
453
454
455 self._picklist_dropdown.SetSize(dropdown_size)
456 self._picklist.SetSize(self._picklist_dropdown.GetClientSize())
457 self.__mac_log('pick list size set to: %s' % self._picklist_dropdown.GetSize())
458 if self.__dropdown_needs_relative_position:
459 dropdown_new_x, dropdown_new_y = self._picklist_dropdown.GetParent().ScreenToClientXY(dropdown_new_x, dropdown_new_y)
460 self._picklist_dropdown.MoveXY(dropdown_new_x, dropdown_new_y)
461
462
463 self._picklist.Select(0)
464
465
466 self._picklist_dropdown.Show(True)
467
468
469
470
471
472
473
474
475
476
477
479 """Hide the pick list."""
480 self._picklist_dropdown.Hide()
481
483 """Mark the given picklist row as selected."""
484 if old_row_idx is not None:
485 pass
486 self._picklist.Select(new_row_idx)
487 self._picklist.EnsureVisible(new_row_idx)
488
490 """Get string to display in the field for the given picklist item."""
491 if item is None:
492 item = self._picklist.get_selected_item()
493 try:
494 return item['field_label']
495 except KeyError:
496 pass
497 try:
498 return item['list_label']
499 except KeyError:
500 pass
501 try:
502 return item['label']
503 except KeyError:
504 return u'<no field_*/list_*/label in item>'
505
506
508 """Update the display to show item strings."""
509
510 display_string = self._picklist_item2display_string(item = item)
511 self.suppress_text_update_smarts = True
512 super(cPhraseWheelBase, self).SetValue(display_string)
513
514 self.SetInsertionPoint(self.GetLastPosition())
515 return
516
517
518
520 raise NotImplementedError('[%s]: fragment extraction not implemented' % self.__class__.__name__)
521
523 """Get candidates matching the currently typed input."""
524
525
526 self._current_match_candidates = []
527 if self.matcher is not None:
528 matched, self._current_match_candidates = self.matcher.getMatches(val)
529 self._picklist.SetItems(self._current_match_candidates)
530
531
532
533
534
535 if len(self._current_match_candidates) == 0:
536 suggestions = self._get_suggestions_from_spell_checker(val)
537 if suggestions is not None:
538 self._current_match_candidates = [
539 {'list_label': suggestion, 'field_label': suggestion, 'data': None}
540 for suggestion in suggestions
541 ]
542 self._picklist.SetItems(self._current_match_candidates)
543
544
545
549
590
592 return self.__static_tt_extra
593
595 self.__static_tt_extra = tt
596
597 static_tooltip_extra = property(_get_static_tt_extra, _set_static_tt_extra)
598
599
600
602 wx.EVT_KEY_DOWN (self, self._on_key_down)
603 wx.EVT_SET_FOCUS(self, self._on_set_focus)
604 wx.EVT_KILL_FOCUS(self, self._on_lose_focus)
605 wx.EVT_TEXT(self, self.GetId(), self._on_text_update)
606 self._picklist.Bind(wx.EVT_LEFT_DCLICK, self._on_list_item_selected)
607
609 """Is called when a key is pressed."""
610
611 keycode = event.GetKeyCode()
612
613 if keycode == wx.WXK_DOWN:
614 self.__on_cursor_down()
615 return
616
617 if keycode == wx.WXK_UP:
618 self.__on_cursor_up()
619 return
620
621 if keycode == wx.WXK_RETURN:
622 self._on_enter()
623 return
624
625 if keycode == wx.WXK_TAB:
626 if event.ShiftDown():
627 self.Navigate(flags = wx.NavigationKeyEvent.IsBackward)
628 return
629 self.__on_tab()
630 self.Navigate(flags = wx.NavigationKeyEvent.IsForward)
631 return
632
633
634 if keycode in [wx.WXK_SHIFT, wx.WXK_BACK, wx.WXK_DELETE, wx.WXK_LEFT, wx.WXK_RIGHT]:
635 pass
636
637
638 elif not self.__char_is_allowed(char = unichr(event.GetUnicodeKey())):
639 wx.Bell()
640
641 return
642
643 event.Skip()
644 return
645
647
648 self._has_focus = True
649 event.Skip()
650
651 self.__non_edit_font = self.GetFont()
652 edit_font = self.GetFont()
653 edit_font.SetPointSize(pointSize = self.__non_edit_font.GetPointSize() + 1)
654 self.SetFont(edit_font)
655 self.Refresh()
656
657
658 for callback in self._on_set_focus_callbacks:
659 callback()
660
661 self.__timer.Start(oneShot = True, milliseconds = self.picklist_delay)
662 return True
663
665 """Do stuff when leaving the control.
666
667 The user has had her say, so don't second guess
668 intentions but do report error conditions.
669 """
670 self._has_focus = False
671
672 self.__timer.Stop()
673 self._hide_picklist()
674 self.SetSelection(1,1)
675 self.SetFont(self.__non_edit_font)
676 self.Refresh()
677
678 is_valid = True
679
680
681
682
683 self._set_data_to_first_match()
684
685
686 if self.__final_regex.match(self.GetValue().strip()) is None:
687 wx.lib.pubsub.Publisher().sendMessage (
688 topic = 'statustext',
689 data = {'msg': self.final_regex_error_msg}
690 )
691 is_valid = False
692
693 self.display_as_valid(valid = is_valid)
694
695
696 for callback in self._on_lose_focus_callbacks:
697 callback()
698
699 event.Skip()
700 return True
701
703 """Gets called when user selected a list item."""
704
705 self._hide_picklist()
706
707 item = self._picklist.get_selected_item()
708
709 if item is None:
710 self.display_as_valid(valid = True)
711 return
712
713 self._update_display_from_picked_item(item)
714 self._update_data_from_picked_item(item)
715 self.MarkDirty()
716
717
718 for callback in self._on_selection_callbacks:
719 callback(self._data)
720
721 if self.navigate_after_selection:
722 self.Navigate()
723
724 return
725
726 - def _on_text_update (self, event):
727 """Internal handler for wx.EVT_TEXT.
728
729 Called when text was changed by user or by SetValue().
730 """
731 if self.suppress_text_update_smarts:
732 self.suppress_text_update_smarts = False
733 return
734
735 self._adjust_data_after_text_update()
736 self._current_match_candidates = []
737
738 val = self.GetValue().strip()
739 ins_point = self.GetInsertionPoint()
740
741
742
743 if val == u'':
744 self._hide_picklist()
745 self.__timer.Stop()
746 else:
747 new_val = gmTools.capitalize(text = val, mode = self.capitalisation_mode)
748 if new_val != val:
749 self.suppress_text_update_smarts = True
750 super(cPhraseWheelBase, self).SetValue(new_val)
751 if ins_point > len(new_val):
752 self.SetInsertionPointEnd()
753 else:
754 self.SetInsertionPoint(ins_point)
755
756
757
758 self.__timer.Start(oneShot = True, milliseconds = self.picklist_delay)
759
760
761 for callback in self._on_modified_callbacks:
762 callback()
763
764 return
765
766
767
769 """Called when the user pressed <ENTER>."""
770 if self._picklist_dropdown.IsShown():
771 self._on_list_item_selected()
772 else:
773
774 self.Navigate()
775
777
778 if self._picklist_dropdown.IsShown():
779 idx_selected = self._picklist.GetFirstSelected()
780 if idx_selected < (len(self._current_match_candidates) - 1):
781 self._select_picklist_row(idx_selected + 1, idx_selected)
782 return
783
784
785
786
787
788 self.__timer.Stop()
789 if self.GetValue().strip() == u'':
790 val = u'*'
791 else:
792 val = self._extract_fragment_to_match_on()
793 self._update_candidates_in_picklist(val = val)
794 self._show_picklist(input2match = val)
795
797 if self._picklist_dropdown.IsShown():
798 selected = self._picklist.GetFirstSelected()
799 if selected > 0:
800 self._select_picklist_row(selected-1, selected)
801 else:
802
803 pass
804
806 """Under certain circumstances take special action on <TAB>.
807
808 returns:
809 True: <TAB> was handled
810 False: <TAB> was not handled
811
812 -> can be used to decide whether to do further <TAB> handling outside this class
813 """
814
815 if not self._picklist_dropdown.IsShown():
816 return False
817
818
819 if len(self._current_match_candidates) != 1:
820 return False
821
822
823 if not self.selection_only:
824 return False
825
826
827 self._select_picklist_row(new_row_idx = 0)
828 self._on_list_item_selected()
829
830 return True
831
832
833
835 self.__timer = _cPRWTimer()
836 self.__timer.callback = self._on_timer_fired
837
838 self.__timer.Stop()
839
841 """Callback for delayed match retrieval timer.
842
843 if we end up here:
844 - delay has passed without user input
845 - the value in the input field has not changed since the timer started
846 """
847
848 val = self._extract_fragment_to_match_on()
849 self._update_candidates_in_picklist(val = val)
850
851
852
853
854
855
856 wx.CallAfter(self._show_picklist, input2match = val)
857
858
859
861 if self.__use_fake_popup:
862 _log.debug(msg)
863
865
866 if self.accepted_chars is None:
867 return True
868 return (self.__accepted_chars.match(char) is not None)
869
875
877 if self.__accepted_chars is None:
878 return None
879 return self.__accepted_chars.pattern
880
881 accepted_chars = property(_get_accepted_chars, _set_accepted_chars)
882
884 self.__final_regex = regex.compile(final_regex, flags = regex.LOCALE | regex.UNICODE)
885
887 return self.__final_regex.pattern
888
889 final_regex = property(_get_final_regex, _set_final_regex)
890
892 self.__final_regex_error_msg = msg % self.final_regex
893
895 return self.__final_regex_error_msg
896
897 final_regex_error_msg = property(_get_final_regex_error_msg, _set_final_regex_error_msg)
898
899
900
903
905 self.data = {item['field_label']: item}
906
908 raise NotImplementedError('[%s]: _dictify_data()' % self.__class__.__name__)
909
911 raise NotImplementedError('[%s]: cannot adjust data after text update' % self.__class__.__name__)
912
914 raise NotImplementedError('[%s]: cannot create data object' % self.__class__.__name__)
915
918
920 self._data = data
921 self.__recalculate_tooltip()
922
923 data = property(_get_data, _set_data)
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
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
994
995 - def GetData(self, can_create=False, as_instance=False):
996
997 super(cPhraseWheel, self).GetData(can_create = can_create)
998
999 if len(self._data) > 0:
1000 if as_instance:
1001 return self._data2instance()
1002
1003 if len(self._data) == 0:
1004 return None
1005
1006 return self._data.values()[0]['data']
1007
1009 """Set the data and thereby set the value, too. if possible.
1010
1011 If you call SetData() you better be prepared
1012 doing a scan of the entire potential match space.
1013
1014 The whole thing will only work if data is found
1015 in the match space anyways.
1016 """
1017
1018 self._update_candidates_in_picklist(u'*')
1019
1020
1021 if self.selection_only:
1022
1023 if len(self._current_match_candidates) == 0:
1024 return False
1025
1026
1027 for candidate in self._current_match_candidates:
1028 if candidate['data'] == data:
1029 super(cPhraseWheel, self).SetText (
1030 value = candidate['field_label'],
1031 data = data,
1032 suppress_smarts = True
1033 )
1034 return True
1035
1036
1037 if self.selection_only:
1038 self.display_as_valid(valid = False)
1039 return False
1040
1041 self.data = self._dictify_data(data = data)
1042 self.display_as_valid(valid = True)
1043 return True
1044
1045
1046
1048
1049
1050
1051
1052 if len(self._data) > 0:
1053 self._picklist_dropdown.Hide()
1054 return
1055
1056 return super(cPhraseWheel, self)._show_picklist(input2match = input2match)
1057
1059
1060 if len(self._data) > 0:
1061 return True
1062
1063
1064 val = self.GetValue().strip()
1065 if val == u'':
1066 return True
1067
1068
1069 self._update_candidates_in_picklist(val = val)
1070 for candidate in self._current_match_candidates:
1071 if candidate['field_label'] == val:
1072 self.data = {candidate['field_label']: candidate}
1073 self.MarkDirty()
1074 return True
1075
1076
1077 if self.selection_only:
1078 wx.lib.pubsub.Publisher().sendMessage (
1079 topic = 'statustext',
1080 data = {'msg': self.selection_only_error_msg}
1081 )
1082 is_valid = False
1083 return False
1084
1085 return True
1086
1089
1092
1098
1100
1109
1110 - def GetData(self, can_create=False, as_instance=False):
1111
1112 super(cMultiPhraseWheel, self).GetData(can_create = can_create)
1113
1114 if len(self._data) > 0:
1115 if as_instance:
1116 return self._data2instance()
1117
1118 return self._data.values()
1119
1121 self.speller = None
1122 return True
1123
1125
1126 data_dict = {}
1127
1128 for item in data_items:
1129 try:
1130 list_label = item['list_label']
1131 except KeyError:
1132 list_label = item['label']
1133 try:
1134 field_label = item['field_label']
1135 except KeyError:
1136 field_label = list_label
1137 data_dict[field_label] = {'data': item['data'], 'list_label': list_label, 'field_label': field_label}
1138
1139 return data_dict
1140
1141
1142
1145
1147
1148 displayed_labels = [ p.strip() for p in self.__phrase_separators.split(self.GetValue().strip()) ]
1149 new_data = {}
1150
1151
1152 for displayed_label in displayed_labels:
1153 try:
1154 new_data[displayed_label] = self._data[displayed_label]
1155 except KeyError:
1156
1157
1158 pass
1159
1160 self.data = new_data
1161
1163
1164 cursor_pos = self.GetInsertionPoint()
1165
1166 entire_input = self.GetValue()
1167 if self.__phrase_separators.search(entire_input) is None:
1168 self.left_part = u''
1169 self.right_part = u''
1170 return self.GetValue().strip()
1171
1172 string_left_of_cursor = entire_input[:cursor_pos]
1173 string_right_of_cursor = entire_input[cursor_pos:]
1174
1175 left_parts = [ lp.strip() for lp in self.__phrase_separators.split(string_left_of_cursor) ]
1176 if len(left_parts) == 0:
1177 self.left_part = u''
1178 else:
1179 self.left_part = u'%s%s ' % (
1180 (u'%s ' % self.__phrase_separators.pattern[0]).join(left_parts[:-1]),
1181 self.__phrase_separators.pattern[0]
1182 )
1183
1184 right_parts = [ rp.strip() for rp in self.__phrase_separators.split(string_right_of_cursor) ]
1185 self.right_part = u'%s %s' % (
1186 self.__phrase_separators.pattern[0],
1187 (u'%s ' % self.__phrase_separators.pattern[0]).join(right_parts[1:])
1188 )
1189
1190 val = (left_parts[-1] + right_parts[0]).strip()
1191 return val
1192
1194 val = (u'%s%s%s' % (
1195 self.left_part,
1196 self._picklist_item2display_string(item = item),
1197 self.right_part
1198 )).lstrip().lstrip(';').strip()
1199 self.suppress_text_update_smarts = True
1200 super(cMultiPhraseWheel, self).SetValue(val)
1201
1202 item_end = val.index(item['field_label']) + len(item['field_label'])
1203 self.SetInsertionPoint(item_end)
1204 return
1205
1207
1208
1209 self._data[item['field_label']] = item
1210
1211
1212 field_labels = [ p.strip() for p in self.__phrase_separators.split(self.GetValue().strip()) ]
1213 new_data = {}
1214
1215
1216 for field_label in field_labels:
1217 try:
1218 new_data[field_label] = self._data[field_label]
1219 except KeyError:
1220
1221
1222 pass
1223
1224 self.data = new_data
1225
1232
1233
1234
1236 """Set phrase separators.
1237
1238 - must be a valid regular expression pattern
1239
1240 input is split into phrases at boundaries defined by
1241 this regex and matching is performed on the phrase
1242 the cursor is in only,
1243
1244 after selection from picklist phrase_separators[0] is
1245 added to the end of the match in the PRW
1246 """
1247 self.__phrase_separators = regex.compile(phrase_separators, flags = regex.LOCALE | regex.UNICODE)
1248
1250 return self.__phrase_separators.pattern
1251
1252 phrase_separators = property(_get_phrase_separators, _set_phrase_separators)
1253
1254
1255
1256
1257 if __name__ == '__main__':
1258
1259 if len(sys.argv) < 2:
1260 sys.exit()
1261
1262 if sys.argv[1] != u'test':
1263 sys.exit()
1264
1265 from Gnumed.pycommon import gmI18N
1266 gmI18N.activate_locale()
1267 gmI18N.install_domain(domain='gnumed')
1268
1269 from Gnumed.pycommon import gmPG2, gmMatchProvider
1270
1271 prw = None
1272
1274 print "got focus:"
1275 print "value:", prw.GetValue()
1276 print "data :", prw.GetData()
1277 return True
1278
1280 print "lost focus:"
1281 print "value:", prw.GetValue()
1282 print "data :", prw.GetData()
1283 return True
1284
1286 print "modified:"
1287 print "value:", prw.GetValue()
1288 print "data :", prw.GetData()
1289 return True
1290
1292 print "selected:"
1293 print "value:", prw.GetValue()
1294 print "data :", prw.GetData()
1295 return True
1296
1297
1299 app = wx.PyWidgetTester(size = (200, 50))
1300
1301 items = [ {'data': 1, 'list_label': "Bloggs", 'field_label': "Bloggs", 'weight': 0},
1302 {'data': 2, 'list_label': "Baker", 'field_label': "Baker", 'weight': 0},
1303 {'data': 3, 'list_label': "Jones", 'field_label': "Jones", 'weight': 0},
1304 {'data': 4, 'list_label': "Judson", 'field_label': "Judson", 'weight': 0},
1305 {'data': 5, 'list_label': "Jacobs", 'field_label': "Jacobs", 'weight': 0},
1306 {'data': 6, 'list_label': "Judson-Jacobs", 'field_label': "Judson-Jacobs", 'weight': 0}
1307 ]
1308
1309 mp = gmMatchProvider.cMatchProvider_FixedList(items)
1310
1311 mp.word_separators = '[ \t=+&:@]+'
1312 global prw
1313 prw = cPhraseWheel(parent = app.frame, id = -1)
1314 prw.matcher = mp
1315 prw.capitalisation_mode = gmTools.CAPS_NAMES
1316 prw.add_callback_on_set_focus(callback=display_values_set_focus)
1317 prw.add_callback_on_modified(callback=display_values_modified)
1318 prw.add_callback_on_lose_focus(callback=display_values_lose_focus)
1319 prw.add_callback_on_selection(callback=display_values_selected)
1320
1321 app.frame.Show(True)
1322 app.MainLoop()
1323
1324 return True
1325
1327 print "Do you want to test the database connected phrase wheel ?"
1328 yes_no = raw_input('y/n: ')
1329 if yes_no != 'y':
1330 return True
1331
1332 gmPG2.get_connection()
1333 query = u"""SELECT code, code || ': ' || _(name), _(name) FROM dem.country WHERE _(name) %(fragment_condition)s"""
1334 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query])
1335 app = wx.PyWidgetTester(size = (400, 50))
1336 global prw
1337
1338 prw = cMultiPhraseWheel(parent = app.frame, id = -1)
1339 prw.matcher = mp
1340
1341 app.frame.Show(True)
1342 app.MainLoop()
1343
1344 return True
1345
1347 gmPG2.get_connection()
1348 query = u"""
1349 select
1350 pk_identity,
1351 firstnames || ' ' || lastnames || ', ' || to_char(dob, 'YYYY-MM-DD'),
1352 firstnames || ' ' || lastnames
1353 from
1354 dem.v_basic_person
1355 where
1356 firstnames || lastnames %(fragment_condition)s
1357 """
1358 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query])
1359 app = wx.PyWidgetTester(size = (500, 50))
1360 global prw
1361 prw = cPhraseWheel(parent = app.frame, id = -1)
1362 prw.matcher = mp
1363 prw.selection_only = True
1364
1365 app.frame.Show(True)
1366 app.MainLoop()
1367
1368 return True
1369
1387
1388
1389
1390
1391 test_prw_patients()
1392
1393
1394