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
11
12 __version__ = "$Revision: 1.135 $"
13 __author__ = "K.Hilbert <Karsten.Hilbert@gmx.net>, I.Haywood, S.J.Tan <sjtan@bigpond.com>"
14 __license__ = "GPL"
15
16
17 import string, types, time, sys, re as regex, os.path
18
19
20
21 import wx
22 import wx.lib.mixins.listctrl as listmixins
23 import wx.lib.pubsub
24
25
26
27 if __name__ == '__main__':
28 sys.path.insert(0, '../../')
29 from Gnumed.pycommon import gmTools
30
31
32 import logging
33 _log = logging.getLogger('macosx')
34
35
36 color_prw_invalid = 'pink'
37 color_prw_valid = None
38
39 default_phrase_separators = '[;/|]+'
40 default_spelling_word_separators = '[\W\d_]+'
41
42
43 NUMERIC = '0-9'
44 ALPHANUMERIC = 'a-zA-Z0-9'
45 EMAIL_CHARS = "a-zA-Z0-9\-_@\."
46 WEB_CHARS = "a-zA-Z0-9\.\-_/:"
47
48
49 _timers = []
50
52 """It can be useful to call this early from your shutdown code to avoid hangs on Notify()."""
53 global _timers
54 _log.info('shutting down %s pending timers', len(_timers))
55 for timer in _timers:
56 _log.debug('timer [%s]', timer)
57 timer.Stop()
58 _timers = []
59
61
63 wx.Timer.__init__(self, *args, **kwargs)
64 self.callback = lambda x:x
65 global _timers
66 _timers.append(self)
67
70
71
74 try:
75 kwargs['style'] = kwargs['style'] | wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.SIMPLE_BORDER
76 except: pass
77 wx.ListCtrl.__init__(self, *args, **kwargs)
78 listmixins.ListCtrlAutoWidthMixin.__init__(self)
79
81 self.DeleteAllItems()
82 self.__data = items
83 pos = len(items) + 1
84 for item in items:
85 row_num = self.InsertStringItem(pos, label=item['label'])
86
88 sel_idx = self.GetFirstSelected()
89 if sel_idx == -1:
90 return None
91 return self.__data[sel_idx]['data']
92
94 sel_idx = self.GetFirstSelected()
95 if sel_idx == -1:
96 return None
97 return self.__data[sel_idx]['label']
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
163 """Widget for smart guessing of user fields, after Richard Terry's interface.
164
165 - VB implementation by Richard Terry
166 - Python port by Ian Haywood for GNUmed
167 - enhanced by Karsten Hilbert for GNUmed
168 - enhanced by Ian Haywood for aumed
169 - enhanced by Karsten Hilbert for GNUmed
170
171 @param matcher: a class used to find matches for the current input
172 @type matcher: a L{match provider<Gnumed.pycommon.gmMatchProvider.cMatchProvider>}
173 instance or C{None}
174
175 @param selection_only: whether free-text can be entered without associated data
176 @type selection_only: boolean
177
178 @param capitalisation_mode: how to auto-capitalize input, valid values
179 are found in L{capitalize()<Gnumed.pycommon.gmTools.capitalize>}
180 @type capitalisation_mode: integer
181
182 @param accepted_chars: a regex pattern defining the characters
183 acceptable in the input string, if None no checking is performed
184 @type accepted_chars: None or a string holding a valid regex pattern
185
186 @param final_regex: when the control loses focus the input is
187 checked against this regular expression
188 @type final_regex: a string holding a valid regex pattern
189
190 @param phrase_separators: if not None, input is split into phrases
191 at boundaries defined by this regex and matching/spellchecking
192 is performed on the phrase the cursor is in only
193 @type phrase_separators: None or a string holding a valid regex pattern
194
195 @param navigate_after_selection: whether or not to immediately
196 navigate to the widget next-in-tab-order after selecting an
197 item from the dropdown picklist
198 @type navigate_after_selection: boolean
199
200 @param speller: if not None used to spellcheck the current input
201 and to retrieve suggested replacements/completions
202 @type speller: None or a L{enchant Dict<enchant>} descendant
203
204 @param picklist_delay: this much time of user inactivity must have
205 passed before the input related smarts kick in and the drop
206 down pick list is shown
207 @type picklist_delay: integer (milliseconds)
208 """
209 - def __init__ (self, parent=None, id=-1, value='', *args, **kwargs):
210
211
212 self.matcher = None
213 self.selection_only = False
214 self.selection_only_error_msg = _('You must select a value from the picklist or type an exact match.')
215 self.capitalisation_mode = gmTools.CAPS_NONE
216 self.accepted_chars = None
217 self.final_regex = '.*'
218 self.final_regex_error_msg = _('The content is invalid. It must match the regular expression: [%%s]. <%s>') % self.__class__.__name__
219 self.phrase_separators = default_phrase_separators
220 self.navigate_after_selection = False
221 self.speller = None
222 self.speller_word_separators = default_spelling_word_separators
223 self.picklist_delay = 150
224
225
226 self._has_focus = False
227 self.suppress_text_update_smarts = False
228 self.__current_matches = []
229 self._screenheight = wx.SystemSettings.GetMetric(wx.SYS_SCREEN_Y)
230 self.input2match = ''
231 self.left_part = ''
232 self.right_part = ''
233 self.data = None
234
235 self._on_selection_callbacks = []
236 self._on_lose_focus_callbacks = []
237 self._on_set_focus_callbacks = []
238 self._on_modified_callbacks = []
239
240 try:
241 kwargs['style'] = kwargs['style'] | wx.TE_PROCESS_TAB
242 except KeyError:
243 kwargs['style'] = wx.TE_PROCESS_TAB
244 wx.TextCtrl.__init__(self, parent, id, **kwargs)
245
246 self.__non_edit_font = self.GetFont()
247 self.__color_valid = self.GetBackgroundColour()
248 global color_prw_valid
249 if color_prw_valid is None:
250 color_prw_valid = wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW)
251
252 self.__init_dropdown(parent = parent)
253 self.__register_events()
254 self.__init_timer()
255
256
257
259 """
260 Add a callback for invocation when a picklist item is selected.
261
262 The callback will be invoked whenever an item is selected
263 from the picklist. The associated data is passed in as
264 a single parameter. Callbacks must be able to cope with
265 None as the data parameter as that is sent whenever the
266 user changes a previously selected value.
267 """
268 if not callable(callback):
269 raise ValueError('[add_callback_on_selection]: ignoring callback [%s], it is not callable' % callback)
270
271 self._on_selection_callbacks.append(callback)
272
274 """
275 Add a callback for invocation when getting focus.
276 """
277 if not callable(callback):
278 raise ValueError('[add_callback_on_set_focus]: ignoring callback [%s] - not callable' % callback)
279
280 self._on_set_focus_callbacks.append(callback)
281
283 """
284 Add a callback for invocation when losing focus.
285 """
286 if not callable(callback):
287 raise ValueError('[add_callback_on_lose_focus]: ignoring callback [%s] - not callable' % callback)
288
289 self._on_lose_focus_callbacks.append(callback)
290
292 """
293 Add a callback for invocation when the content is modified.
294 """
295 if not callable(callback):
296 raise ValueError('[add_callback_on_modified]: ignoring callback [%s] - not callable' % callback)
297
298 self._on_modified_callbacks.append(callback)
299
301 """
302 Set the data and thereby set the value, too.
303
304 If you call SetData() you better be prepared
305 doing a scan of the entire potential match space.
306
307 The whole thing will only work if data is found
308 in the match space anyways.
309 """
310 if self.matcher is None:
311 matched, matches = (False, [])
312 else:
313 matched, matches = self.matcher.getMatches('*')
314
315 if self.selection_only:
316 if not matched or (len(matches) == 0):
317 return False
318
319 for match in matches:
320 if match['data'] == data:
321 self.display_as_valid(valid = True)
322 self.suppress_text_update_smarts = True
323 wx.TextCtrl.SetValue(self, match['label'])
324 self.data = data
325 return True
326
327
328 if self.selection_only:
329 return False
330
331 self.data = data
332 self.display_as_valid(valid = True)
333 return True
334
336 """Retrieve the data associated with the displayed string.
337 """
338 return self.data
339
340 - def SetText(self, value=u'', data=None, suppress_smarts=False):
341
342 self.suppress_text_update_smarts = suppress_smarts
343
344 if data is not None:
345 self.suppress_text_update_smarts = True
346 self.data = data
347 wx.TextCtrl.SetValue(self, value)
348 self.display_as_valid(valid = True)
349
350
351 if self.data is not None:
352 return True
353
354 if value == u'' and not self.selection_only:
355 return True
356
357
358 if self.matcher is None:
359 stat, matches = (False, [])
360 else:
361 stat, matches = self.matcher.getMatches(aFragment = value)
362
363 for match in matches:
364 if match['label'] == value:
365 self.data = match['data']
366 return True
367
368
369 if self.selection_only:
370 self.display_as_valid(valid = False)
371 return False
372
373 return True
374
375 - def set_context(self, context=None, val=None):
376 if self.matcher is not None:
377 self.matcher.set_context(context=context, val=val)
378
379 - def unset_context(self, context=None):
380 if self.matcher is not None:
381 self.matcher.unset_context(context=context)
382
384
385 try:
386 import enchant
387 except ImportError:
388 self.speller = None
389 return False
390 try:
391 self.speller = enchant.DictWithPWL(None, os.path.expanduser(os.path.join('~', '.gnumed', 'spellcheck', 'wordlist.pwl')))
392 except enchant.DictNotFoundError:
393 self.speller = None
394 return False
395 return True
396
398 if valid is True:
399 self.SetBackgroundColour(self.__color_valid)
400 elif valid is False:
401 self.SetBackgroundColour(color_prw_invalid)
402 else:
403 raise ArgumentError(u'<valid> must be True or False')
404 self.Refresh()
405
406
407
408
409
411 szr_dropdown = None
412 try:
413
414 self.__dropdown_needs_relative_position = False
415 self.__picklist_dropdown = wx.PopupWindow(parent)
416 list_parent = self.__picklist_dropdown
417 self.__use_fake_popup = False
418 except NotImplementedError:
419 self.__use_fake_popup = True
420
421
422 add_picklist_to_sizer = True
423 szr_dropdown = wx.BoxSizer(wx.VERTICAL)
424
425
426 self.__dropdown_needs_relative_position = False
427 self.__picklist_dropdown = wx.MiniFrame (
428 parent = parent,
429 id = -1,
430 style = wx.SIMPLE_BORDER | wx.FRAME_FLOAT_ON_PARENT | wx.FRAME_NO_TASKBAR | wx.POPUP_WINDOW
431 )
432 scroll_win = wx.ScrolledWindow(parent = self.__picklist_dropdown, style = wx.NO_BORDER)
433 scroll_win.SetSizer(szr_dropdown)
434 list_parent = scroll_win
435
436
437
438
439
440
441
442 self.mac_log('dropdown parent: %s' % self.__picklist_dropdown.GetParent())
443
444
445
446
447
448
449 self._picklist = cPhraseWheelListCtrl (
450 list_parent,
451 style = wx.LC_NO_HEADER
452 )
453 self._picklist.InsertColumn(0, '')
454
455 if szr_dropdown is not None:
456 szr_dropdown.Add(self._picklist, 1, wx.EXPAND)
457
458 self.__picklist_dropdown.Hide()
459
461 """Display the pick list."""
462
463 border_width = 4
464 extra_height = 25
465
466 self.__picklist_dropdown.Hide()
467
468
469
470 if self.data is not None:
471 return
472
473 if not self._has_focus:
474 return
475
476 if len(self.__current_matches) == 0:
477 return
478
479
480 if len(self.__current_matches) == 1:
481 if self.__current_matches[0]['label'] == self.input2match:
482 self.data = self.__current_matches[0]['data']
483 return
484
485
486 rows = len(self.__current_matches)
487 if rows < 2:
488 rows = 2
489 if rows > 20:
490 rows = 20
491 self.mac_log('dropdown needs rows: %s' % rows)
492 dropdown_size = self.__picklist_dropdown.GetSize()
493 pw_size = self.GetSize()
494 dropdown_size.SetWidth(pw_size.width)
495 dropdown_size.SetHeight (
496 (pw_size.height * rows)
497 + border_width
498 + extra_height
499 )
500
501
502 (pw_x_abs, pw_y_abs) = self.ClientToScreenXY(0,0)
503 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)))
504 dropdown_new_x = pw_x_abs
505 dropdown_new_y = pw_y_abs + pw_size.height
506 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)))
507 self.mac_log('desired dropdown size: %s' % dropdown_size)
508
509
510 if (dropdown_new_y + dropdown_size.height) > self._screenheight:
511 self.mac_log('dropdown extends offscreen (screen max y: %s)' % self._screenheight)
512 max_height = self._screenheight - dropdown_new_y - 4
513 self.mac_log('max dropdown height would be: %s' % max_height)
514 if max_height > ((pw_size.height * 2) + 4):
515 dropdown_size.SetHeight(max_height)
516 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)))
517 self.mac_log('possible dropdown size: %s' % dropdown_size)
518
519
520 self.__picklist_dropdown.SetSize(dropdown_size)
521 self._picklist.SetSize(self.__picklist_dropdown.GetClientSize())
522 self.mac_log('pick list size set to: %s' % self.__picklist_dropdown.GetSize())
523 if self.__dropdown_needs_relative_position:
524 dropdown_new_x, dropdown_new_y = self.__picklist_dropdown.GetParent().ScreenToClientXY(dropdown_new_x, dropdown_new_y)
525 self.__picklist_dropdown.MoveXY(dropdown_new_x, dropdown_new_y)
526
527
528 self._picklist.Select(0)
529
530
531 self.__picklist_dropdown.Show(True)
532
533 dd_tl = self.__picklist_dropdown.ClientToScreenXY(0,0)
534 dd_size = self.__picklist_dropdown.GetSize()
535 dd_br = self.__picklist_dropdown.ClientToScreenXY(dd_size.width, dd_size.height)
536 self.mac_log('dropdown placement now (on screen): x:%s-%s, y:%s-%s' % (dd_tl[0], dd_br[0], dd_tl[1], dd_br[1]))
537
539 """Hide the pick list."""
540 self.__picklist_dropdown.Hide()
541
543 if old_row_idx is not None:
544 pass
545 self._picklist.Select(new_row_idx)
546 self._picklist.EnsureVisible(new_row_idx)
547
549 """Get the matches for the currently typed input fragment."""
550
551 self.input2match = val
552 if self.input2match is None:
553 if self.__phrase_separators is None:
554 self.input2match = self.GetValue().strip()
555 else:
556
557 entire_input = self.GetValue()
558 cursor_pos = self.GetInsertionPoint()
559 left_of_cursor = entire_input[:cursor_pos]
560 right_of_cursor = entire_input[cursor_pos:]
561 left_boundary = self.__phrase_separators.search(left_of_cursor)
562 if left_boundary is not None:
563 phrase_start = left_boundary.end()
564 else:
565 phrase_start = 0
566 self.left_part = entire_input[:phrase_start]
567
568 right_boundary = self.__phrase_separators.search(right_of_cursor)
569 if right_boundary is not None:
570 phrase_end = cursor_pos + (right_boundary.start() - 1)
571 else:
572 phrase_end = len(entire_input) - 1
573 self.right_part = entire_input[phrase_end+1:]
574 self.input2match = entire_input[phrase_start:phrase_end+1]
575
576
577 if self.matcher is not None:
578 matched, self.__current_matches = self.matcher.getMatches(self.input2match)
579 self._picklist.SetItems(self.__current_matches)
580
581
582 if len(self.__current_matches) == 0:
583 if self.speller is not None:
584
585 word = regex.split(self.__speller_word_separators, self.input2match)[-1]
586 if word.strip() != u'':
587 success = False
588 try:
589 success = self.speller.check(word)
590 except:
591 _log.exception('had to disable enchant spell checker')
592 self.speller = None
593 if success:
594 spells = self.speller.suggest(word)
595 truncated_input2match = self.input2match[:self.input2match.rindex(word)]
596 for spell in spells:
597 self.__current_matches.append({'label': truncated_input2match + spell, 'data': None})
598 self._picklist.SetItems(self.__current_matches)
599
601 return self._picklist.GetItemText(self._picklist.GetFirstSelected())
602
603
604
606 """Called when the user pressed <ENTER>."""
607 if self.__picklist_dropdown.IsShown():
608 self._on_list_item_selected()
609 else:
610
611 self.Navigate()
612
614
615 if self.__picklist_dropdown.IsShown():
616 selected = self._picklist.GetFirstSelected()
617 if selected < (len(self.__current_matches) - 1):
618 self.__select_picklist_row(selected+1, selected)
619
620
621
622
623
624 else:
625 self.__timer.Stop()
626 if self.GetValue().strip() == u'':
627 self.__update_matches_in_picklist(val='*')
628 else:
629 self.__update_matches_in_picklist()
630 self._show_picklist()
631
633 if self.__picklist_dropdown.IsShown():
634 selected = self._picklist.GetFirstSelected()
635 if selected > 0:
636 self.__select_picklist_row(selected-1, selected)
637 else:
638
639 pass
640
642 """Under certain circumstances takes special action on TAB.
643
644 returns:
645 True: TAB was handled
646 False: TAB was not handled
647 """
648 if not self.__picklist_dropdown.IsShown():
649 return False
650
651 if len(self.__current_matches) != 1:
652 return False
653
654 if not self.selection_only:
655 return False
656
657 self.__select_picklist_row(new_row_idx=0)
658 self._on_list_item_selected()
659
660 return True
661
662
663
665
666 if self.accepted_chars is None:
667 return True
668 return (self.__accepted_chars.match(char) is not None)
669
675
677 if self.__accepted_chars is None:
678 return None
679 return self.__accepted_chars.pattern
680
681 accepted_chars = property(_get_accepted_chars, _set_accepted_chars)
682
684 self.__final_regex = regex.compile(final_regex, flags = regex.LOCALE | regex.UNICODE)
685
687 return self.__final_regex.pattern
688
689 final_regex = property(_get_final_regex, _set_final_regex)
690
692 self.__final_regex_error_msg = msg % self.final_regex
693
695 return self.__final_regex_error_msg
696
697 final_regex_error_msg = property(_get_final_regex_error_msg, _set_final_regex_error_msg)
698
700 if phrase_separators is None:
701 self.__phrase_separators = None
702 else:
703 self.__phrase_separators = regex.compile(phrase_separators, flags = regex.LOCALE | regex.UNICODE)
704
706 if self.__phrase_separators is None:
707 return None
708 return self.__phrase_separators.pattern
709
710 phrase_separators = property(_get_phrase_separators, _set_phrase_separators)
711
713 if word_separators is None:
714 self.__speller_word_separators = regex.compile('[\W\d_]+', flags = regex.LOCALE | regex.UNICODE)
715 else:
716 self.__speller_word_separators = regex.compile(word_separators, flags = regex.LOCALE | regex.UNICODE)
717
719 return self.__speller_word_separators.pattern
720
721 speller_word_separators = property(_get_speller_word_separators, _set_speller_word_separators)
722
724 self.__timer = _cPRWTimer()
725 self.__timer.callback = self._on_timer_fired
726
727 self.__timer.Stop()
728
730 """Callback for delayed match retrieval timer.
731
732 if we end up here:
733 - delay has passed without user input
734 - the value in the input field has not changed since the timer started
735 """
736
737 self.__update_matches_in_picklist()
738
739
740
741
742
743
744
745 wx.CallAfter(self._show_picklist)
746
747
748
750 wx.EVT_TEXT(self, self.GetId(), self._on_text_update)
751 wx.EVT_KEY_DOWN (self, self._on_key_down)
752 wx.EVT_SET_FOCUS(self, self._on_set_focus)
753 wx.EVT_KILL_FOCUS(self, self._on_lose_focus)
754 self._picklist.Bind(wx.EVT_LEFT_DCLICK, self._on_list_item_selected)
755
757 """Gets called when user selected a list item."""
758
759 self._hide_picklist()
760 self.display_as_valid(valid = True)
761
762 data = self._picklist.GetSelectedItemData()
763 if data is None:
764 return
765
766 self.data = data
767
768
769 self.suppress_text_update_smarts = True
770 if self.__phrase_separators is not None:
771 wx.TextCtrl.SetValue(self, u'%s%s%s' % (self.left_part, self._picklist_selection2display_string(), self.right_part))
772 else:
773 wx.TextCtrl.SetValue(self, self._picklist_selection2display_string())
774
775 self.data = self._picklist.GetSelectedItemData()
776 self.MarkDirty()
777
778
779 for callback in self._on_selection_callbacks:
780 callback(self.data)
781
782 if self.navigate_after_selection:
783 self.Navigate()
784 else:
785 self.SetInsertionPoint(self.GetLastPosition())
786
787 return
788
790 """Is called when a key is pressed."""
791
792 keycode = event.GetKeyCode()
793
794 if keycode == wx.WXK_DOWN:
795 self.__on_cursor_down()
796 return
797
798 if keycode == wx.WXK_UP:
799 self.__on_cursor_up()
800 return
801
802 if keycode == wx.WXK_RETURN:
803 self._on_enter()
804 return
805
806 if keycode == wx.WXK_TAB:
807 if event.ShiftDown():
808 self.Navigate(flags = wx.NavigationKeyEvent.IsBackward)
809 return
810 self.__on_tab()
811 self.Navigate(flags = wx.NavigationKeyEvent.IsForward)
812 return
813
814
815 if keycode in [wx.WXK_SHIFT, wx.WXK_BACK, wx.WXK_DELETE, wx.WXK_LEFT, wx.WXK_RIGHT]:
816 pass
817
818
819 elif not self.__char_is_allowed(char = unichr(event.GetUnicodeKey())):
820
821 wx.Bell()
822
823 return
824
825 event.Skip()
826 return
827
828 - def _on_text_update (self, event):
829 """Internal handler for wx.EVT_TEXT.
830
831 Called when text was changed by user or SetValue().
832 """
833 if self.suppress_text_update_smarts:
834 self.suppress_text_update_smarts = False
835 return
836
837 self.data = None
838 self.__current_matches = []
839
840
841
842 val = self.GetValue().strip()
843 ins_point = self.GetInsertionPoint()
844 if val == u'':
845 self._hide_picklist()
846 self.__timer.Stop()
847 else:
848 new_val = gmTools.capitalize(text = val, mode = self.capitalisation_mode)
849 if new_val != val:
850 self.suppress_text_update_smarts = True
851 wx.TextCtrl.SetValue(self, new_val)
852 if ins_point > len(new_val):
853 self.SetInsertionPointEnd()
854 else:
855 self.SetInsertionPoint(ins_point)
856
857
858
859 self.__timer.Start(oneShot = True, milliseconds = self.picklist_delay)
860
861
862 for callback in self._on_modified_callbacks:
863 callback()
864
865 return
866
868
869 self._has_focus = True
870 event.Skip()
871
872 self.__non_edit_font = self.GetFont()
873 edit_font = self.GetFont()
874 edit_font.SetPointSize(pointSize = self.__non_edit_font.GetPointSize() + 1)
875 self.SetFont(edit_font)
876 self.Refresh()
877
878
879 for callback in self._on_set_focus_callbacks:
880 callback()
881
882 self.__timer.Start(oneShot = True, milliseconds = self.picklist_delay)
883 return True
884
886 """Do stuff when leaving the control.
887
888 The user has had her say, so don't second guess
889 intentions but do report error conditions.
890 """
891 self._has_focus = False
892
893
894 self.__timer.Stop()
895 self._hide_picklist()
896
897
898 self.SetSelection(1,1)
899
900 self.SetFont(self.__non_edit_font)
901 self.Refresh()
902
903 is_valid = True
904
905
906
907
908 if self.data is None:
909 val = self.GetValue().strip()
910 if val != u'':
911 self.__update_matches_in_picklist()
912 for match in self.__current_matches:
913 if match['label'] == val:
914 self.data = match['data']
915 self.MarkDirty()
916 break
917
918
919 if self.data is None:
920 if self.selection_only:
921 wx.lib.pubsub.Publisher().sendMessage (
922 topic = 'statustext',
923 data = {'msg': self.selection_only_error_msg}
924 )
925 is_valid = False
926
927
928 if self.__final_regex.match(self.GetValue().strip()) is None:
929 wx.lib.pubsub.Publisher().sendMessage (
930 topic = 'statustext',
931 data = {'msg': self.final_regex_error_msg}
932 )
933 is_valid = False
934
935 self.display_as_valid(valid = is_valid)
936
937
938 for callback in self._on_lose_focus_callbacks:
939 callback()
940
941 event.Skip()
942 return True
943
945 if self.__use_fake_popup:
946 _log.debug(msg)
947
948
949
950 if __name__ == '__main__':
951 from Gnumed.pycommon import gmI18N
952 gmI18N.activate_locale()
953 gmI18N.install_domain(domain='gnumed')
954
955 from Gnumed.pycommon import gmPG2, gmMatchProvider
956
957 prw = None
958
960 print "got focus:"
961 print "value:", prw.GetValue()
962 print "data :", prw.GetData()
963 return True
964
966 print "lost focus:"
967 print "value:", prw.GetValue()
968 print "data :", prw.GetData()
969 return True
970
972 print "modified:"
973 print "value:", prw.GetValue()
974 print "data :", prw.GetData()
975 return True
976
978 print "selected:"
979 print "value:", prw.GetValue()
980 print "data :", prw.GetData()
981 return True
982
984 app = wx.PyWidgetTester(size = (200, 50))
985
986 items = [ {'data':1, 'label':"Bloggs"},
987 {'data':2, 'label':"Baker"},
988 {'data':3, 'label':"Jones"},
989 {'data':4, 'label':"Judson"},
990 {'data':5, 'label':"Jacobs"},
991 {'data':6, 'label':"Judson-Jacobs"}
992 ]
993
994 mp = gmMatchProvider.cMatchProvider_FixedList(items)
995
996 mp.word_separators = '[ \t=+&:@]+'
997 global prw
998 prw = cPhraseWheel(parent = app.frame, id = -1)
999 prw.matcher = mp
1000 prw.capitalisation_mode = gmTools.CAPS_NAMES
1001 prw.add_callback_on_set_focus(callback=display_values_set_focus)
1002 prw.add_callback_on_modified(callback=display_values_modified)
1003 prw.add_callback_on_lose_focus(callback=display_values_lose_focus)
1004 prw.add_callback_on_selection(callback=display_values_selected)
1005
1006 app.frame.Show(True)
1007 app.MainLoop()
1008
1009 return True
1010
1012 print "Do you want to test the database connected phrase wheel ?"
1013 yes_no = raw_input('y/n: ')
1014 if yes_no != 'y':
1015 return True
1016
1017 gmPG2.get_connection()
1018
1019
1020 query = u'select code, name from dem.country where _(name) %(fragment_condition)s'
1021 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query])
1022 app = wx.PyWidgetTester(size = (200, 50))
1023 global prw
1024 prw = cPhraseWheel(parent = app.frame, id = -1)
1025 prw.matcher = mp
1026
1027 app.frame.Show(True)
1028 app.MainLoop()
1029
1030 return True
1031
1033 gmPG2.get_connection()
1034 query = u"select pk_identity, firstnames || ' ' || lastnames || ' ' || dob::text as pat_name from dem.v_basic_person where firstnames || lastnames %(fragment_condition)s"
1035
1036 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query])
1037 app = wx.PyWidgetTester(size = (200, 50))
1038 global prw
1039 prw = cPhraseWheel(parent = app.frame, id = -1)
1040 prw.matcher = mp
1041
1042 app.frame.Show(True)
1043 app.MainLoop()
1044
1045 return True
1046
1064
1065
1066
1067 test_spell_checking_prw()
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561