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 else:
838
839 self.Navigate()
840
842
843 if self._picklist_dropdown.IsShown():
844 idx_selected = self._picklist.GetFirstSelected()
845 if idx_selected < (len(self._current_match_candidates) - 1):
846 self._select_picklist_row(idx_selected + 1, idx_selected)
847 return
848
849
850
851
852
853 self.__timer.Stop()
854 if self.GetValue().strip() == '':
855 val = '*'
856 else:
857 val = self._extract_fragment_to_match_on()
858 self._update_candidates_in_picklist(val = val)
859 self._show_picklist(input2match = val)
860
862 if self._picklist_dropdown.IsShown():
863 selected = self._picklist.GetFirstSelected()
864 if selected > 0:
865 self._select_picklist_row(selected-1, selected)
866 else:
867
868 pass
869
871 """Under certain circumstances take special action on <TAB>.
872
873 returns:
874 True: <TAB> was handled
875 False: <TAB> was not handled
876
877 -> can be used to decide whether to do further <TAB> handling outside this class
878 """
879
880 if not self._picklist_dropdown.IsShown():
881 return False
882
883
884 if len(self._current_match_candidates) != 1:
885 return False
886
887
888 if not self.selection_only:
889 return False
890
891
892 self._select_picklist_row(new_row_idx = 0)
893 self._on_list_item_selected()
894
895 return True
896
897
898
900 self.__timer = _cPRWTimer()
901 self.__timer.callback = self._on_timer_fired
902
903 self.__timer.Stop()
904
906 """Callback for delayed match retrieval timer.
907
908 if we end up here:
909 - delay has passed without user input
910 - the value in the input field has not changed since the timer started
911 """
912
913 val = self._extract_fragment_to_match_on()
914 self._update_candidates_in_picklist(val = val)
915
916
917
918
919
920
921 wx.CallAfter(self._show_picklist, input2match = val)
922
923
924
926 if self.__use_fake_popup:
927 _log.debug(msg)
928
929
931
932 if self.accepted_chars is None:
933 return True
934 return (self.__accepted_chars.match(char) is not None)
935
936
942
944 if self.__accepted_chars is None:
945 return None
946 return self.__accepted_chars.pattern
947
948 accepted_chars = property(_get_accepted_chars, _set_accepted_chars)
949
950
952 self.__final_regex = regex.compile(final_regex, flags = regex.UNICODE)
953
955 return self.__final_regex.pattern
956
957 final_regex = property(_get_final_regex, _set_final_regex)
958
959
961 self.__final_regex_error_msg = msg
962
964 return self.__final_regex_error_msg
965
966 final_regex_error_msg = property(_get_final_regex_error_msg, _set_final_regex_error_msg)
967
968
969
970
973
976
978 raise NotImplementedError('[%s]: _dictify_data()' % self.__class__.__name__)
979
981 raise NotImplementedError('[%s]: cannot adjust data after text update' % self.__class__.__name__)
982
987
989 raise NotImplementedError('[%s]: cannot create data object' % self.__class__.__name__)
990
993
995 self._data = data
996 self.__recalculate_tooltip()
997
998 data = property(_get_data, _set_data)
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
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
1069
1070 - def GetData(self, can_create=False, as_instance=False):
1071
1072 super(cPhraseWheel, self).GetData(can_create = can_create)
1073
1074 if len(self._data) > 0:
1075 if as_instance:
1076 return self._data2instance()
1077
1078 if len(self._data) == 0:
1079 return None
1080
1081 return list(self._data.values())[0]['data']
1082
1083
1085 """Set the data and thereby set the value, too. if possible.
1086
1087 If you call SetData() you better be prepared
1088 doing a scan of the entire potential match space.
1089
1090 The whole thing will only work if data is found
1091 in the match space anyways.
1092 """
1093 if data is None:
1094 self._data = {}
1095 return True
1096
1097
1098 self._update_candidates_in_picklist('*')
1099
1100
1101 if self.selection_only:
1102
1103 if len(self._current_match_candidates) == 0:
1104 return False
1105
1106
1107 for candidate in self._current_match_candidates:
1108 if candidate['data'] == data:
1109 super(cPhraseWheel, self).SetText (
1110 value = candidate['field_label'],
1111 data = data,
1112 suppress_smarts = True
1113 )
1114 return True
1115
1116
1117 if self.selection_only:
1118 self.display_as_valid(valid = False)
1119 return False
1120
1121 self.data = self._dictify_data(data = data)
1122 self.display_as_valid(valid = True)
1123 return True
1124
1125
1126
1127
1129
1130
1131
1132
1133 if len(self._data) > 0:
1134 self._picklist_dropdown.Hide()
1135 return
1136
1137 return super(cPhraseWheel, self)._show_picklist(input2match = input2match)
1138
1139
1141
1142 if len(self._data) > 0:
1143 return True
1144
1145
1146 val = self.GetValue().strip()
1147 if val == '':
1148 return True
1149
1150
1151 self._update_candidates_in_picklist(val = val)
1152 for candidate in self._current_match_candidates:
1153 if candidate['field_label'] == val:
1154 self._update_data_from_picked_item(candidate)
1155 self.MarkDirty()
1156
1157 for callback in self._on_selection_callbacks:
1158 callback(self._data)
1159 return True
1160
1161
1162 if self.selection_only:
1163 gmDispatcher.send(signal = 'statustext', msg = self.selection_only_error_msg)
1164 is_valid = False
1165 return False
1166
1167 return True
1168
1169
1172
1173
1176
1177
1183
1184
1186
1195
1196 - def GetData(self, can_create=False, as_instance=False):
1197
1198 super(cMultiPhraseWheel, self).GetData(can_create = can_create)
1199
1200 if len(self._data) > 0:
1201 if as_instance:
1202 return self._data2instance()
1203
1204 return list(self._data.values())
1205
1207 self.speller = None
1208 return True
1209
1211
1212 data_dict = {}
1213
1214 for item in data_items:
1215 try:
1216 list_label = item['list_label']
1217 except KeyError:
1218 list_label = item['label']
1219 try:
1220 field_label = item['field_label']
1221 except KeyError:
1222 field_label = list_label
1223 data_dict[field_label] = {'data': item['data'], 'list_label': list_label, 'field_label': field_label}
1224
1225 return data_dict
1226
1227
1228
1231
1233
1234 new_data = {}
1235
1236
1237 for displayed_label in self.displayed_strings:
1238 try:
1239 new_data[displayed_label] = self._data[displayed_label]
1240 except KeyError:
1241
1242
1243 pass
1244
1245 self.data = new_data
1246
1248
1249 cursor_pos = self.GetInsertionPoint()
1250
1251 entire_input = self.GetValue()
1252 if self.__phrase_separators.search(entire_input) is None:
1253 self.left_part = ''
1254 self.right_part = ''
1255 return self.GetValue().strip()
1256
1257 string_left_of_cursor = entire_input[:cursor_pos]
1258 string_right_of_cursor = entire_input[cursor_pos:]
1259
1260 left_parts = [ lp.strip() for lp in self.__phrase_separators.split(string_left_of_cursor) ]
1261 if len(left_parts) == 0:
1262 self.left_part = ''
1263 else:
1264 self.left_part = '%s%s ' % (
1265 ('%s ' % self.__phrase_separators.pattern[0]).join(left_parts[:-1]),
1266 self.__phrase_separators.pattern[0]
1267 )
1268
1269 right_parts = [ rp.strip() for rp in self.__phrase_separators.split(string_right_of_cursor) ]
1270 self.right_part = '%s %s' % (
1271 self.__phrase_separators.pattern[0],
1272 ('%s ' % self.__phrase_separators.pattern[0]).join(right_parts[1:])
1273 )
1274
1275 val = (left_parts[-1] + right_parts[0]).strip()
1276 return val
1277
1279 val = ('%s%s%s' % (
1280 self.left_part,
1281 self._picklist_item2display_string(item = item),
1282 self.right_part
1283 )).lstrip().lstrip(';').strip()
1284 self.suppress_text_update_smarts = True
1285 super(cMultiPhraseWheel, self).SetValue(val)
1286
1287 item_end = val.index(item['field_label']) + len(item['field_label'])
1288 self.SetInsertionPoint(item_end)
1289 return
1290
1292
1293
1294 self._data[item['field_label']] = item
1295
1296
1297 field_labels = [ p.strip() for p in self.__phrase_separators.split(self.GetValue().strip()) ]
1298 new_data = {}
1299
1300
1301 for field_label in field_labels:
1302 try:
1303 new_data[field_label] = self._data[field_label]
1304 except KeyError:
1305
1306
1307 pass
1308
1309 self.data = new_data
1310
1317
1318
1319
1321 """Set phrase separators.
1322
1323 - must be a valid regular expression pattern
1324
1325 input is split into phrases at boundaries defined by
1326 this regex and matching is performed on the phrase
1327 the cursor is in only,
1328
1329 after selection from picklist phrase_separators[0] is
1330 added to the end of the match in the PRW
1331 """
1332 self.__phrase_separators = regex.compile(phrase_separators, flags = regex.UNICODE)
1333
1335 return self.__phrase_separators.pattern
1336
1337 phrase_separators = property(_get_phrase_separators, _set_phrase_separators)
1338
1340 return [ p.strip() for p in self.__phrase_separators.split(self.GetValue().strip()) if p.strip() != '' ]
1341
1342 displayed_strings = property(_get_displayed_strings, lambda x:x)
1343
1344
1345
1346 if __name__ == '__main__':
1347
1348 if len(sys.argv) < 2:
1349 sys.exit()
1350
1351 if sys.argv[1] != 'test':
1352 sys.exit()
1353
1354 from Gnumed.pycommon import gmI18N
1355 gmI18N.activate_locale()
1356 gmI18N.install_domain(domain='gnumed')
1357
1358 from Gnumed.pycommon import gmPG2, gmMatchProvider
1359
1360 prw = None
1361
1363 print("got focus:")
1364 print("value:", prw.GetValue())
1365 print("data :", prw.GetData())
1366 return True
1367
1369 print("lost focus:")
1370 print("value:", prw.GetValue())
1371 print("data :", prw.GetData())
1372 return True
1373
1375 print("modified:")
1376 print("value:", prw.GetValue())
1377 print("data :", prw.GetData())
1378 return True
1379
1381 print("selected:")
1382 print("value:", prw.GetValue())
1383 print("data :", prw.GetData())
1384 return True
1385
1386
1388 app = wx.PyWidgetTester(size = (200, 50))
1389
1390 items = [ {'data': 1, 'list_label': "Bloggs", 'field_label': "Bloggs", 'weight': 0},
1391 {'data': 2, 'list_label': "Baker", 'field_label': "Baker", 'weight': 0},
1392 {'data': 3, 'list_label': "Jones", 'field_label': "Jones", 'weight': 0},
1393 {'data': 4, 'list_label': "Judson", 'field_label': "Judson", 'weight': 0},
1394 {'data': 5, 'list_label': "Jacobs", 'field_label': "Jacobs", 'weight': 0},
1395 {'data': 6, 'list_label': "Judson-Jacobs", 'field_label': "Judson-Jacobs", 'weight': 0}
1396 ]
1397
1398 mp = gmMatchProvider.cMatchProvider_FixedList(items)
1399
1400 mp.word_separators = '[ \t=+&:@]+'
1401 global prw
1402 prw = cPhraseWheel(app.frame, -1)
1403 prw.matcher = mp
1404 prw.capitalisation_mode = gmTools.CAPS_NAMES
1405 prw.add_callback_on_set_focus(callback=display_values_set_focus)
1406 prw.add_callback_on_modified(callback=display_values_modified)
1407 prw.add_callback_on_lose_focus(callback=display_values_lose_focus)
1408 prw.add_callback_on_selection(callback=display_values_selected)
1409
1410 app.frame.Show(True)
1411 app.MainLoop()
1412
1413 return True
1414
1416 print("Do you want to test the database connected phrase wheel ?")
1417 yes_no = input('y/n: ')
1418 if yes_no != 'y':
1419 return True
1420
1421 gmPG2.get_connection()
1422 query = """SELECT code, code || ': ' || _(name), _(name) FROM dem.country WHERE _(name) %(fragment_condition)s"""
1423 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query])
1424 app = wx.PyWidgetTester(size = (400, 50))
1425 global prw
1426
1427 prw = cMultiPhraseWheel(app.frame, -1)
1428 prw.matcher = mp
1429
1430 app.frame.Show(True)
1431 app.MainLoop()
1432
1433 return True
1434
1436 gmPG2.get_connection()
1437 query = """
1438 select
1439 pk_identity,
1440 firstnames || ' ' || lastnames || ', ' || to_char(dob, 'YYYY-MM-DD'),
1441 firstnames || ' ' || lastnames
1442 from
1443 dem.v_active_persons
1444 where
1445 firstnames || lastnames %(fragment_condition)s
1446 """
1447 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query])
1448 app = wx.PyWidgetTester(size = (500, 50))
1449 global prw
1450 prw = cPhraseWheel(app.frame, -1)
1451 prw.matcher = mp
1452 prw.selection_only = True
1453
1454 app.frame.Show(True)
1455 app.MainLoop()
1456
1457 return True
1458
1476
1477
1478
1479
1480 test_prw_patients()
1481
1482
1483