| Home | Trees | Indices | Help |
|
|---|
|
|
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 # stdlib
15 import string, types, time, sys, re as regex, os.path
16
17
18 # 3rd party
19 import wx
20 import wx.lib.mixins.listctrl as listmixins
21 import wx.lib.pubsub
22
23
24 # GNUmed specific
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 # this is used by code outside this module
36
37 #default_phrase_separators = r'[;/|]+'
38 default_phrase_separators = r';+'
39 default_spelling_word_separators = r'[\W\d_]+'
40
41 # those can be used by the <accepted_chars> phrasewheel parameter
42 NUMERIC = '0-9'
43 ALPHANUMERIC = 'a-zA-Z0-9'
44 EMAIL_CHARS = "a-zA-Z0-9\-_@\."
45 WEB_CHARS = "a-zA-Z0-9\.\-_/:"
46
47
48 _timers = []
49 #============================================================
51 """It can be useful to call this early from your shutdown code to avoid hangs on Notify()."""
52 global _timers
53 _log.info('shutting down %s pending timers', len(_timers))
54 for timer in _timers:
55 _log.debug('timer [%s]', timer)
56 timer.Stop()
57 _timers = []
58 #------------------------------------------------------------
60
62 wx.Timer.__init__(self, *args, **kwargs)
63 self.callback = lambda x:x
64 global _timers
65 _timers.append(self)
66
69 #============================================================
70 # FIXME: merge with gmListWidgets
72 """Widget for smart guessing of user fields, after Richard Terry's interface.
73
74 - VB implementation by Richard Terry
75 - Python port by Ian Haywood for GNUmed
76 - enhanced by Karsten Hilbert for GNUmed
77 - enhanced by Ian Haywood for aumed
78 - enhanced by Karsten Hilbert for GNUmed
79
80 @param matcher: a class used to find matches for the current input
81 @type matcher: a L{match provider<Gnumed.pycommon.gmMatchProvider.cMatchProvider>}
82 instance or C{None}
83
84 @param selection_only: whether free-text can be entered without associated data
85 @type selection_only: boolean
86
87 @param capitalisation_mode: how to auto-capitalize input, valid values
88 are found in L{capitalize()<Gnumed.pycommon.gmTools.capitalize>}
89 @type capitalisation_mode: integer
90
91 @param accepted_chars: a regex pattern defining the characters
92 acceptable in the input string, if None no checking is performed
93 @type accepted_chars: None or a string holding a valid regex pattern
94
95 @param final_regex: when the control loses focus the input is
96 checked against this regular expression
97 @type final_regex: a string holding a valid regex pattern
98
99 @param navigate_after_selection: whether or not to immediately
100 navigate to the widget next-in-tab-order after selecting an
101 item from the dropdown picklist
102 @type navigate_after_selection: boolean
103
104 @param speller: if not None used to spellcheck the current input
105 and to retrieve suggested replacements/completions
106 @type speller: None or a L{enchant Dict<enchant>} descendant
107
108 @param picklist_delay: this much time of user inactivity must have
109 passed before the input related smarts kick in and the drop
110 down pick list is shown
111 @type picklist_delay: integer (milliseconds)
112 """
113
115 try:
116 kwargs['style'] = kwargs['style'] | wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.SIMPLE_BORDER
117 except: pass
118 wx.ListCtrl.__init__(self, *args, **kwargs)
119 listmixins.ListCtrlAutoWidthMixin.__init__(self)
120 #--------------------------------------------------------
122 self.DeleteAllItems()
123 self.__data = items
124 pos = len(items) + 1
125 for item in items:
126 row_num = self.InsertStringItem(pos, label=item['list_label'])
127 #--------------------------------------------------------
129 sel_idx = self.GetFirstSelected()
130 if sel_idx == -1:
131 return None
132 return self.__data[sel_idx]['data']
133 #--------------------------------------------------------
135 sel_idx = self.GetFirstSelected()
136 if sel_idx == -1:
137 return None
138 return self.__data[sel_idx]
139 #--------------------------------------------------------
145 #============================================================
146 # base class for both single- and multi-phrase phrase wheels
147 #------------------------------------------------------------
149
151
152 # behaviour
153 self.matcher = None
154 self.selection_only = False
155 self.selection_only_error_msg = _('You must select a value from the picklist or type an exact match.')
156 self.capitalisation_mode = gmTools.CAPS_NONE
157 self.accepted_chars = None
158 self.final_regex = '.*'
159 self.final_regex_error_msg = _('The content is invalid. It must match the regular expression: [%%s]. <%s>') % self.__class__.__name__
160 self.navigate_after_selection = False
161 self.speller = None
162 self.speller_word_separators = default_spelling_word_separators
163 self.picklist_delay = 150 # milliseconds
164
165 # state tracking
166 self._has_focus = False
167 self._current_match_candidates = []
168 self._screenheight = wx.SystemSettings.GetMetric(wx.SYS_SCREEN_Y)
169 self.suppress_text_update_smarts = False
170
171 self.__static_tt = None
172 self.__static_tt_extra = None
173 # don't do this or the tooltip code will fail: self.data = {}
174 # do this instead:
175 self._data = {}
176
177 self._on_selection_callbacks = []
178 self._on_lose_focus_callbacks = []
179 self._on_set_focus_callbacks = []
180 self._on_modified_callbacks = []
181
182 try:
183 kwargs['style'] = kwargs['style'] | wx.TE_PROCESS_TAB
184 except KeyError:
185 kwargs['style'] = wx.TE_PROCESS_TAB
186 super(cPhraseWheelBase, self).__init__(parent, id, **kwargs)
187
188 self.__non_edit_font = self.GetFont()
189 self.__color_valid = self.GetBackgroundColour()
190 global color_prw_valid
191 if color_prw_valid is None:
192 color_prw_valid = wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW)
193
194 self.__init_dropdown(parent = parent)
195 self.__register_events()
196 self.__init_timer()
197 #--------------------------------------------------------
198 # external API
199 #---------------------------------------------------------
201 """Retrieve the data associated with the displayed string(s).
202
203 - self._create_data() must set self.data if possible (/successful)
204 """
205 if len(self._data) == 0:
206 if can_create:
207 self._create_data()
208
209 return self._data
210 #---------------------------------------------------------
212
213 if value is None:
214 value = u''
215
216 self.suppress_text_update_smarts = suppress_smarts
217
218 if data is not None:
219 self.suppress_text_update_smarts = True
220 self.data = self._dictify_data(data = data, value = value)
221 super(cPhraseWheelBase, self).SetValue(value)
222 self.display_as_valid(valid = True)
223
224 # if data already available
225 if len(self._data) > 0:
226 return True
227
228 # empty text value ?
229 if value == u'':
230 # valid value not required ?
231 if not self.selection_only:
232 return True
233
234 if not self._set_data_to_first_match():
235 # not found
236 if self.selection_only:
237 self.display_as_valid(valid = False)
238 return False
239
240 return True
241 #--------------------------------------------------------
243 if valid is True:
244 self.SetBackgroundColour(self.__color_valid)
245 elif valid is False:
246 self.SetBackgroundColour(color_prw_invalid)
247 else:
248 raise ValueError(u'<valid> must be True or False')
249 self.Refresh()
250 #--------------------------------------------------------
251 # callback API
252 #--------------------------------------------------------
254 """Add a callback for invocation when a picklist item is selected.
255
256 The callback will be invoked whenever an item is selected
257 from the picklist. The associated data is passed in as
258 a single parameter. Callbacks must be able to cope with
259 None as the data parameter as that is sent whenever the
260 user changes a previously selected value.
261 """
262 if not callable(callback):
263 raise ValueError('[add_callback_on_selection]: ignoring callback [%s], it is not callable' % callback)
264
265 self._on_selection_callbacks.append(callback)
266 #---------------------------------------------------------
268 """Add a callback for invocation when getting focus."""
269 if not callable(callback):
270 raise ValueError('[add_callback_on_set_focus]: ignoring callback [%s] - not callable' % callback)
271
272 self._on_set_focus_callbacks.append(callback)
273 #---------------------------------------------------------
275 """Add a callback for invocation when losing focus."""
276 if not callable(callback):
277 raise ValueError('[add_callback_on_lose_focus]: ignoring callback [%s] - not callable' % callback)
278
279 self._on_lose_focus_callbacks.append(callback)
280 #---------------------------------------------------------
282 """Add a callback for invocation when the content is modified."""
283 if not callable(callback):
284 raise ValueError('[add_callback_on_modified]: ignoring callback [%s] - not callable' % callback)
285
286 self._on_modified_callbacks.append(callback)
287 #--------------------------------------------------------
288 # match provider proxies
289 #--------------------------------------------------------
293 #---------------------------------------------------------
297 #--------------------------------------------------------
298 # spell-checking
299 #--------------------------------------------------------
301 # FIXME: use Debian's wgerman-medical as "personal" wordlist if available
302 try:
303 import enchant
304 except ImportError:
305 self.speller = None
306 return False
307
308 try:
309 self.speller = enchant.DictWithPWL(None, os.path.expanduser(os.path.join('~', '.gnumed', 'spellcheck', 'wordlist.pwl')))
310 except enchant.DictNotFoundError:
311 self.speller = None
312 return False
313
314 return True
315 #---------------------------------------------------------
317 if self.speller is None:
318 return None
319
320 # get the last word
321 last_word = self.__speller_word_separators.split(val)[-1]
322 if last_word.strip() == u'':
323 return None
324
325 try:
326 suggestions = self.speller.suggest(last_word)
327 except:
328 _log.exception('had to disable (enchant) spell checker')
329 self.speller = None
330 return None
331
332 if len(suggestions) == 0:
333 return None
334
335 input2match_without_last_word = val[:val.rindex(last_word)]
336 return [ input2match_without_last_word + suggestion for suggestion in suggestions ]
337 #--------------------------------------------------------
339 if word_separators is None:
340 self.__speller_word_separators = regex.compile(default_spelling_word_separators, flags = regex.LOCALE | regex.UNICODE)
341 else:
342 self.__speller_word_separators = regex.compile(word_separators, flags = regex.LOCALE | regex.UNICODE)
343
346
347 speller_word_separators = property(_get_speller_word_separators, _set_speller_word_separators)
348 #--------------------------------------------------------
349 # internal API
350 #--------------------------------------------------------
351 # picklist handling
352 #--------------------------------------------------------
354 szr_dropdown = None
355 try:
356 #raise NotImplementedError # uncomment for testing
357 self.__dropdown_needs_relative_position = False
358 self._picklist_dropdown = wx.PopupWindow(parent)
359 list_parent = self._picklist_dropdown
360 self.__use_fake_popup = False
361 except NotImplementedError:
362 self.__use_fake_popup = True
363
364 # on MacOSX wx.PopupWindow is not implemented, so emulate it
365 add_picklist_to_sizer = True
366 szr_dropdown = wx.BoxSizer(wx.VERTICAL)
367
368 # using wx.MiniFrame
369 self.__dropdown_needs_relative_position = False
370 self._picklist_dropdown = wx.MiniFrame (
371 parent = parent,
372 id = -1,
373 style = wx.SIMPLE_BORDER | wx.FRAME_FLOAT_ON_PARENT | wx.FRAME_NO_TASKBAR | wx.POPUP_WINDOW
374 )
375 scroll_win = wx.ScrolledWindow(parent = self._picklist_dropdown, style = wx.NO_BORDER)
376 scroll_win.SetSizer(szr_dropdown)
377 list_parent = scroll_win
378
379 # using wx.Window
380 #self.__dropdown_needs_relative_position = True
381 #self._picklist_dropdown = wx.ScrolledWindow(parent=parent, style = wx.RAISED_BORDER)
382 #self._picklist_dropdown.SetSizer(szr_dropdown)
383 #list_parent = self._picklist_dropdown
384
385 self.__mac_log('dropdown parent: %s' % self._picklist_dropdown.GetParent())
386
387 self._picklist = cPhraseWheelListCtrl (
388 list_parent,
389 style = wx.LC_NO_HEADER
390 )
391 self._picklist.InsertColumn(0, u'')
392
393 if szr_dropdown is not None:
394 szr_dropdown.Add(self._picklist, 1, wx.EXPAND)
395
396 self._picklist_dropdown.Hide()
397 #--------------------------------------------------------
399 """Display the pick list if useful."""
400
401 self._picklist_dropdown.Hide()
402
403 if not self._has_focus:
404 return
405
406 if len(self._current_match_candidates) == 0:
407 return
408
409 # if only one match and text == match: do not show
410 # picklist but rather pick that match
411 if len(self._current_match_candidates) == 1:
412 candidate = self._current_match_candidates[0]
413 if candidate['field_label'] == input2match:
414 self._update_data_from_picked_item(item)
415 return
416
417 border_width = 4
418 extra_height = 25
419
420 # recalculate size
421 rows = len(self._current_match_candidates)
422 if rows < 2: # 2 rows minimum
423 rows = 2
424 if rows > 20: # 20 rows maximum
425 rows = 20
426 self.__mac_log('dropdown needs rows: %s' % rows)
427 dropdown_size = self._picklist_dropdown.GetSize()
428 pw_size = self.GetSize()
429 dropdown_size.SetWidth(pw_size.width)
430 dropdown_size.SetHeight (
431 (pw_size.height * rows)
432 + border_width
433 + extra_height
434 )
435
436 # recalculate position
437 (pw_x_abs, pw_y_abs) = self.ClientToScreenXY(0,0)
438 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)))
439 dropdown_new_x = pw_x_abs
440 dropdown_new_y = pw_y_abs + pw_size.height
441 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)))
442 self.__mac_log('desired dropdown size: %s' % dropdown_size)
443
444 # reaches beyond screen ?
445 if (dropdown_new_y + dropdown_size.height) > self._screenheight:
446 self.__mac_log('dropdown extends offscreen (screen max y: %s)' % self._screenheight)
447 max_height = self._screenheight - dropdown_new_y - 4
448 self.__mac_log('max dropdown height would be: %s' % max_height)
449 if max_height > ((pw_size.height * 2) + 4):
450 dropdown_size.SetHeight(max_height)
451 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)))
452 self.__mac_log('possible dropdown size: %s' % dropdown_size)
453
454 # now set dimensions
455 self._picklist_dropdown.SetSize(dropdown_size)
456 self._picklist.SetSize(self._picklist_dropdown.GetClientSize())
457 self.__mac_log('pick list size set to: %s' % self._picklist_dropdown.GetSize())
458 if self.__dropdown_needs_relative_position:
459 dropdown_new_x, dropdown_new_y = self._picklist_dropdown.GetParent().ScreenToClientXY(dropdown_new_x, dropdown_new_y)
460 self._picklist_dropdown.MoveXY(dropdown_new_x, dropdown_new_y)
461
462 # select first value
463 self._picklist.Select(0)
464
465 # and show it
466 self._picklist_dropdown.Show(True)
467
468 # dropdown_top_left = self._picklist_dropdown.ClientToScreenXY(0,0)
469 # dropdown_size = self._picklist_dropdown.GetSize()
470 # dropdown_bottom_right = self._picklist_dropdown.ClientToScreenXY(dropdown_size.width, dropdown_size.height)
471 # self.__mac_log('dropdown placement now (on screen): x:%s-%s, y:%s-%s' % (
472 # dropdown_top_left[0],
473 # dropdown_bottom_right[0],
474 # dropdown_top_left[1],
475 # dropdown_bottom_right[1])
476 # )
477 #--------------------------------------------------------
481 #--------------------------------------------------------
483 """Mark the given picklist row as selected."""
484 if old_row_idx is not None:
485 pass # FIXME: do we need unselect here ? Select() should do it for us
486 self._picklist.Select(new_row_idx)
487 self._picklist.EnsureVisible(new_row_idx)
488 #--------------------------------------------------------
490 """Get string to display in the field for the given picklist item."""
491 if item is None:
492 item = self._picklist.get_selected_item()
493 try:
494 return item['field_label']
495 except KeyError:
496 pass
497 try:
498 return item['list_label']
499 except KeyError:
500 pass
501 try:
502 return item['label']
503 except KeyError:
504 return u'<no field_*/list_*/label in item>'
505 #return self._picklist.GetItemText(self._picklist.GetFirstSelected())
506 #--------------------------------------------------------
508 """Update the display to show item strings."""
509 # default to single phrase
510 display_string = self._picklist_item2display_string(item = item)
511 self.suppress_text_update_smarts = True
512 super(cPhraseWheelBase, self).SetValue(display_string)
513 # in single-phrase phrasewheels always set cursor to end of string
514 self.SetInsertionPoint(self.GetLastPosition())
515 return
516 #--------------------------------------------------------
517 # match generation
518 #--------------------------------------------------------
520 raise NotImplementedError('[%s]: fragment extraction not implemented' % self.__class__.__name__)
521 #---------------------------------------------------------
523 """Get candidates matching the currently typed input."""
524
525 # get all currently matching items
526 self._current_match_candidates = []
527 if self.matcher is not None:
528 matched, self._current_match_candidates = self.matcher.getMatches(val)
529 self._picklist.SetItems(self._current_match_candidates)
530
531 # no matches:
532 # - none found (perhaps due to a typo)
533 # - or no matcher available
534 # anyway: spellcheck
535 if len(self._current_match_candidates) == 0:
536 suggestions = self._get_suggestions_from_spell_checker(val)
537 if suggestions is not None:
538 self._current_match_candidates = [
539 {'list_label': suggestion, 'field_label': suggestion, 'data': None}
540 for suggestion in suggestions
541 ]
542 self._picklist.SetItems(self._current_match_candidates)
543 #--------------------------------------------------------
544 # tooltip handling
545 #--------------------------------------------------------
549 #--------------------------------------------------------
551 """Calculate dynamic tooltip part based on data item.
552
553 - called via ._set_data() each time property .data (-> .__data) is set
554 - hence also called the first time data is set
555 - the static tooltip can be set any number of ways before that
556 - only when data is first set does the dynamic part become relevant
557 - hence it is sufficient to remember the static part when .data is
558 set for the first time
559 """
560 if self.__static_tt is None:
561 if self.ToolTip is None:
562 self.__static_tt = u''
563 else:
564 self.__static_tt = self.ToolTip.Tip
565
566 dynamic_part = self._get_data_tooltip()
567 if dynamic_part is None:
568 return
569
570 static_part = self.__static_tt
571 if (self.__static_tt_extra) is not None and (self.__static_tt_extra.strip() != u''):
572 static_part = u'%s\n\n%s' % (
573 static_part,
574 self.__static_tt_extra
575 )
576
577 if static_part == u'':
578 tt = dynamic_part
579 else:
580 if dynamic_part.strip() == u'':
581 tt = static_part
582 else:
583 tt = u'%s\n\n%s\n\n%s' % (
584 dynamic_part,
585 gmTools.u_box_horiz_single * 32,
586 static_part
587 )
588
589 self.SetToolTipString(tt)
590 #--------------------------------------------------------
593
596
597 static_tooltip_extra = property(_get_static_tt_extra, _set_static_tt_extra)
598 #--------------------------------------------------------
599 # event handling
600 #--------------------------------------------------------
602 wx.EVT_KEY_DOWN (self, self._on_key_down)
603 wx.EVT_SET_FOCUS(self, self._on_set_focus)
604 wx.EVT_KILL_FOCUS(self, self._on_lose_focus)
605 wx.EVT_TEXT(self, self.GetId(), self._on_text_update)
606 self._picklist.Bind(wx.EVT_LEFT_DCLICK, self._on_list_item_selected)
607 #--------------------------------------------------------
609 """Is called when a key is pressed."""
610
611 keycode = event.GetKeyCode()
612
613 if keycode == wx.WXK_DOWN:
614 self.__on_cursor_down()
615 return
616
617 if keycode == wx.WXK_UP:
618 self.__on_cursor_up()
619 return
620
621 if keycode == wx.WXK_RETURN:
622 self._on_enter()
623 return
624
625 if keycode == wx.WXK_TAB:
626 if event.ShiftDown():
627 self.Navigate(flags = wx.NavigationKeyEvent.IsBackward)
628 return
629 self.__on_tab()
630 self.Navigate(flags = wx.NavigationKeyEvent.IsForward)
631 return
632
633 # FIXME: need PAGE UP/DOWN//POS1/END here to move in picklist
634 if keycode in [wx.WXK_SHIFT, wx.WXK_BACK, wx.WXK_DELETE, wx.WXK_LEFT, wx.WXK_RIGHT]:
635 pass
636
637 # need to handle all non-character key presses *before* this check
638 elif not self.__char_is_allowed(char = unichr(event.GetUnicodeKey())):
639 wx.Bell()
640 # Richard doesn't show any error message here
641 return
642
643 event.Skip()
644 return
645 #--------------------------------------------------------
647
648 self._has_focus = True
649 event.Skip()
650
651 self.__non_edit_font = self.GetFont()
652 edit_font = self.GetFont()
653 edit_font.SetPointSize(pointSize = self.__non_edit_font.GetPointSize() + 1)
654 self.SetFont(edit_font)
655 self.Refresh()
656
657 # notify interested parties
658 for callback in self._on_set_focus_callbacks:
659 callback()
660
661 self.__timer.Start(oneShot = True, milliseconds = self.picklist_delay)
662 return True
663 #--------------------------------------------------------
665 """Do stuff when leaving the control.
666
667 The user has had her say, so don't second guess
668 intentions but do report error conditions.
669 """
670 self._has_focus = False
671
672 self.__timer.Stop()
673 self._hide_picklist()
674 self.SetSelection(1,1)
675 self.SetFont(self.__non_edit_font)
676 self.Refresh()
677
678 is_valid = True
679
680 # the user may have typed a phrase that is an exact match,
681 # however, just typing it won't associate data from the
682 # picklist, so try do that now
683 self._set_data_to_first_match()
684
685 # check value against final_regex if any given
686 if self.__final_regex.match(self.GetValue().strip()) is None:
687 wx.lib.pubsub.Publisher().sendMessage (
688 topic = 'statustext',
689 data = {'msg': self.final_regex_error_msg}
690 )
691 is_valid = False
692
693 self.display_as_valid(valid = is_valid)
694
695 # notify interested parties
696 for callback in self._on_lose_focus_callbacks:
697 callback()
698
699 event.Skip()
700 return True
701 #--------------------------------------------------------
703 """Gets called when user selected a list item."""
704
705 self._hide_picklist()
706
707 item = self._picklist.get_selected_item()
708 # huh ?
709 if item is None:
710 self.display_as_valid(valid = True)
711 return
712
713 self._update_display_from_picked_item(item)
714 self._update_data_from_picked_item(item)
715 self.MarkDirty()
716
717 # and tell the listeners about the user's selection
718 for callback in self._on_selection_callbacks:
719 callback(self._data)
720
721 if self.navigate_after_selection:
722 self.Navigate()
723
724 return
725 #--------------------------------------------------------
727 """Internal handler for wx.EVT_TEXT.
728
729 Called when text was changed by user or by SetValue().
730 """
731 if self.suppress_text_update_smarts:
732 self.suppress_text_update_smarts = False
733 return
734
735 self._adjust_data_after_text_update()
736 self._current_match_candidates = []
737
738 val = self.GetValue().strip()
739 ins_point = self.GetInsertionPoint()
740
741 # if empty string then hide list dropdown window
742 # we also don't need a timer event then
743 if val == u'':
744 self._hide_picklist()
745 self.__timer.Stop()
746 else:
747 new_val = gmTools.capitalize(text = val, mode = self.capitalisation_mode)
748 if new_val != val:
749 self.suppress_text_update_smarts = True
750 super(cPhraseWheelBase, self).SetValue(new_val)
751 if ins_point > len(new_val):
752 self.SetInsertionPointEnd()
753 else:
754 self.SetInsertionPoint(ins_point)
755 # FIXME: SetSelection() ?
756
757 # start timer for delayed match retrieval
758 self.__timer.Start(oneShot = True, milliseconds = self.picklist_delay)
759
760 # notify interested parties
761 for callback in self._on_modified_callbacks:
762 callback()
763
764 return
765 #--------------------------------------------------------
766 # keypress handling
767 #--------------------------------------------------------
769 """Called when the user pressed <ENTER>."""
770 if self._picklist_dropdown.IsShown():
771 self._on_list_item_selected()
772 else:
773 # FIXME: check for errors before navigation
774 self.Navigate()
775 #--------------------------------------------------------
777
778 if self._picklist_dropdown.IsShown():
779 idx_selected = self._picklist.GetFirstSelected()
780 if idx_selected < (len(self._current_match_candidates) - 1):
781 self._select_picklist_row(idx_selected + 1, idx_selected)
782 return
783
784 # if we don't yet have a pick list: open new pick list
785 # (this can happen when we TAB into a field pre-filled
786 # with the top-weighted contextual item but want to
787 # select another contextual item)
788 self.__timer.Stop()
789 if self.GetValue().strip() == u'':
790 val = u'*'
791 else:
792 val = self._extract_fragment_to_match_on()
793 self._update_candidates_in_picklist(val = val)
794 self._show_picklist(input2match = val)
795 #--------------------------------------------------------
797 if self._picklist_dropdown.IsShown():
798 selected = self._picklist.GetFirstSelected()
799 if selected > 0:
800 self._select_picklist_row(selected-1, selected)
801 else:
802 # FIXME: input history ?
803 pass
804 #--------------------------------------------------------
806 """Under certain circumstances take special action on <TAB>.
807
808 returns:
809 True: <TAB> was handled
810 False: <TAB> was not handled
811
812 -> can be used to decide whether to do further <TAB> handling outside this class
813 """
814 # are we seeing the picklist ?
815 if not self._picklist_dropdown.IsShown():
816 return False
817
818 # with only one candidate ?
819 if len(self._current_match_candidates) != 1:
820 return False
821
822 # and do we require the input to be picked from the candidates ?
823 if not self.selection_only:
824 return False
825
826 # then auto-select that item
827 self._select_picklist_row(new_row_idx = 0)
828 self._on_list_item_selected()
829
830 return True
831 #--------------------------------------------------------
832 # timer handling
833 #--------------------------------------------------------
835 self.__timer = _cPRWTimer()
836 self.__timer.callback = self._on_timer_fired
837 # initially stopped
838 self.__timer.Stop()
839 #--------------------------------------------------------
841 """Callback for delayed match retrieval timer.
842
843 if we end up here:
844 - delay has passed without user input
845 - the value in the input field has not changed since the timer started
846 """
847 # update matches according to current input
848 val = self._extract_fragment_to_match_on()
849 self._update_candidates_in_picklist(val = val)
850
851 # we now have either:
852 # - all possible items (within reasonable limits) if input was '*'
853 # - all matching items
854 # - an empty match list if no matches were found
855 # also, our picklist is refilled and sorted according to weight
856 wx.CallAfter(self._show_picklist, input2match = val)
857 #----------------------------------------------------
858 # random helpers and properties
859 #----------------------------------------------------
863 #--------------------------------------------------------
865 # if undefined accept all chars
866 if self.accepted_chars is None:
867 return True
868 return (self.__accepted_chars.match(char) is not None)
869 #--------------------------------------------------------
871 if accepted_chars is None:
872 self.__accepted_chars = None
873 else:
874 self.__accepted_chars = regex.compile(accepted_chars)
875
880
881 accepted_chars = property(_get_accepted_chars, _set_accepted_chars)
882 #--------------------------------------------------------
884 self.__final_regex = regex.compile(final_regex, flags = regex.LOCALE | regex.UNICODE)
885
888
889 final_regex = property(_get_final_regex, _set_final_regex)
890 #--------------------------------------------------------
892 self.__final_regex_error_msg = msg % self.final_regex
893
896
897 final_regex_error_msg = property(_get_final_regex_error_msg, _set_final_regex_error_msg)
898 #--------------------------------------------------------
899 # data munging
900 #--------------------------------------------------------
903 #--------------------------------------------------------
905 self.data = {item['field_label']: item}
906 #--------------------------------------------------------
909 #---------------------------------------------------------
911 raise NotImplementedError('[%s]: cannot adjust data after text update' % self.__class__.__name__)
912 #--------------------------------------------------------
915 #--------------------------------------------------------
918
922
923 data = property(_get_data, _set_data)
924
925
926 #============================================================
927 # FIXME: cols in pick list
928 # FIXME: snap_to_basename+set selection
929 # FIXME: learn() -> PWL
930 # FIXME: up-arrow: show recent (in-memory) history
931 #----------------------------------------------------------
932 # ideas
933 #----------------------------------------------------------
934 #- display possible completion but highlighted for deletion
935 #(- cycle through possible completions)
936 #- pre-fill selection with SELECT ... LIMIT 25
937 #- async threads for match retrieval instead of timer
938 # - on truncated results return item "..." -> selection forcefully retrieves all matches
939
940 #- generators/yield()
941 #- OnChar() - process a char event
942
943 # split input into words and match components against known phrases
944
945 # make special list window:
946 # - deletion of items
947 # - highlight matched parts
948 # - faster scrolling
949 # - wxEditableListBox ?
950
951 # - if non-learning (i.e. fast select only): autocomplete with match
952 # and move cursor to end of match
953 #-----------------------------------------------------------------------------------------------
954 # darn ! this clever hack won't work since we may have crossed a search location threshold
955 #----
956 # #self.__prevFragment = "***********-very-unlikely--------------***************"
957 # #self.__prevMatches = [] # a list of tuples (ID, listbox name, weight)
958 #
959 # # is the current fragment just a longer version of the previous fragment ?
960 # if string.find(aFragment, self.__prevFragment) == 0:
961 # # we then need to search in the previous matches only
962 # for prevMatch in self.__prevMatches:
963 # if string.find(prevMatch[1], aFragment) == 0:
964 # matches.append(prevMatch)
965 # # remember current matches
966 # self.__prefMatches = matches
967 # # no matches found
968 # if len(matches) == 0:
969 # return [(1,_('*no matching items found*'),1)]
970 # else:
971 # return matches
972 #----
973 #TODO:
974 # - see spincontrol for list box handling
975 # stop list (list of negatives): "an" -> "animal" but not "and"
976 #-----
977 #> > remember, you should be searching on either weighted data, or in some
978 #> > situations a start string search on indexed data
979 #>
980 #> Can you be a bit more specific on this ?
981
982 #seaching ones own previous text entered would usually be instring but
983 #weighted (ie the phrases you use the most auto filter to the top)
984
985 #Searching a drug database for a drug brand name is usually more
986 #functional if it does a start string search, not an instring search which is
987 #much slower and usually unecesary. There are many other examples but trust
988 #me one needs both
989
990 # FIXME: support selection-only-or-empty
991
992
993 #============================================================
995
997
998 super(cPhraseWheel, self).GetData(can_create = can_create)
999
1000 if len(self._data) > 0:
1001 if as_instance:
1002 return self._data2instance()
1003
1004 if len(self._data) == 0:
1005 return None
1006
1007 return self._data.values()[0]['data']
1008 #---------------------------------------------------------
1010 """Set the data and thereby set the value, too. if possible.
1011
1012 If you call SetData() you better be prepared
1013 doing a scan of the entire potential match space.
1014
1015 The whole thing will only work if data is found
1016 in the match space anyways.
1017 """
1018 # try getting match candidates
1019 self._update_candidates_in_picklist(u'*')
1020
1021 # do we require a match ?
1022 if self.selection_only:
1023 # yes, but we don't have any candidates
1024 if len(self._current_match_candidates) == 0:
1025 return False
1026
1027 # among candidates look for a match with <data>
1028 for candidate in self._current_match_candidates:
1029 if candidate['data'] == data:
1030 super(cPhraseWheel, self).SetText (
1031 value = candidate['field_label'],
1032 data = data,
1033 suppress_smarts = True
1034 )
1035 return True
1036
1037 # no match found in candidates (but needed) ...
1038 if self.selection_only:
1039 self.display_as_valid(valid = False)
1040 return False
1041
1042 self.data = self._dictify_data(data = data)
1043 self.display_as_valid(valid = True)
1044 return True
1045 #--------------------------------------------------------
1046 # internal API
1047 #--------------------------------------------------------
1049
1050 # this helps if the current input was already selected from the
1051 # list but still is the substring of another pick list item or
1052 # else the picklist will re-open just after selection
1053 if len(self._data) > 0:
1054 self._picklist_dropdown.Hide()
1055 return
1056
1057 return super(cPhraseWheel, self)._show_picklist(input2match = input2match)
1058 #--------------------------------------------------------
1060 # possible ?
1061 if len(self._data) > 0:
1062 return True
1063
1064 # needed ?
1065 val = self.GetValue().strip()
1066 if val == u'':
1067 return True
1068
1069 # so try
1070 self._update_candidates_in_picklist(val = val)
1071 for candidate in self._current_match_candidates:
1072 if candidate['field_label'] == val:
1073 self.data = {candidate['field_label']: candidate}
1074 self.MarkDirty()
1075 return True
1076
1077 # no exact match found
1078 if self.selection_only:
1079 wx.lib.pubsub.Publisher().sendMessage (
1080 topic = 'statustext',
1081 data = {'msg': self.selection_only_error_msg}
1082 )
1083 is_valid = False
1084 return False
1085
1086 return True
1087 #---------------------------------------------------------
1089 self.data = {}
1090 #---------------------------------------------------------
1092 return self.GetValue().strip()
1093 #---------------------------------------------------------
1099 #============================================================
1101
1103
1104 super(cMultiPhraseWheel, self).__init__(*args, **kwargs)
1105
1106 self.phrase_separators = default_phrase_separators
1107 self.left_part = u''
1108 self.right_part = u''
1109 self.speller = None
1110 #---------------------------------------------------------
1112
1113 super(cMultiPhraseWheel, self).GetData(can_create = can_create)
1114
1115 if len(self._data) > 0:
1116 if as_instance:
1117 return self._data2instance()
1118
1119 return self._data.values()
1120 #---------------------------------------------------------
1124 #---------------------------------------------------------
1126
1127 data_dict = {}
1128
1129 for item in data_items:
1130 try:
1131 list_label = item['list_label']
1132 except KeyError:
1133 list_label = item['label']
1134 try:
1135 field_label = item['field_label']
1136 except KeyError:
1137 field_label = list_label
1138 data_dict[field_label] = {'data': item['data'], 'list_label': list_label, 'field_label': field_label}
1139
1140 return data_dict
1141 #---------------------------------------------------------
1142 # internal API
1143 #---------------------------------------------------------
1146 #---------------------------------------------------------
1148 # the textctrl display must already be set properly
1149 displayed_labels = [ p.strip() for p in self.__phrase_separators.split(self.GetValue().strip()) ]
1150 new_data = {}
1151 # this way of looping automatically removes stale
1152 # data for labels which are no longer displayed
1153 for displayed_label in displayed_labels:
1154 try:
1155 new_data[displayed_label] = self._data[displayed_label]
1156 except KeyError:
1157 # this removes stale data for which there
1158 # is no displayed_label anymore
1159 pass
1160
1161 self.data = new_data
1162 #---------------------------------------------------------
1164
1165 cursor_pos = self.GetInsertionPoint()
1166
1167 entire_input = self.GetValue()
1168 if self.__phrase_separators.search(entire_input) is None:
1169 self.left_part = u''
1170 self.right_part = u''
1171 return self.GetValue().strip()
1172
1173 string_left_of_cursor = entire_input[:cursor_pos]
1174 string_right_of_cursor = entire_input[cursor_pos:]
1175
1176 left_parts = [ lp.strip() for lp in self.__phrase_separators.split(string_left_of_cursor) ]
1177 if len(left_parts) == 0:
1178 self.left_part = u''
1179 else:
1180 self.left_part = u'%s%s ' % (
1181 (u'%s ' % self.__phrase_separators.pattern[0]).join(left_parts[:-1]),
1182 self.__phrase_separators.pattern[0]
1183 )
1184
1185 right_parts = [ rp.strip() for rp in self.__phrase_separators.split(string_right_of_cursor) ]
1186 self.right_part = u'%s %s' % (
1187 self.__phrase_separators.pattern[0],
1188 (u'%s ' % self.__phrase_separators.pattern[0]).join(right_parts[1:])
1189 )
1190
1191 val = (left_parts[-1] + right_parts[0]).strip()
1192 return val
1193 #--------------------------------------------------------
1195 val = (u'%s%s%s' % (
1196 self.left_part,
1197 self._picklist_item2display_string(item = item),
1198 self.right_part
1199 )).lstrip().lstrip(';').strip()
1200 self.suppress_text_update_smarts = True
1201 super(cMultiPhraseWheel, self).SetValue(val)
1202 # find item end and move cursor to that place:
1203 item_end = val.index(item['field_label']) + len(item['field_label'])
1204 self.SetInsertionPoint(item_end)
1205 return
1206 #--------------------------------------------------------
1208
1209 # add item to the data
1210 self._data[item['field_label']] = item
1211
1212 # the textctrl display must already be set properly
1213 field_labels = [ p.strip() for p in self.__phrase_separators.split(self.GetValue().strip()) ]
1214 new_data = {}
1215 # this way of looping automatically removes stale
1216 # data for labels which are no longer displayed
1217 for field_label in field_labels:
1218 try:
1219 new_data[field_label] = self._data[field_label]
1220 except KeyError:
1221 # this removes stale data for which there
1222 # is no displayed_label anymore
1223 pass
1224
1225 self.data = new_data
1226 #---------------------------------------------------------
1228 if type(data) == type([]):
1229 # useful because self.GetData() returns just such a list
1230 return self.list2data_dict(data_items = data)
1231 # else assume new-style already-dictified data
1232 return data
1233 #--------------------------------------------------------
1234 # properties
1235 #--------------------------------------------------------
1237 """Set phrase separators.
1238
1239 - must be a valid regular expression pattern
1240
1241 input is split into phrases at boundaries defined by
1242 this regex and matching is performed on the phrase
1243 the cursor is in only,
1244
1245 after selection from picklist phrase_separators[0] is
1246 added to the end of the match in the PRW
1247 """
1248 self.__phrase_separators = regex.compile(phrase_separators, flags = regex.LOCALE | regex.UNICODE)
1249
1252
1253 phrase_separators = property(_get_phrase_separators, _set_phrase_separators)
1254
1255 #============================================================
1256 # main
1257 #------------------------------------------------------------
1258 if __name__ == '__main__':
1259
1260 if len(sys.argv) < 2:
1261 sys.exit()
1262
1263 if sys.argv[1] != u'test':
1264 sys.exit()
1265
1266 from Gnumed.pycommon import gmI18N
1267 gmI18N.activate_locale()
1268 gmI18N.install_domain(domain='gnumed')
1269
1270 from Gnumed.pycommon import gmPG2, gmMatchProvider
1271
1272 prw = None # used for access from display_values_*
1273 #--------------------------------------------------------
1275 print "got focus:"
1276 print "value:", prw.GetValue()
1277 print "data :", prw.GetData()
1278 return True
1279 #--------------------------------------------------------
1281 print "lost focus:"
1282 print "value:", prw.GetValue()
1283 print "data :", prw.GetData()
1284 return True
1285 #--------------------------------------------------------
1287 print "modified:"
1288 print "value:", prw.GetValue()
1289 print "data :", prw.GetData()
1290 return True
1291 #--------------------------------------------------------
1293 print "selected:"
1294 print "value:", prw.GetValue()
1295 print "data :", prw.GetData()
1296 return True
1297 #--------------------------------------------------------
1298 #--------------------------------------------------------
1300 app = wx.PyWidgetTester(size = (200, 50))
1301
1302 items = [ {'data': 1, 'list_label': "Bloggs", 'field_label': "Bloggs", 'weight': 0},
1303 {'data': 2, 'list_label': "Baker", 'field_label': "Baker", 'weight': 0},
1304 {'data': 3, 'list_label': "Jones", 'field_label': "Jones", 'weight': 0},
1305 {'data': 4, 'list_label': "Judson", 'field_label': "Judson", 'weight': 0},
1306 {'data': 5, 'list_label': "Jacobs", 'field_label': "Jacobs", 'weight': 0},
1307 {'data': 6, 'list_label': "Judson-Jacobs", 'field_label': "Judson-Jacobs", 'weight': 0}
1308 ]
1309
1310 mp = gmMatchProvider.cMatchProvider_FixedList(items)
1311 # do NOT treat "-" as a word separator here as there are names like "asa-sismussen"
1312 mp.word_separators = '[ \t=+&:@]+'
1313 global prw
1314 prw = cPhraseWheel(parent = app.frame, id = -1)
1315 prw.matcher = mp
1316 prw.capitalisation_mode = gmTools.CAPS_NAMES
1317 prw.add_callback_on_set_focus(callback=display_values_set_focus)
1318 prw.add_callback_on_modified(callback=display_values_modified)
1319 prw.add_callback_on_lose_focus(callback=display_values_lose_focus)
1320 prw.add_callback_on_selection(callback=display_values_selected)
1321
1322 app.frame.Show(True)
1323 app.MainLoop()
1324
1325 return True
1326 #--------------------------------------------------------
1328 print "Do you want to test the database connected phrase wheel ?"
1329 yes_no = raw_input('y/n: ')
1330 if yes_no != 'y':
1331 return True
1332
1333 gmPG2.get_connection()
1334 query = u"""SELECT code, code || ': ' || _(name), _(name) FROM dem.country WHERE _(name) %(fragment_condition)s"""
1335 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query])
1336 app = wx.PyWidgetTester(size = (400, 50))
1337 global prw
1338 #prw = cPhraseWheel(parent = app.frame, id = -1)
1339 prw = cMultiPhraseWheel(parent = app.frame, id = -1)
1340 prw.matcher = mp
1341
1342 app.frame.Show(True)
1343 app.MainLoop()
1344
1345 return True
1346 #--------------------------------------------------------
1348 gmPG2.get_connection()
1349 query = u"""
1350 select
1351 pk_identity,
1352 firstnames || ' ' || lastnames || ', ' || to_char(dob, 'YYYY-MM-DD'),
1353 firstnames || ' ' || lastnames
1354 from
1355 dem.v_basic_person
1356 where
1357 firstnames || lastnames %(fragment_condition)s
1358 """
1359 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query])
1360 app = wx.PyWidgetTester(size = (500, 50))
1361 global prw
1362 prw = cPhraseWheel(parent = app.frame, id = -1)
1363 prw.matcher = mp
1364 prw.selection_only = True
1365
1366 app.frame.Show(True)
1367 app.MainLoop()
1368
1369 return True
1370 #--------------------------------------------------------
1372 app = wx.PyWidgetTester(size = (200, 50))
1373
1374 global prw
1375 prw = cPhraseWheel(parent = app.frame, id = -1)
1376
1377 prw.add_callback_on_set_focus(callback=display_values_set_focus)
1378 prw.add_callback_on_modified(callback=display_values_modified)
1379 prw.add_callback_on_lose_focus(callback=display_values_lose_focus)
1380 prw.add_callback_on_selection(callback=display_values_selected)
1381
1382 prw.enable_default_spellchecker()
1383
1384 app.frame.Show(True)
1385 app.MainLoop()
1386
1387 return True
1388 #--------------------------------------------------------
1389 #test_prw_fixed_list()
1390 #test_prw_sql2()
1391 #test_spell_checking_prw()
1392 test_prw_patients()
1393
1394 #==================================================
1395
| Home | Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Tue Jun 7 03:58:32 2011 | http://epydoc.sourceforge.net |