1 """GNUmed phrasewheel.
2
3 A class, extending wx.TextCtrl, which has a drop-down pick list,
4 automatically filled based on the inital letters typed. Based on the
5 interface of Richard Terry's Visual Basic client
6
7 This is based on seminal work by Ian Haywood <ihaywood@gnu.org>
8 """
9
10 __version__ = "$Revision: 1.136 $"
11 __author__ = "K.Hilbert <Karsten.Hilbert@gmx.net>, I.Haywood, S.J.Tan <sjtan@bigpond.com>"
12 __license__ = "GPL"
13
14
15 import string, types, time, sys, re as regex, os.path
16
17
18
19 import wx
20 import wx.lib.mixins.listctrl as listmixins
21 import wx.lib.pubsub
22
23
24
25 if __name__ == '__main__':
26 sys.path.insert(0, '../../')
27 from Gnumed.pycommon import gmTools
28
29
30 import logging
31 _log = logging.getLogger('macosx')
32
33
34 color_prw_invalid = 'pink'
35 color_prw_valid = None
36
37 default_phrase_separators = '[;/|]+'
38 default_spelling_word_separators = '[\W\d_]+'
39
40
41 NUMERIC = '0-9'
42 ALPHANUMERIC = 'a-zA-Z0-9'
43 EMAIL_CHARS = "a-zA-Z0-9\-_@\."
44 WEB_CHARS = "a-zA-Z0-9\.\-_/:"
45
46
47 _timers = []
48
50 """It can be useful to call this early from your shutdown code to avoid hangs on Notify()."""
51 global _timers
52 _log.info('shutting down %s pending timers', len(_timers))
53 for timer in _timers:
54 _log.debug('timer [%s]', timer)
55 timer.Stop()
56 _timers = []
57
59
61 wx.Timer.__init__(self, *args, **kwargs)
62 self.callback = lambda x:x
63 global _timers
64 _timers.append(self)
65
68
69
72 try:
73 kwargs['style'] = kwargs['style'] | wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.SIMPLE_BORDER
74 except: pass
75 wx.ListCtrl.__init__(self, *args, **kwargs)
76 listmixins.ListCtrlAutoWidthMixin.__init__(self)
77
79 self.DeleteAllItems()
80 self.__data = items
81 pos = len(items) + 1
82 for item in items:
83 row_num = self.InsertStringItem(pos, label=item['label'])
84
86 sel_idx = self.GetFirstSelected()
87 if sel_idx == -1:
88 return None
89 return self.__data[sel_idx]['data']
90
92 sel_idx = self.GetFirstSelected()
93 if sel_idx == -1:
94 return None
95 return self.__data[sel_idx]['label']
96
97
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
161 """Widget for smart guessing of user fields, after Richard Terry's interface.
162
163 - VB implementation by Richard Terry
164 - Python port by Ian Haywood for GNUmed
165 - enhanced by Karsten Hilbert for GNUmed
166 - enhanced by Ian Haywood for aumed
167 - enhanced by Karsten Hilbert for GNUmed
168
169 @param matcher: a class used to find matches for the current input
170 @type matcher: a L{match provider<Gnumed.pycommon.gmMatchProvider.cMatchProvider>}
171 instance or C{None}
172
173 @param selection_only: whether free-text can be entered without associated data
174 @type selection_only: boolean
175
176 @param capitalisation_mode: how to auto-capitalize input, valid values
177 are found in L{capitalize()<Gnumed.pycommon.gmTools.capitalize>}
178 @type capitalisation_mode: integer
179
180 @param accepted_chars: a regex pattern defining the characters
181 acceptable in the input string, if None no checking is performed
182 @type accepted_chars: None or a string holding a valid regex pattern
183
184 @param final_regex: when the control loses focus the input is
185 checked against this regular expression
186 @type final_regex: a string holding a valid regex pattern
187
188 @param phrase_separators: if not None, input is split into phrases
189 at boundaries defined by this regex and matching/spellchecking
190 is performed on the phrase the cursor is in only
191 @type phrase_separators: None or a string holding a valid regex pattern
192
193 @param navigate_after_selection: whether or not to immediately
194 navigate to the widget next-in-tab-order after selecting an
195 item from the dropdown picklist
196 @type navigate_after_selection: boolean
197
198 @param speller: if not None used to spellcheck the current input
199 and to retrieve suggested replacements/completions
200 @type speller: None or a L{enchant Dict<enchant>} descendant
201
202 @param picklist_delay: this much time of user inactivity must have
203 passed before the input related smarts kick in and the drop
204 down pick list is shown
205 @type picklist_delay: integer (milliseconds)
206 """
207 - def __init__ (self, parent=None, id=-1, value='', *args, **kwargs):
208
209
210 self.matcher = None
211 self.selection_only = False
212 self.selection_only_error_msg = _('You must select a value from the picklist or type an exact match.')
213 self.capitalisation_mode = gmTools.CAPS_NONE
214 self.accepted_chars = None
215 self.final_regex = '.*'
216 self.final_regex_error_msg = _('The content is invalid. It must match the regular expression: [%%s]. <%s>') % self.__class__.__name__
217 self.phrase_separators = default_phrase_separators
218 self.navigate_after_selection = False
219 self.speller = None
220 self.speller_word_separators = default_spelling_word_separators
221 self.picklist_delay = 150
222
223
224 self._has_focus = False
225 self.suppress_text_update_smarts = False
226 self.__current_matches = []
227 self._screenheight = wx.SystemSettings.GetMetric(wx.SYS_SCREEN_Y)
228 self.input2match = ''
229 self.left_part = ''
230 self.right_part = ''
231 self.data = None
232
233 self._on_selection_callbacks = []
234 self._on_lose_focus_callbacks = []
235 self._on_set_focus_callbacks = []
236 self._on_modified_callbacks = []
237
238 try:
239 kwargs['style'] = kwargs['style'] | wx.TE_PROCESS_TAB
240 except KeyError:
241 kwargs['style'] = wx.TE_PROCESS_TAB
242 wx.TextCtrl.__init__(self, parent, id, **kwargs)
243
244 self.__non_edit_font = self.GetFont()
245 self.__color_valid = self.GetBackgroundColour()
246 global color_prw_valid
247 if color_prw_valid is None:
248 color_prw_valid = wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW)
249
250 self.__init_dropdown(parent = parent)
251 self.__register_events()
252 self.__init_timer()
253
254
255
257 """
258 Add a callback for invocation when a picklist item is selected.
259
260 The callback will be invoked whenever an item is selected
261 from the picklist. The associated data is passed in as
262 a single parameter. Callbacks must be able to cope with
263 None as the data parameter as that is sent whenever the
264 user changes a previously selected value.
265 """
266 if not callable(callback):
267 raise ValueError('[add_callback_on_selection]: ignoring callback [%s], it is not callable' % callback)
268
269 self._on_selection_callbacks.append(callback)
270
272 """
273 Add a callback for invocation when getting focus.
274 """
275 if not callable(callback):
276 raise ValueError('[add_callback_on_set_focus]: ignoring callback [%s] - not callable' % callback)
277
278 self._on_set_focus_callbacks.append(callback)
279
281 """
282 Add a callback for invocation when losing focus.
283 """
284 if not callable(callback):
285 raise ValueError('[add_callback_on_lose_focus]: ignoring callback [%s] - not callable' % callback)
286
287 self._on_lose_focus_callbacks.append(callback)
288
290 """
291 Add a callback for invocation when the content is modified.
292 """
293 if not callable(callback):
294 raise ValueError('[add_callback_on_modified]: ignoring callback [%s] - not callable' % callback)
295
296 self._on_modified_callbacks.append(callback)
297
299 """
300 Set the data and thereby set the value, too.
301
302 If you call SetData() you better be prepared
303 doing a scan of the entire potential match space.
304
305 The whole thing will only work if data is found
306 in the match space anyways.
307 """
308 if self.matcher is None:
309 matched, matches = (False, [])
310 else:
311 matched, matches = self.matcher.getMatches('*')
312
313 if self.selection_only:
314 if not matched or (len(matches) == 0):
315 return False
316
317 for match in matches:
318 if match['data'] == data:
319 self.display_as_valid(valid = True)
320 self.suppress_text_update_smarts = True
321 wx.TextCtrl.SetValue(self, match['label'])
322 self.data = data
323 return True
324
325
326 if self.selection_only:
327 return False
328
329 self.data = data
330 self.display_as_valid(valid = True)
331 return True
332
333 - def GetData(self, can_create=False, as_instance=False):
334 """Retrieve the data associated with the displayed string.
335
336 _create_data() must set self.data if possible
337 """
338 if self.data is None:
339 if can_create:
340 self._create_data()
341
342 if self.data is not None:
343 if as_instance:
344 return self._data2instance()
345
346 return self.data
347
348 - def SetText(self, value=u'', data=None, suppress_smarts=False):
349
350 self.suppress_text_update_smarts = suppress_smarts
351
352 if data is not None:
353 self.suppress_text_update_smarts = True
354 self.data = data
355 if value is None:
356 value = u''
357 wx.TextCtrl.SetValue(self, value)
358 self.display_as_valid(valid = True)
359
360
361 if self.data is not None:
362 return True
363
364 if value == u'' and not self.selection_only:
365 return True
366
367
368 if self.matcher is None:
369 stat, matches = (False, [])
370 else:
371 stat, matches = self.matcher.getMatches(aFragment = value)
372
373 for match in matches:
374 if match['label'] == value:
375 self.data = match['data']
376 return True
377
378
379 if self.selection_only:
380 self.display_as_valid(valid = False)
381 return False
382
383 return True
384
385 - def set_context(self, context=None, val=None):
386 if self.matcher is not None:
387 self.matcher.set_context(context=context, val=val)
388
389 - def unset_context(self, context=None):
390 if self.matcher is not None:
391 self.matcher.unset_context(context=context)
392
394
395 try:
396 import enchant
397 except ImportError:
398 self.speller = None
399 return False
400 try:
401 self.speller = enchant.DictWithPWL(None, os.path.expanduser(os.path.join('~', '.gnumed', 'spellcheck', 'wordlist.pwl')))
402 except enchant.DictNotFoundError:
403 self.speller = None
404 return False
405 return True
406
408 if valid is True:
409 self.SetBackgroundColour(self.__color_valid)
410 elif valid is False:
411 self.SetBackgroundColour(color_prw_invalid)
412 else:
413 raise ArgumentError(u'<valid> must be True or False')
414 self.Refresh()
415
416
417
418
419
421 szr_dropdown = None
422 try:
423
424 self.__dropdown_needs_relative_position = False
425 self.__picklist_dropdown = wx.PopupWindow(parent)
426 list_parent = self.__picklist_dropdown
427 self.__use_fake_popup = False
428 except NotImplementedError:
429 self.__use_fake_popup = True
430
431
432 add_picklist_to_sizer = True
433 szr_dropdown = wx.BoxSizer(wx.VERTICAL)
434
435
436 self.__dropdown_needs_relative_position = False
437 self.__picklist_dropdown = wx.MiniFrame (
438 parent = parent,
439 id = -1,
440 style = wx.SIMPLE_BORDER | wx.FRAME_FLOAT_ON_PARENT | wx.FRAME_NO_TASKBAR | wx.POPUP_WINDOW
441 )
442 scroll_win = wx.ScrolledWindow(parent = self.__picklist_dropdown, style = wx.NO_BORDER)
443 scroll_win.SetSizer(szr_dropdown)
444 list_parent = scroll_win
445
446
447
448
449
450
451
452 self.mac_log('dropdown parent: %s' % self.__picklist_dropdown.GetParent())
453
454
455
456
457
458
459 self._picklist = cPhraseWheelListCtrl (
460 list_parent,
461 style = wx.LC_NO_HEADER
462 )
463 self._picklist.InsertColumn(0, '')
464
465 if szr_dropdown is not None:
466 szr_dropdown.Add(self._picklist, 1, wx.EXPAND)
467
468 self.__picklist_dropdown.Hide()
469
471 """Display the pick list."""
472
473 border_width = 4
474 extra_height = 25
475
476 self.__picklist_dropdown.Hide()
477
478
479
480 if self.data is not None:
481 return
482
483 if not self._has_focus:
484 return
485
486 if len(self.__current_matches) == 0:
487 return
488
489
490 if len(self.__current_matches) == 1:
491 if self.__current_matches[0]['label'] == self.input2match:
492 self.data = self.__current_matches[0]['data']
493 return
494
495
496 rows = len(self.__current_matches)
497 if rows < 2:
498 rows = 2
499 if rows > 20:
500 rows = 20
501 self.mac_log('dropdown needs rows: %s' % rows)
502 dropdown_size = self.__picklist_dropdown.GetSize()
503 pw_size = self.GetSize()
504 dropdown_size.SetWidth(pw_size.width)
505 dropdown_size.SetHeight (
506 (pw_size.height * rows)
507 + border_width
508 + extra_height
509 )
510
511
512 (pw_x_abs, pw_y_abs) = self.ClientToScreenXY(0,0)
513 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)))
514 dropdown_new_x = pw_x_abs
515 dropdown_new_y = pw_y_abs + pw_size.height
516 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)))
517 self.mac_log('desired dropdown size: %s' % dropdown_size)
518
519
520 if (dropdown_new_y + dropdown_size.height) > self._screenheight:
521 self.mac_log('dropdown extends offscreen (screen max y: %s)' % self._screenheight)
522 max_height = self._screenheight - dropdown_new_y - 4
523 self.mac_log('max dropdown height would be: %s' % max_height)
524 if max_height > ((pw_size.height * 2) + 4):
525 dropdown_size.SetHeight(max_height)
526 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)))
527 self.mac_log('possible dropdown size: %s' % dropdown_size)
528
529
530 self.__picklist_dropdown.SetSize(dropdown_size)
531 self._picklist.SetSize(self.__picklist_dropdown.GetClientSize())
532 self.mac_log('pick list size set to: %s' % self.__picklist_dropdown.GetSize())
533 if self.__dropdown_needs_relative_position:
534 dropdown_new_x, dropdown_new_y = self.__picklist_dropdown.GetParent().ScreenToClientXY(dropdown_new_x, dropdown_new_y)
535 self.__picklist_dropdown.MoveXY(dropdown_new_x, dropdown_new_y)
536
537
538 self._picklist.Select(0)
539
540
541 self.__picklist_dropdown.Show(True)
542
543 dd_tl = self.__picklist_dropdown.ClientToScreenXY(0,0)
544 dd_size = self.__picklist_dropdown.GetSize()
545 dd_br = self.__picklist_dropdown.ClientToScreenXY(dd_size.width, dd_size.height)
546 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]))
547
549 """Hide the pick list."""
550 self.__picklist_dropdown.Hide()
551
553 if old_row_idx is not None:
554 pass
555 self._picklist.Select(new_row_idx)
556 self._picklist.EnsureVisible(new_row_idx)
557
559 """Get the matches for the currently typed input fragment."""
560
561 self.input2match = val
562 if self.input2match is None:
563 if self.__phrase_separators is None:
564 self.input2match = self.GetValue().strip()
565 else:
566
567 entire_input = self.GetValue()
568 cursor_pos = self.GetInsertionPoint()
569 left_of_cursor = entire_input[:cursor_pos]
570 right_of_cursor = entire_input[cursor_pos:]
571 left_boundary = self.__phrase_separators.search(left_of_cursor)
572 if left_boundary is not None:
573 phrase_start = left_boundary.end()
574 else:
575 phrase_start = 0
576 self.left_part = entire_input[:phrase_start]
577
578 right_boundary = self.__phrase_separators.search(right_of_cursor)
579 if right_boundary is not None:
580 phrase_end = cursor_pos + (right_boundary.start() - 1)
581 else:
582 phrase_end = len(entire_input) - 1
583 self.right_part = entire_input[phrase_end+1:]
584 self.input2match = entire_input[phrase_start:phrase_end+1]
585
586
587 if self.matcher is not None:
588 matched, self.__current_matches = self.matcher.getMatches(self.input2match)
589 self._picklist.SetItems(self.__current_matches)
590
591
592 if len(self.__current_matches) == 0:
593 if self.speller is not None:
594
595 word = regex.split(self.__speller_word_separators, self.input2match)[-1]
596 if word.strip() != u'':
597 success = False
598 try:
599 success = self.speller.check(word)
600 except:
601 _log.exception('had to disable enchant spell checker')
602 self.speller = None
603 if success:
604 spells = self.speller.suggest(word)
605 truncated_input2match = self.input2match[:self.input2match.rindex(word)]
606 for spell in spells:
607 self.__current_matches.append({'label': truncated_input2match + spell, 'data': None})
608 self._picklist.SetItems(self.__current_matches)
609
611 return self._picklist.GetItemText(self._picklist.GetFirstSelected())
612
613
614
616 """Called when the user pressed <ENTER>."""
617 if self.__picklist_dropdown.IsShown():
618 self._on_list_item_selected()
619 else:
620
621 self.Navigate()
622
624
625 if self.__picklist_dropdown.IsShown():
626 selected = self._picklist.GetFirstSelected()
627 if selected < (len(self.__current_matches) - 1):
628 self.__select_picklist_row(selected+1, selected)
629
630
631
632
633
634 else:
635 self.__timer.Stop()
636 if self.GetValue().strip() == u'':
637 self.__update_matches_in_picklist(val='*')
638 else:
639 self.__update_matches_in_picklist()
640 self._show_picklist()
641
643 if self.__picklist_dropdown.IsShown():
644 selected = self._picklist.GetFirstSelected()
645 if selected > 0:
646 self.__select_picklist_row(selected-1, selected)
647 else:
648
649 pass
650
652 """Under certain circumstances takes special action on TAB.
653
654 returns:
655 True: TAB was handled
656 False: TAB was not handled
657 """
658 if not self.__picklist_dropdown.IsShown():
659 return False
660
661 if len(self.__current_matches) != 1:
662 return False
663
664 if not self.selection_only:
665 return False
666
667 self.__select_picklist_row(new_row_idx=0)
668 self._on_list_item_selected()
669
670 return True
671
672
673
675 raise NotImplementedError('[%s]: cannot create data object' % self.__class__.__name__)
676
678
679 if self.accepted_chars is None:
680 return True
681 return (self.__accepted_chars.match(char) is not None)
682
688
690 if self.__accepted_chars is None:
691 return None
692 return self.__accepted_chars.pattern
693
694 accepted_chars = property(_get_accepted_chars, _set_accepted_chars)
695
697 self.__final_regex = regex.compile(final_regex, flags = regex.LOCALE | regex.UNICODE)
698
700 return self.__final_regex.pattern
701
702 final_regex = property(_get_final_regex, _set_final_regex)
703
705 self.__final_regex_error_msg = msg % self.final_regex
706
708 return self.__final_regex_error_msg
709
710 final_regex_error_msg = property(_get_final_regex_error_msg, _set_final_regex_error_msg)
711
713 if phrase_separators is None:
714 self.__phrase_separators = None
715 else:
716 self.__phrase_separators = regex.compile(phrase_separators, flags = regex.LOCALE | regex.UNICODE)
717
719 if self.__phrase_separators is None:
720 return None
721 return self.__phrase_separators.pattern
722
723 phrase_separators = property(_get_phrase_separators, _set_phrase_separators)
724
726 if word_separators is None:
727 self.__speller_word_separators = regex.compile('[\W\d_]+', flags = regex.LOCALE | regex.UNICODE)
728 else:
729 self.__speller_word_separators = regex.compile(word_separators, flags = regex.LOCALE | regex.UNICODE)
730
732 return self.__speller_word_separators.pattern
733
734 speller_word_separators = property(_get_speller_word_separators, _set_speller_word_separators)
735
737 self.__timer = _cPRWTimer()
738 self.__timer.callback = self._on_timer_fired
739
740 self.__timer.Stop()
741
743 """Callback for delayed match retrieval timer.
744
745 if we end up here:
746 - delay has passed without user input
747 - the value in the input field has not changed since the timer started
748 """
749
750 self.__update_matches_in_picklist()
751
752
753
754
755
756
757
758 wx.CallAfter(self._show_picklist)
759
760
761
763 wx.EVT_TEXT(self, self.GetId(), self._on_text_update)
764 wx.EVT_KEY_DOWN (self, self._on_key_down)
765 wx.EVT_SET_FOCUS(self, self._on_set_focus)
766 wx.EVT_KILL_FOCUS(self, self._on_lose_focus)
767 self._picklist.Bind(wx.EVT_LEFT_DCLICK, self._on_list_item_selected)
768
770 """Gets called when user selected a list item."""
771
772 self._hide_picklist()
773 self.display_as_valid(valid = True)
774
775 data = self._picklist.GetSelectedItemData()
776 if data is None:
777 return
778
779 self.data = data
780
781
782 self.suppress_text_update_smarts = True
783 if self.__phrase_separators is not None:
784 wx.TextCtrl.SetValue(self, u'%s%s%s' % (self.left_part, self._picklist_selection2display_string(), self.right_part))
785 else:
786 wx.TextCtrl.SetValue(self, self._picklist_selection2display_string())
787
788 self.data = self._picklist.GetSelectedItemData()
789 self.MarkDirty()
790
791
792 for callback in self._on_selection_callbacks:
793 callback(self.data)
794
795 if self.navigate_after_selection:
796 self.Navigate()
797 else:
798 self.SetInsertionPoint(self.GetLastPosition())
799
800 return
801
803 """Is called when a key is pressed."""
804
805 keycode = event.GetKeyCode()
806
807 if keycode == wx.WXK_DOWN:
808 self.__on_cursor_down()
809 return
810
811 if keycode == wx.WXK_UP:
812 self.__on_cursor_up()
813 return
814
815 if keycode == wx.WXK_RETURN:
816 self._on_enter()
817 return
818
819 if keycode == wx.WXK_TAB:
820 if event.ShiftDown():
821 self.Navigate(flags = wx.NavigationKeyEvent.IsBackward)
822 return
823 self.__on_tab()
824 self.Navigate(flags = wx.NavigationKeyEvent.IsForward)
825 return
826
827
828 if keycode in [wx.WXK_SHIFT, wx.WXK_BACK, wx.WXK_DELETE, wx.WXK_LEFT, wx.WXK_RIGHT]:
829 pass
830
831
832 elif not self.__char_is_allowed(char = unichr(event.GetUnicodeKey())):
833
834 wx.Bell()
835
836 return
837
838 event.Skip()
839 return
840
841 - def _on_text_update (self, event):
842 """Internal handler for wx.EVT_TEXT.
843
844 Called when text was changed by user or SetValue().
845 """
846 if self.suppress_text_update_smarts:
847 self.suppress_text_update_smarts = False
848 return
849
850 self.data = None
851 self.__current_matches = []
852
853
854
855 val = self.GetValue().strip()
856 ins_point = self.GetInsertionPoint()
857 if val == u'':
858 self._hide_picklist()
859 self.__timer.Stop()
860 else:
861 new_val = gmTools.capitalize(text = val, mode = self.capitalisation_mode)
862 if new_val != val:
863 self.suppress_text_update_smarts = True
864 wx.TextCtrl.SetValue(self, new_val)
865 if ins_point > len(new_val):
866 self.SetInsertionPointEnd()
867 else:
868 self.SetInsertionPoint(ins_point)
869
870
871
872 self.__timer.Start(oneShot = True, milliseconds = self.picklist_delay)
873
874
875 for callback in self._on_modified_callbacks:
876 callback()
877
878 return
879
881
882 self._has_focus = True
883 event.Skip()
884
885 self.__non_edit_font = self.GetFont()
886 edit_font = self.GetFont()
887 edit_font.SetPointSize(pointSize = self.__non_edit_font.GetPointSize() + 1)
888 self.SetFont(edit_font)
889 self.Refresh()
890
891
892 for callback in self._on_set_focus_callbacks:
893 callback()
894
895 self.__timer.Start(oneShot = True, milliseconds = self.picklist_delay)
896 return True
897
899 """Do stuff when leaving the control.
900
901 The user has had her say, so don't second guess
902 intentions but do report error conditions.
903 """
904 self._has_focus = False
905
906
907 self.__timer.Stop()
908 self._hide_picklist()
909
910
911 self.SetSelection(1,1)
912
913 self.SetFont(self.__non_edit_font)
914 self.Refresh()
915
916 is_valid = True
917
918
919
920
921 if self.data is None:
922 val = self.GetValue().strip()
923 if val != u'':
924 self.__update_matches_in_picklist()
925 for match in self.__current_matches:
926 if match['label'] == val:
927 self.data = match['data']
928 self.MarkDirty()
929 break
930
931
932 if self.data is None:
933 if self.selection_only:
934 wx.lib.pubsub.Publisher().sendMessage (
935 topic = 'statustext',
936 data = {'msg': self.selection_only_error_msg}
937 )
938 is_valid = False
939
940
941 if self.__final_regex.match(self.GetValue().strip()) is None:
942 wx.lib.pubsub.Publisher().sendMessage (
943 topic = 'statustext',
944 data = {'msg': self.final_regex_error_msg}
945 )
946 is_valid = False
947
948 self.display_as_valid(valid = is_valid)
949
950
951 for callback in self._on_lose_focus_callbacks:
952 callback()
953
954 event.Skip()
955 return True
956
958 if self.__use_fake_popup:
959 _log.debug(msg)
960
961
962
963 if __name__ == '__main__':
964
965 if len(sys.argv) < 2:
966 sys.exit()
967
968 if sys.argv[1] != u'test':
969 sys.exit()
970
971 from Gnumed.pycommon import gmI18N
972 gmI18N.activate_locale()
973 gmI18N.install_domain(domain='gnumed')
974
975 from Gnumed.pycommon import gmPG2, gmMatchProvider
976
977 prw = None
978
980 print "got focus:"
981 print "value:", prw.GetValue()
982 print "data :", prw.GetData()
983 return True
984
986 print "lost focus:"
987 print "value:", prw.GetValue()
988 print "data :", prw.GetData()
989 return True
990
992 print "modified:"
993 print "value:", prw.GetValue()
994 print "data :", prw.GetData()
995 return True
996
998 print "selected:"
999 print "value:", prw.GetValue()
1000 print "data :", prw.GetData()
1001 return True
1002
1004 app = wx.PyWidgetTester(size = (200, 50))
1005
1006 items = [ {'data':1, 'label':"Bloggs"},
1007 {'data':2, 'label':"Baker"},
1008 {'data':3, 'label':"Jones"},
1009 {'data':4, 'label':"Judson"},
1010 {'data':5, 'label':"Jacobs"},
1011 {'data':6, 'label':"Judson-Jacobs"}
1012 ]
1013
1014 mp = gmMatchProvider.cMatchProvider_FixedList(items)
1015
1016 mp.word_separators = '[ \t=+&:@]+'
1017 global prw
1018 prw = cPhraseWheel(parent = app.frame, id = -1)
1019 prw.matcher = mp
1020 prw.capitalisation_mode = gmTools.CAPS_NAMES
1021 prw.add_callback_on_set_focus(callback=display_values_set_focus)
1022 prw.add_callback_on_modified(callback=display_values_modified)
1023 prw.add_callback_on_lose_focus(callback=display_values_lose_focus)
1024 prw.add_callback_on_selection(callback=display_values_selected)
1025
1026 app.frame.Show(True)
1027 app.MainLoop()
1028
1029 return True
1030
1032 print "Do you want to test the database connected phrase wheel ?"
1033 yes_no = raw_input('y/n: ')
1034 if yes_no != 'y':
1035 return True
1036
1037 gmPG2.get_connection()
1038
1039
1040 query = u'select code, name from dem.country where _(name) %(fragment_condition)s'
1041 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query])
1042 app = wx.PyWidgetTester(size = (200, 50))
1043 global prw
1044 prw = cPhraseWheel(parent = app.frame, id = -1)
1045 prw.matcher = mp
1046
1047 app.frame.Show(True)
1048 app.MainLoop()
1049
1050 return True
1051
1053 gmPG2.get_connection()
1054 query = u"select pk_identity, firstnames || ' ' || lastnames || ' ' || dob::text as pat_name from dem.v_basic_person where firstnames || lastnames %(fragment_condition)s"
1055
1056 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query])
1057 app = wx.PyWidgetTester(size = (200, 50))
1058 global prw
1059 prw = cPhraseWheel(parent = app.frame, id = -1)
1060 prw.matcher = mp
1061
1062 app.frame.Show(True)
1063 app.MainLoop()
1064
1065 return True
1066
1084
1085
1086
1087 test_spell_checking_prw()
1088
1089
1090
1091