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