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
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, *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.__static_tt = None
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 (successful)
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
683
715
717
718 if self.accepted_chars is None:
719 return True
720 return (self.__accepted_chars.match(char) is not None)
721
722
723
726
728 self.__data = data
729 self.__reset_tooltip()
730
731 data = property(_get_data, _set_data)
732
738
740 if self.__accepted_chars is None:
741 return None
742 return self.__accepted_chars.pattern
743
744 accepted_chars = property(_get_accepted_chars, _set_accepted_chars)
745
747 self.__final_regex = regex.compile(final_regex, flags = regex.LOCALE | regex.UNICODE)
748
750 return self.__final_regex.pattern
751
752 final_regex = property(_get_final_regex, _set_final_regex)
753
755 self.__final_regex_error_msg = msg % self.final_regex
756
758 return self.__final_regex_error_msg
759
760 final_regex_error_msg = property(_get_final_regex_error_msg, _set_final_regex_error_msg)
761
763 if phrase_separators is None:
764 self.__phrase_separators = None
765 else:
766 self.__phrase_separators = regex.compile(phrase_separators, flags = regex.LOCALE | regex.UNICODE)
767
769 if self.__phrase_separators is None:
770 return None
771 return self.__phrase_separators.pattern
772
773 phrase_separators = property(_get_phrase_separators, _set_phrase_separators)
774
776 if word_separators is None:
777 self.__speller_word_separators = regex.compile('[\W\d_]+', flags = regex.LOCALE | regex.UNICODE)
778 else:
779 self.__speller_word_separators = regex.compile(word_separators, flags = regex.LOCALE | regex.UNICODE)
780
782 return self.__speller_word_separators.pattern
783
784 speller_word_separators = property(_get_speller_word_separators, _set_speller_word_separators)
785
787 self.__timer = _cPRWTimer()
788 self.__timer.callback = self._on_timer_fired
789
790 self.__timer.Stop()
791
793 """Callback for delayed match retrieval timer.
794
795 if we end up here:
796 - delay has passed without user input
797 - the value in the input field has not changed since the timer started
798 """
799
800 self.__update_matches_in_picklist()
801
802
803
804
805
806
807
808 wx.CallAfter(self._show_picklist)
809
810
811
813 wx.EVT_TEXT(self, self.GetId(), self._on_text_update)
814 wx.EVT_KEY_DOWN (self, self._on_key_down)
815 wx.EVT_SET_FOCUS(self, self._on_set_focus)
816 wx.EVT_KILL_FOCUS(self, self._on_lose_focus)
817 self._picklist.Bind(wx.EVT_LEFT_DCLICK, self._on_list_item_selected)
818
820 """Gets called when user selected a list item."""
821
822 self._hide_picklist()
823 self.display_as_valid(valid = True)
824
825 data = self._picklist.GetSelectedItemData()
826 if data is None:
827 return
828
829 self.data = data
830
831
832 self.suppress_text_update_smarts = True
833 if self.__phrase_separators is not None:
834 wx.TextCtrl.SetValue(self, u'%s%s%s' % (self.left_part, self._picklist_selection2display_string(), self.right_part))
835 else:
836 wx.TextCtrl.SetValue(self, self._picklist_selection2display_string())
837
838 self.data = self._picklist.GetSelectedItemData()
839 self.MarkDirty()
840
841
842 for callback in self._on_selection_callbacks:
843 callback(self.data)
844
845 if self.navigate_after_selection:
846 self.Navigate()
847 else:
848 self.SetInsertionPoint(self.GetLastPosition())
849
850 return
851
853 """Is called when a key is pressed."""
854
855 keycode = event.GetKeyCode()
856
857 if keycode == wx.WXK_DOWN:
858 self.__on_cursor_down()
859 return
860
861 if keycode == wx.WXK_UP:
862 self.__on_cursor_up()
863 return
864
865 if keycode == wx.WXK_RETURN:
866 self._on_enter()
867 return
868
869 if keycode == wx.WXK_TAB:
870 if event.ShiftDown():
871 self.Navigate(flags = wx.NavigationKeyEvent.IsBackward)
872 return
873 self.__on_tab()
874 self.Navigate(flags = wx.NavigationKeyEvent.IsForward)
875 return
876
877
878 if keycode in [wx.WXK_SHIFT, wx.WXK_BACK, wx.WXK_DELETE, wx.WXK_LEFT, wx.WXK_RIGHT]:
879 pass
880
881
882 elif not self.__char_is_allowed(char = unichr(event.GetUnicodeKey())):
883
884 wx.Bell()
885
886 return
887
888 event.Skip()
889 return
890
891 - def _on_text_update (self, event):
892 """Internal handler for wx.EVT_TEXT.
893
894 Called when text was changed by user or SetValue().
895 """
896 if self.suppress_text_update_smarts:
897 self.suppress_text_update_smarts = False
898 return
899
900 self.data = None
901 self.__current_matches = []
902
903
904
905 val = self.GetValue().strip()
906 ins_point = self.GetInsertionPoint()
907 if val == u'':
908 self._hide_picklist()
909 self.__timer.Stop()
910 else:
911 new_val = gmTools.capitalize(text = val, mode = self.capitalisation_mode)
912 if new_val != val:
913 self.suppress_text_update_smarts = True
914 wx.TextCtrl.SetValue(self, new_val)
915 if ins_point > len(new_val):
916 self.SetInsertionPointEnd()
917 else:
918 self.SetInsertionPoint(ins_point)
919
920
921
922 self.__timer.Start(oneShot = True, milliseconds = self.picklist_delay)
923
924
925 for callback in self._on_modified_callbacks:
926 callback()
927
928 return
929
931
932 self._has_focus = True
933 event.Skip()
934
935 self.__non_edit_font = self.GetFont()
936 edit_font = self.GetFont()
937 edit_font.SetPointSize(pointSize = self.__non_edit_font.GetPointSize() + 1)
938 self.SetFont(edit_font)
939 self.Refresh()
940
941
942 for callback in self._on_set_focus_callbacks:
943 callback()
944
945 self.__timer.Start(oneShot = True, milliseconds = self.picklist_delay)
946 return True
947
949 """Do stuff when leaving the control.
950
951 The user has had her say, so don't second guess
952 intentions but do report error conditions.
953 """
954 self._has_focus = False
955
956
957 self.__timer.Stop()
958 self._hide_picklist()
959
960
961 self.SetSelection(1,1)
962
963 self.SetFont(self.__non_edit_font)
964 self.Refresh()
965
966 is_valid = True
967
968
969
970
971 if self.data is None:
972 val = self.GetValue().strip()
973 if val != u'':
974 self.__update_matches_in_picklist()
975 for match in self.__current_matches:
976 if match['label'] == val:
977 self.data = match['data']
978 self.MarkDirty()
979 break
980
981
982 if self.data is None:
983 if self.selection_only:
984 wx.lib.pubsub.Publisher().sendMessage (
985 topic = 'statustext',
986 data = {'msg': self.selection_only_error_msg}
987 )
988 is_valid = False
989
990
991 if self.__final_regex.match(self.GetValue().strip()) is None:
992 wx.lib.pubsub.Publisher().sendMessage (
993 topic = 'statustext',
994 data = {'msg': self.final_regex_error_msg}
995 )
996 is_valid = False
997
998 self.display_as_valid(valid = is_valid)
999
1000
1001 for callback in self._on_lose_focus_callbacks:
1002 callback()
1003
1004 event.Skip()
1005 return True
1006
1008 if self.__use_fake_popup:
1009 _log.debug(msg)
1010
1011
1012
1013 if __name__ == '__main__':
1014
1015 if len(sys.argv) < 2:
1016 sys.exit()
1017
1018 if sys.argv[1] != u'test':
1019 sys.exit()
1020
1021 from Gnumed.pycommon import gmI18N
1022 gmI18N.activate_locale()
1023 gmI18N.install_domain(domain='gnumed')
1024
1025 from Gnumed.pycommon import gmPG2, gmMatchProvider
1026
1027 prw = None
1028
1030 print "got focus:"
1031 print "value:", prw.GetValue()
1032 print "data :", prw.GetData()
1033 return True
1034
1036 print "lost focus:"
1037 print "value:", prw.GetValue()
1038 print "data :", prw.GetData()
1039 return True
1040
1042 print "modified:"
1043 print "value:", prw.GetValue()
1044 print "data :", prw.GetData()
1045 return True
1046
1048 print "selected:"
1049 print "value:", prw.GetValue()
1050 print "data :", prw.GetData()
1051 return True
1052
1054 app = wx.PyWidgetTester(size = (200, 50))
1055
1056 items = [ {'data':1, 'label':"Bloggs"},
1057 {'data':2, 'label':"Baker"},
1058 {'data':3, 'label':"Jones"},
1059 {'data':4, 'label':"Judson"},
1060 {'data':5, 'label':"Jacobs"},
1061 {'data':6, 'label':"Judson-Jacobs"}
1062 ]
1063
1064 mp = gmMatchProvider.cMatchProvider_FixedList(items)
1065
1066 mp.word_separators = '[ \t=+&:@]+'
1067 global prw
1068 prw = cPhraseWheel(parent = app.frame, id = -1)
1069 prw.matcher = mp
1070 prw.capitalisation_mode = gmTools.CAPS_NAMES
1071 prw.add_callback_on_set_focus(callback=display_values_set_focus)
1072 prw.add_callback_on_modified(callback=display_values_modified)
1073 prw.add_callback_on_lose_focus(callback=display_values_lose_focus)
1074 prw.add_callback_on_selection(callback=display_values_selected)
1075
1076 app.frame.Show(True)
1077 app.MainLoop()
1078
1079 return True
1080
1082 print "Do you want to test the database connected phrase wheel ?"
1083 yes_no = raw_input('y/n: ')
1084 if yes_no != 'y':
1085 return True
1086
1087 gmPG2.get_connection()
1088
1089
1090 query = u'select code, name from dem.country where _(name) %(fragment_condition)s'
1091 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query])
1092 app = wx.PyWidgetTester(size = (200, 50))
1093 global prw
1094 prw = cPhraseWheel(parent = app.frame, id = -1)
1095 prw.matcher = mp
1096
1097 app.frame.Show(True)
1098 app.MainLoop()
1099
1100 return True
1101
1103 gmPG2.get_connection()
1104 query = u"select pk_identity, firstnames || ' ' || lastnames || ' ' || dob::text as pat_name from dem.v_basic_person where firstnames || lastnames %(fragment_condition)s"
1105
1106 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query])
1107 app = wx.PyWidgetTester(size = (200, 50))
1108 global prw
1109 prw = cPhraseWheel(parent = app.frame, id = -1)
1110 prw.matcher = mp
1111
1112 app.frame.Show(True)
1113 app.MainLoop()
1114
1115 return True
1116
1134
1135
1136
1137 test_spell_checking_prw()
1138
1139
1140
1141