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.136 $"
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
335 - def GetData(self, can_create=False):
336 """Retrieve the data associated with the displayed string.
337 """
338 if self.data is None:
339 if can_create:
340 self._create_data()
341 return self.data
342
343 - def SetText(self, value=u'', data=None, suppress_smarts=False):
344
345 self.suppress_text_update_smarts = suppress_smarts
346
347 if data is not None:
348 self.suppress_text_update_smarts = True
349 self.data = data
350 wx.TextCtrl.SetValue(self, value)
351 self.display_as_valid(valid = True)
352
353
354 if self.data is not None:
355 return True
356
357 if value == u'' and not self.selection_only:
358 return True
359
360
361 if self.matcher is None:
362 stat, matches = (False, [])
363 else:
364 stat, matches = self.matcher.getMatches(aFragment = value)
365
366 for match in matches:
367 if match['label'] == value:
368 self.data = match['data']
369 return True
370
371
372 if self.selection_only:
373 self.display_as_valid(valid = False)
374 return False
375
376 return True
377
378 - def set_context(self, context=None, val=None):
379 if self.matcher is not None:
380 self.matcher.set_context(context=context, val=val)
381
382 - def unset_context(self, context=None):
383 if self.matcher is not None:
384 self.matcher.unset_context(context=context)
385
387
388 try:
389 import enchant
390 except ImportError:
391 self.speller = None
392 return False
393 try:
394 self.speller = enchant.DictWithPWL(None, os.path.expanduser(os.path.join('~', '.gnumed', 'spellcheck', 'wordlist.pwl')))
395 except enchant.DictNotFoundError:
396 self.speller = None
397 return False
398 return True
399
401 if valid is True:
402 self.SetBackgroundColour(self.__color_valid)
403 elif valid is False:
404 self.SetBackgroundColour(color_prw_invalid)
405 else:
406 raise ArgumentError(u'<valid> must be True or False')
407 self.Refresh()
408
409
410
411
412
414 szr_dropdown = None
415 try:
416
417 self.__dropdown_needs_relative_position = False
418 self.__picklist_dropdown = wx.PopupWindow(parent)
419 list_parent = self.__picklist_dropdown
420 self.__use_fake_popup = False
421 except NotImplementedError:
422 self.__use_fake_popup = True
423
424
425 add_picklist_to_sizer = True
426 szr_dropdown = wx.BoxSizer(wx.VERTICAL)
427
428
429 self.__dropdown_needs_relative_position = False
430 self.__picklist_dropdown = wx.MiniFrame (
431 parent = parent,
432 id = -1,
433 style = wx.SIMPLE_BORDER | wx.FRAME_FLOAT_ON_PARENT | wx.FRAME_NO_TASKBAR | wx.POPUP_WINDOW
434 )
435 scroll_win = wx.ScrolledWindow(parent = self.__picklist_dropdown, style = wx.NO_BORDER)
436 scroll_win.SetSizer(szr_dropdown)
437 list_parent = scroll_win
438
439
440
441
442
443
444
445 self.mac_log('dropdown parent: %s' % self.__picklist_dropdown.GetParent())
446
447
448
449
450
451
452 self._picklist = cPhraseWheelListCtrl (
453 list_parent,
454 style = wx.LC_NO_HEADER
455 )
456 self._picklist.InsertColumn(0, '')
457
458 if szr_dropdown is not None:
459 szr_dropdown.Add(self._picklist, 1, wx.EXPAND)
460
461 self.__picklist_dropdown.Hide()
462
464 """Display the pick list."""
465
466 border_width = 4
467 extra_height = 25
468
469 self.__picklist_dropdown.Hide()
470
471
472
473 if self.data is not None:
474 return
475
476 if not self._has_focus:
477 return
478
479 if len(self.__current_matches) == 0:
480 return
481
482
483 if len(self.__current_matches) == 1:
484 if self.__current_matches[0]['label'] == self.input2match:
485 self.data = self.__current_matches[0]['data']
486 return
487
488
489 rows = len(self.__current_matches)
490 if rows < 2:
491 rows = 2
492 if rows > 20:
493 rows = 20
494 self.mac_log('dropdown needs rows: %s' % rows)
495 dropdown_size = self.__picklist_dropdown.GetSize()
496 pw_size = self.GetSize()
497 dropdown_size.SetWidth(pw_size.width)
498 dropdown_size.SetHeight (
499 (pw_size.height * rows)
500 + border_width
501 + extra_height
502 )
503
504
505 (pw_x_abs, pw_y_abs) = self.ClientToScreenXY(0,0)
506 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)))
507 dropdown_new_x = pw_x_abs
508 dropdown_new_y = pw_y_abs + pw_size.height
509 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)))
510 self.mac_log('desired dropdown size: %s' % dropdown_size)
511
512
513 if (dropdown_new_y + dropdown_size.height) > self._screenheight:
514 self.mac_log('dropdown extends offscreen (screen max y: %s)' % self._screenheight)
515 max_height = self._screenheight - dropdown_new_y - 4
516 self.mac_log('max dropdown height would be: %s' % max_height)
517 if max_height > ((pw_size.height * 2) + 4):
518 dropdown_size.SetHeight(max_height)
519 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)))
520 self.mac_log('possible dropdown size: %s' % dropdown_size)
521
522
523 self.__picklist_dropdown.SetSize(dropdown_size)
524 self._picklist.SetSize(self.__picklist_dropdown.GetClientSize())
525 self.mac_log('pick list size set to: %s' % self.__picklist_dropdown.GetSize())
526 if self.__dropdown_needs_relative_position:
527 dropdown_new_x, dropdown_new_y = self.__picklist_dropdown.GetParent().ScreenToClientXY(dropdown_new_x, dropdown_new_y)
528 self.__picklist_dropdown.MoveXY(dropdown_new_x, dropdown_new_y)
529
530
531 self._picklist.Select(0)
532
533
534 self.__picklist_dropdown.Show(True)
535
536 dd_tl = self.__picklist_dropdown.ClientToScreenXY(0,0)
537 dd_size = self.__picklist_dropdown.GetSize()
538 dd_br = self.__picklist_dropdown.ClientToScreenXY(dd_size.width, dd_size.height)
539 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]))
540
542 """Hide the pick list."""
543 self.__picklist_dropdown.Hide()
544
546 if old_row_idx is not None:
547 pass
548 self._picklist.Select(new_row_idx)
549 self._picklist.EnsureVisible(new_row_idx)
550
552 """Get the matches for the currently typed input fragment."""
553
554 self.input2match = val
555 if self.input2match is None:
556 if self.__phrase_separators is None:
557 self.input2match = self.GetValue().strip()
558 else:
559
560 entire_input = self.GetValue()
561 cursor_pos = self.GetInsertionPoint()
562 left_of_cursor = entire_input[:cursor_pos]
563 right_of_cursor = entire_input[cursor_pos:]
564 left_boundary = self.__phrase_separators.search(left_of_cursor)
565 if left_boundary is not None:
566 phrase_start = left_boundary.end()
567 else:
568 phrase_start = 0
569 self.left_part = entire_input[:phrase_start]
570
571 right_boundary = self.__phrase_separators.search(right_of_cursor)
572 if right_boundary is not None:
573 phrase_end = cursor_pos + (right_boundary.start() - 1)
574 else:
575 phrase_end = len(entire_input) - 1
576 self.right_part = entire_input[phrase_end+1:]
577 self.input2match = entire_input[phrase_start:phrase_end+1]
578
579
580 if self.matcher is not None:
581 matched, self.__current_matches = self.matcher.getMatches(self.input2match)
582 self._picklist.SetItems(self.__current_matches)
583
584
585 if len(self.__current_matches) == 0:
586 if self.speller is not None:
587
588 word = regex.split(self.__speller_word_separators, self.input2match)[-1]
589 if word.strip() != u'':
590 success = False
591 try:
592 success = self.speller.check(word)
593 except:
594 _log.exception('had to disable enchant spell checker')
595 self.speller = None
596 if success:
597 spells = self.speller.suggest(word)
598 truncated_input2match = self.input2match[:self.input2match.rindex(word)]
599 for spell in spells:
600 self.__current_matches.append({'label': truncated_input2match + spell, 'data': None})
601 self._picklist.SetItems(self.__current_matches)
602
604 return self._picklist.GetItemText(self._picklist.GetFirstSelected())
605
606
607
609 """Called when the user pressed <ENTER>."""
610 if self.__picklist_dropdown.IsShown():
611 self._on_list_item_selected()
612 else:
613
614 self.Navigate()
615
617
618 if self.__picklist_dropdown.IsShown():
619 selected = self._picklist.GetFirstSelected()
620 if selected < (len(self.__current_matches) - 1):
621 self.__select_picklist_row(selected+1, selected)
622
623
624
625
626
627 else:
628 self.__timer.Stop()
629 if self.GetValue().strip() == u'':
630 self.__update_matches_in_picklist(val='*')
631 else:
632 self.__update_matches_in_picklist()
633 self._show_picklist()
634
636 if self.__picklist_dropdown.IsShown():
637 selected = self._picklist.GetFirstSelected()
638 if selected > 0:
639 self.__select_picklist_row(selected-1, selected)
640 else:
641
642 pass
643
645 """Under certain circumstances takes special action on TAB.
646
647 returns:
648 True: TAB was handled
649 False: TAB was not handled
650 """
651 if not self.__picklist_dropdown.IsShown():
652 return False
653
654 if len(self.__current_matches) != 1:
655 return False
656
657 if not self.selection_only:
658 return False
659
660 self.__select_picklist_row(new_row_idx=0)
661 self._on_list_item_selected()
662
663 return True
664
665
666
668 raise NotImplementedError('[%s]: cannot create data object' % self.__class__.__name__)
669
671
672 if self.accepted_chars is None:
673 return True
674 return (self.__accepted_chars.match(char) is not None)
675
681
683 if self.__accepted_chars is None:
684 return None
685 return self.__accepted_chars.pattern
686
687 accepted_chars = property(_get_accepted_chars, _set_accepted_chars)
688
690 self.__final_regex = regex.compile(final_regex, flags = regex.LOCALE | regex.UNICODE)
691
693 return self.__final_regex.pattern
694
695 final_regex = property(_get_final_regex, _set_final_regex)
696
698 self.__final_regex_error_msg = msg % self.final_regex
699
701 return self.__final_regex_error_msg
702
703 final_regex_error_msg = property(_get_final_regex_error_msg, _set_final_regex_error_msg)
704
706 if phrase_separators is None:
707 self.__phrase_separators = None
708 else:
709 self.__phrase_separators = regex.compile(phrase_separators, flags = regex.LOCALE | regex.UNICODE)
710
712 if self.__phrase_separators is None:
713 return None
714 return self.__phrase_separators.pattern
715
716 phrase_separators = property(_get_phrase_separators, _set_phrase_separators)
717
719 if word_separators is None:
720 self.__speller_word_separators = regex.compile('[\W\d_]+', flags = regex.LOCALE | regex.UNICODE)
721 else:
722 self.__speller_word_separators = regex.compile(word_separators, flags = regex.LOCALE | regex.UNICODE)
723
725 return self.__speller_word_separators.pattern
726
727 speller_word_separators = property(_get_speller_word_separators, _set_speller_word_separators)
728
730 self.__timer = _cPRWTimer()
731 self.__timer.callback = self._on_timer_fired
732
733 self.__timer.Stop()
734
736 """Callback for delayed match retrieval timer.
737
738 if we end up here:
739 - delay has passed without user input
740 - the value in the input field has not changed since the timer started
741 """
742
743 self.__update_matches_in_picklist()
744
745
746
747
748
749
750
751 wx.CallAfter(self._show_picklist)
752
753
754
756 wx.EVT_TEXT(self, self.GetId(), self._on_text_update)
757 wx.EVT_KEY_DOWN (self, self._on_key_down)
758 wx.EVT_SET_FOCUS(self, self._on_set_focus)
759 wx.EVT_KILL_FOCUS(self, self._on_lose_focus)
760 self._picklist.Bind(wx.EVT_LEFT_DCLICK, self._on_list_item_selected)
761
763 """Gets called when user selected a list item."""
764
765 self._hide_picklist()
766 self.display_as_valid(valid = True)
767
768 data = self._picklist.GetSelectedItemData()
769 if data is None:
770 return
771
772 self.data = data
773
774
775 self.suppress_text_update_smarts = True
776 if self.__phrase_separators is not None:
777 wx.TextCtrl.SetValue(self, u'%s%s%s' % (self.left_part, self._picklist_selection2display_string(), self.right_part))
778 else:
779 wx.TextCtrl.SetValue(self, self._picklist_selection2display_string())
780
781 self.data = self._picklist.GetSelectedItemData()
782 self.MarkDirty()
783
784
785 for callback in self._on_selection_callbacks:
786 callback(self.data)
787
788 if self.navigate_after_selection:
789 self.Navigate()
790 else:
791 self.SetInsertionPoint(self.GetLastPosition())
792
793 return
794
796 """Is called when a key is pressed."""
797
798 keycode = event.GetKeyCode()
799
800 if keycode == wx.WXK_DOWN:
801 self.__on_cursor_down()
802 return
803
804 if keycode == wx.WXK_UP:
805 self.__on_cursor_up()
806 return
807
808 if keycode == wx.WXK_RETURN:
809 self._on_enter()
810 return
811
812 if keycode == wx.WXK_TAB:
813 if event.ShiftDown():
814 self.Navigate(flags = wx.NavigationKeyEvent.IsBackward)
815 return
816 self.__on_tab()
817 self.Navigate(flags = wx.NavigationKeyEvent.IsForward)
818 return
819
820
821 if keycode in [wx.WXK_SHIFT, wx.WXK_BACK, wx.WXK_DELETE, wx.WXK_LEFT, wx.WXK_RIGHT]:
822 pass
823
824
825 elif not self.__char_is_allowed(char = unichr(event.GetUnicodeKey())):
826
827 wx.Bell()
828
829 return
830
831 event.Skip()
832 return
833
834 - def _on_text_update (self, event):
835 """Internal handler for wx.EVT_TEXT.
836
837 Called when text was changed by user or SetValue().
838 """
839 if self.suppress_text_update_smarts:
840 self.suppress_text_update_smarts = False
841 return
842
843 self.data = None
844 self.__current_matches = []
845
846
847
848 val = self.GetValue().strip()
849 ins_point = self.GetInsertionPoint()
850 if val == u'':
851 self._hide_picklist()
852 self.__timer.Stop()
853 else:
854 new_val = gmTools.capitalize(text = val, mode = self.capitalisation_mode)
855 if new_val != val:
856 self.suppress_text_update_smarts = True
857 wx.TextCtrl.SetValue(self, new_val)
858 if ins_point > len(new_val):
859 self.SetInsertionPointEnd()
860 else:
861 self.SetInsertionPoint(ins_point)
862
863
864
865 self.__timer.Start(oneShot = True, milliseconds = self.picklist_delay)
866
867
868 for callback in self._on_modified_callbacks:
869 callback()
870
871 return
872
874
875 self._has_focus = True
876 event.Skip()
877
878 self.__non_edit_font = self.GetFont()
879 edit_font = self.GetFont()
880 edit_font.SetPointSize(pointSize = self.__non_edit_font.GetPointSize() + 1)
881 self.SetFont(edit_font)
882 self.Refresh()
883
884
885 for callback in self._on_set_focus_callbacks:
886 callback()
887
888 self.__timer.Start(oneShot = True, milliseconds = self.picklist_delay)
889 return True
890
892 """Do stuff when leaving the control.
893
894 The user has had her say, so don't second guess
895 intentions but do report error conditions.
896 """
897 self._has_focus = False
898
899
900 self.__timer.Stop()
901 self._hide_picklist()
902
903
904 self.SetSelection(1,1)
905
906 self.SetFont(self.__non_edit_font)
907 self.Refresh()
908
909 is_valid = True
910
911
912
913
914 if self.data is None:
915 val = self.GetValue().strip()
916 if val != u'':
917 self.__update_matches_in_picklist()
918 for match in self.__current_matches:
919 if match['label'] == val:
920 self.data = match['data']
921 self.MarkDirty()
922 break
923
924
925 if self.data is None:
926 if self.selection_only:
927 wx.lib.pubsub.Publisher().sendMessage (
928 topic = 'statustext',
929 data = {'msg': self.selection_only_error_msg}
930 )
931 is_valid = False
932
933
934 if self.__final_regex.match(self.GetValue().strip()) is None:
935 wx.lib.pubsub.Publisher().sendMessage (
936 topic = 'statustext',
937 data = {'msg': self.final_regex_error_msg}
938 )
939 is_valid = False
940
941 self.display_as_valid(valid = is_valid)
942
943
944 for callback in self._on_lose_focus_callbacks:
945 callback()
946
947 event.Skip()
948 return True
949
951 if self.__use_fake_popup:
952 _log.debug(msg)
953
954
955
956 if __name__ == '__main__':
957 from Gnumed.pycommon import gmI18N
958 gmI18N.activate_locale()
959 gmI18N.install_domain(domain='gnumed')
960
961 from Gnumed.pycommon import gmPG2, gmMatchProvider
962
963 prw = None
964
966 print "got focus:"
967 print "value:", prw.GetValue()
968 print "data :", prw.GetData()
969 return True
970
972 print "lost focus:"
973 print "value:", prw.GetValue()
974 print "data :", prw.GetData()
975 return True
976
978 print "modified:"
979 print "value:", prw.GetValue()
980 print "data :", prw.GetData()
981 return True
982
984 print "selected:"
985 print "value:", prw.GetValue()
986 print "data :", prw.GetData()
987 return True
988
990 app = wx.PyWidgetTester(size = (200, 50))
991
992 items = [ {'data':1, 'label':"Bloggs"},
993 {'data':2, 'label':"Baker"},
994 {'data':3, 'label':"Jones"},
995 {'data':4, 'label':"Judson"},
996 {'data':5, 'label':"Jacobs"},
997 {'data':6, 'label':"Judson-Jacobs"}
998 ]
999
1000 mp = gmMatchProvider.cMatchProvider_FixedList(items)
1001
1002 mp.word_separators = '[ \t=+&:@]+'
1003 global prw
1004 prw = cPhraseWheel(parent = app.frame, id = -1)
1005 prw.matcher = mp
1006 prw.capitalisation_mode = gmTools.CAPS_NAMES
1007 prw.add_callback_on_set_focus(callback=display_values_set_focus)
1008 prw.add_callback_on_modified(callback=display_values_modified)
1009 prw.add_callback_on_lose_focus(callback=display_values_lose_focus)
1010 prw.add_callback_on_selection(callback=display_values_selected)
1011
1012 app.frame.Show(True)
1013 app.MainLoop()
1014
1015 return True
1016
1018 print "Do you want to test the database connected phrase wheel ?"
1019 yes_no = raw_input('y/n: ')
1020 if yes_no != 'y':
1021 return True
1022
1023 gmPG2.get_connection()
1024
1025
1026 query = u'select code, name from dem.country where _(name) %(fragment_condition)s'
1027 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query])
1028 app = wx.PyWidgetTester(size = (200, 50))
1029 global prw
1030 prw = cPhraseWheel(parent = app.frame, id = -1)
1031 prw.matcher = mp
1032
1033 app.frame.Show(True)
1034 app.MainLoop()
1035
1036 return True
1037
1039 gmPG2.get_connection()
1040 query = u"select pk_identity, firstnames || ' ' || lastnames || ' ' || dob::text as pat_name from dem.v_basic_person where firstnames || lastnames %(fragment_condition)s"
1041
1042 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query])
1043 app = wx.PyWidgetTester(size = (200, 50))
1044 global prw
1045 prw = cPhraseWheel(parent = app.frame, id = -1)
1046 prw.matcher = mp
1047
1048 app.frame.Show(True)
1049 app.MainLoop()
1050
1051 return True
1052
1070
1071
1072
1073 test_spell_checking_prw()
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
1562
1563
1564
1565
1566
1567
1568
1569
1570