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