| 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 = '[;/|]+'
38 default_spelling_word_separators = '[\W\d_]+'
39
40 # those can be used by the <accepted_chars> phrasewheel parameter
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 # FIXME: merge with gmListWidgets
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 #--------------------------------------------------------
96 #============================================================
97 # FIXME: cols in pick list
98 # FIXME: snap_to_basename+set selection
99 # FIXME: learn() -> PWL
100 # FIXME: up-arrow: show recent (in-memory) history
101 #----------------------------------------------------------
102 # ideas
103 #----------------------------------------------------------
104 #- display possible completion but highlighted for deletion
105 #(- cycle through possible completions)
106 #- pre-fill selection with SELECT ... LIMIT 25
107 #- async threads for match retrieval instead of timer
108 # - on truncated results return item "..." -> selection forcefully retrieves all matches
109
110 #- generators/yield()
111 #- OnChar() - process a char event
112
113 # split input into words and match components against known phrases
114
115 # make special list window:
116 # - deletion of items
117 # - highlight matched parts
118 # - faster scrolling
119 # - wxEditableListBox ?
120
121 # - if non-learning (i.e. fast select only): autocomplete with match
122 # and move cursor to end of match
123 #-----------------------------------------------------------------------------------------------
124 # darn ! this clever hack won't work since we may have crossed a search location threshold
125 #----
126 # #self.__prevFragment = "XXXXXXXXXXXXXXXXXX-very-unlikely--------------XXXXXXXXXXXXXXX"
127 # #self.__prevMatches = [] # a list of tuples (ID, listbox name, weight)
128 #
129 # # is the current fragment just a longer version of the previous fragment ?
130 # if string.find(aFragment, self.__prevFragment) == 0:
131 # # we then need to search in the previous matches only
132 # for prevMatch in self.__prevMatches:
133 # if string.find(prevMatch[1], aFragment) == 0:
134 # matches.append(prevMatch)
135 # # remember current matches
136 # self.__prefMatches = matches
137 # # no matches found
138 # if len(matches) == 0:
139 # return [(1,_('*no matching items found*'),1)]
140 # else:
141 # return matches
142 #----
143 #TODO:
144 # - see spincontrol for list box handling
145 # stop list (list of negatives): "an" -> "animal" but not "and"
146 #-----
147 #> > remember, you should be searching on either weighted data, or in some
148 #> > situations a start string search on indexed data
149 #>
150 #> Can you be a bit more specific on this ?
151
152 #seaching ones own previous text entered would usually be instring but
153 #weighted (ie the phrases you use the most auto filter to the top)
154
155 #Searching a drug database for a drug brand name is usually more
156 #functional if it does a start string search, not an instring search which is
157 #much slower and usually unecesary. There are many other examples but trust
158 #me one needs both
159 #-----
160
161 # FIXME: support selection-only-or-empty
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 """
210
211 # behaviour
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 # milliseconds
224
225 # state tracking
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.__static_tt_extra = None
235 self.__data = None
236
237 self._on_selection_callbacks = []
238 self._on_lose_focus_callbacks = []
239 self._on_set_focus_callbacks = []
240 self._on_modified_callbacks = []
241
242 try:
243 kwargs['style'] = kwargs['style'] | wx.TE_PROCESS_TAB
244 except KeyError:
245 kwargs['style'] = wx.TE_PROCESS_TAB
246 wx.TextCtrl.__init__(self, parent, id, **kwargs)
247
248 self.__non_edit_font = self.GetFont()
249 self.__color_valid = self.GetBackgroundColour()
250 global color_prw_valid
251 if color_prw_valid is None:
252 color_prw_valid = wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW)
253
254 self.__init_dropdown(parent = parent)
255 self.__register_events()
256 self.__init_timer()
257 #--------------------------------------------------------
258 # external API
259 #--------------------------------------------------------
261 """
262 Add a callback for invocation when a picklist item is selected.
263
264 The callback will be invoked whenever an item is selected
265 from the picklist. The associated data is passed in as
266 a single parameter. Callbacks must be able to cope with
267 None as the data parameter as that is sent whenever the
268 user changes a previously selected value.
269 """
270 if not callable(callback):
271 raise ValueError('[add_callback_on_selection]: ignoring callback [%s], it is not callable' % callback)
272
273 self._on_selection_callbacks.append(callback)
274 #---------------------------------------------------------
276 """
277 Add a callback for invocation when getting focus.
278 """
279 if not callable(callback):
280 raise ValueError('[add_callback_on_set_focus]: ignoring callback [%s] - not callable' % callback)
281
282 self._on_set_focus_callbacks.append(callback)
283 #---------------------------------------------------------
285 """
286 Add a callback for invocation when losing focus.
287 """
288 if not callable(callback):
289 raise ValueError('[add_callback_on_lose_focus]: ignoring callback [%s] - not callable' % callback)
290
291 self._on_lose_focus_callbacks.append(callback)
292 #---------------------------------------------------------
294 """
295 Add a callback for invocation when the content is modified.
296 """
297 if not callable(callback):
298 raise ValueError('[add_callback_on_modified]: ignoring callback [%s] - not callable' % callback)
299
300 self._on_modified_callbacks.append(callback)
301 #---------------------------------------------------------
303 """
304 Set the data and thereby set the value, too.
305
306 If you call SetData() you better be prepared
307 doing a scan of the entire potential match space.
308
309 The whole thing will only work if data is found
310 in the match space anyways.
311 """
312 if self.matcher is None:
313 matched, matches = (False, [])
314 else:
315 matched, matches = self.matcher.getMatches('*')
316
317 if self.selection_only:
318 if not matched or (len(matches) == 0):
319 return False
320
321 for match in matches:
322 if match['data'] == data:
323 self.display_as_valid(valid = True)
324 self.suppress_text_update_smarts = True
325 wx.TextCtrl.SetValue(self, match['label'])
326 self.data = data
327 return True
328
329 # no match found ...
330 if self.selection_only:
331 return False
332
333 self.data = data
334 self.display_as_valid(valid = True)
335 return True
336 #---------------------------------------------------------
338 """Retrieve the data associated with the displayed string.
339
340 _create_data() must set self.data if possible (successful)
341 """
342 if self.data is None:
343 if can_create:
344 self._create_data()
345
346 if self.data is not None:
347 if as_instance:
348 return self._data2instance()
349
350 return self.data
351 #---------------------------------------------------------
353
354 self.suppress_text_update_smarts = suppress_smarts
355
356 if data is not None:
357 self.suppress_text_update_smarts = True
358 self.data = data
359 if value is None:
360 value = u''
361 wx.TextCtrl.SetValue(self, value)
362 self.display_as_valid(valid = True)
363
364 # if data already available
365 if self.data is not None:
366 return True
367
368 if value == u'' and not self.selection_only:
369 return True
370
371 # or try to find data from matches
372 if self.matcher is None:
373 stat, matches = (False, [])
374 else:
375 stat, matches = self.matcher.getMatches(aFragment = value)
376
377 for match in matches:
378 if match['label'] == value:
379 self.data = match['data']
380 return True
381
382 # not found
383 if self.selection_only:
384 self.display_as_valid(valid = False)
385 return False
386
387 return True
388 #--------------------------------------------------------
392 #---------------------------------------------------------
396 #--------------------------------------------------------
398 # FIXME: use Debian's wgerman-medical as "personal" wordlist if available
399 try:
400 import enchant
401 except ImportError:
402 self.speller = None
403 return False
404 try:
405 self.speller = enchant.DictWithPWL(None, os.path.expanduser(os.path.join('~', '.gnumed', 'spellcheck', 'wordlist.pwl')))
406 except enchant.DictNotFoundError:
407 self.speller = None
408 return False
409 return True
410 #--------------------------------------------------------
412 if valid is True:
413 self.SetBackgroundColour(self.__color_valid)
414 elif valid is False:
415 self.SetBackgroundColour(color_prw_invalid)
416 else:
417 raise ValueError(u'<valid> must be True or False')
418 self.Refresh()
419 #--------------------------------------------------------
420 # internal API
421 #--------------------------------------------------------
422 # picklist handling
423 #--------------------------------------------------------
425 szr_dropdown = None
426 try:
427 #raise NotImplementedError # for testing
428 self.__dropdown_needs_relative_position = False
429 self.__picklist_dropdown = wx.PopupWindow(parent)
430 list_parent = self.__picklist_dropdown
431 self.__use_fake_popup = False
432 except NotImplementedError:
433 self.__use_fake_popup = True
434
435 # on MacOSX wx.PopupWindow is not implemented, so emulate it
436 add_picklist_to_sizer = True
437 szr_dropdown = wx.BoxSizer(wx.VERTICAL)
438
439 # using wx.MiniFrame
440 self.__dropdown_needs_relative_position = False
441 self.__picklist_dropdown = wx.MiniFrame (
442 parent = parent,
443 id = -1,
444 style = wx.SIMPLE_BORDER | wx.FRAME_FLOAT_ON_PARENT | wx.FRAME_NO_TASKBAR | wx.POPUP_WINDOW
445 )
446 scroll_win = wx.ScrolledWindow(parent = self.__picklist_dropdown, style = wx.NO_BORDER)
447 scroll_win.SetSizer(szr_dropdown)
448 list_parent = scroll_win
449
450 # using wx.Window
451 #self.__dropdown_needs_relative_position = True
452 #self.__picklist_dropdown = wx.ScrolledWindow(parent=parent, style = wx.RAISED_BORDER)
453 #self.__picklist_dropdown.SetSizer(szr_dropdown)
454 #list_parent = self.__picklist_dropdown
455
456 self.mac_log('dropdown parent: %s' % self.__picklist_dropdown.GetParent())
457
458 # FIXME: support optional headers
459 # if kwargs['show_list_headers']:
460 # flags = 0
461 # else:
462 # flags = wx.LC_NO_HEADER
463 self._picklist = cPhraseWheelListCtrl (
464 list_parent,
465 style = wx.LC_NO_HEADER
466 )
467 self._picklist.InsertColumn(0, '')
468
469 if szr_dropdown is not None:
470 szr_dropdown.Add(self._picklist, 1, wx.EXPAND)
471
472 self.__picklist_dropdown.Hide()
473 #--------------------------------------------------------
475 """Display the pick list."""
476
477 border_width = 4
478 extra_height = 25
479
480 self.__picklist_dropdown.Hide()
481
482 # this helps if the current input was already selected from the
483 # list but still is the substring of another pick list item
484 if self.data is not None:
485 return
486
487 if not self._has_focus:
488 return
489
490 if len(self.__current_matches) == 0:
491 return
492
493 # if only one match and text == match
494 if len(self.__current_matches) == 1:
495 if self.__current_matches[0]['label'] == self.input2match:
496 self.data = self.__current_matches[0]['data']
497 return
498
499 # recalculate size
500 rows = len(self.__current_matches)
501 if rows < 2: # 2 rows minimum
502 rows = 2
503 if rows > 20: # 20 rows maximum
504 rows = 20
505 self.mac_log('dropdown needs rows: %s' % rows)
506 dropdown_size = self.__picklist_dropdown.GetSize()
507 pw_size = self.GetSize()
508 dropdown_size.SetWidth(pw_size.width)
509 dropdown_size.SetHeight (
510 (pw_size.height * rows)
511 + border_width
512 + extra_height
513 )
514
515 # recalculate position
516 (pw_x_abs, pw_y_abs) = self.ClientToScreenXY(0,0)
517 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)))
518 dropdown_new_x = pw_x_abs
519 dropdown_new_y = pw_y_abs + pw_size.height
520 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)))
521 self.mac_log('desired dropdown size: %s' % dropdown_size)
522
523 # reaches beyond screen ?
524 if (dropdown_new_y + dropdown_size.height) > self._screenheight:
525 self.mac_log('dropdown extends offscreen (screen max y: %s)' % self._screenheight)
526 max_height = self._screenheight - dropdown_new_y - 4
527 self.mac_log('max dropdown height would be: %s' % max_height)
528 if max_height > ((pw_size.height * 2) + 4):
529 dropdown_size.SetHeight(max_height)
530 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)))
531 self.mac_log('possible dropdown size: %s' % dropdown_size)
532
533 # now set dimensions
534 self.__picklist_dropdown.SetSize(dropdown_size)
535 self._picklist.SetSize(self.__picklist_dropdown.GetClientSize())
536 self.mac_log('pick list size set to: %s' % self.__picklist_dropdown.GetSize())
537 if self.__dropdown_needs_relative_position:
538 dropdown_new_x, dropdown_new_y = self.__picklist_dropdown.GetParent().ScreenToClientXY(dropdown_new_x, dropdown_new_y)
539 self.__picklist_dropdown.MoveXY(dropdown_new_x, dropdown_new_y)
540
541 # select first value
542 self._picklist.Select(0)
543
544 # and show it
545 self.__picklist_dropdown.Show(True)
546
547 dd_tl = self.__picklist_dropdown.ClientToScreenXY(0,0)
548 dd_size = self.__picklist_dropdown.GetSize()
549 dd_br = self.__picklist_dropdown.ClientToScreenXY(dd_size.width, dd_size.height)
550 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]))
551 #--------------------------------------------------------
553 """Hide the pick list."""
554 self.__picklist_dropdown.Hide() # dismiss the dropdown list window
555 #--------------------------------------------------------
557 if old_row_idx is not None:
558 pass # FIXME: do we need unselect here ? Select() should do it for us
559 self._picklist.Select(new_row_idx)
560 self._picklist.EnsureVisible(new_row_idx)
561 #---------------------------------------------------------
563 """Get the matches for the currently typed input fragment."""
564
565 self.input2match = val
566 if self.input2match is None:
567 if self.__phrase_separators is None:
568 self.input2match = self.GetValue().strip()
569 else:
570 # get current(ly relevant part of) input
571 entire_input = self.GetValue()
572 cursor_pos = self.GetInsertionPoint()
573 left_of_cursor = entire_input[:cursor_pos]
574 right_of_cursor = entire_input[cursor_pos:]
575 left_boundary = self.__phrase_separators.search(left_of_cursor)
576 if left_boundary is not None:
577 phrase_start = left_boundary.end()
578 else:
579 phrase_start = 0
580 self.left_part = entire_input[:phrase_start]
581 # find next phrase separator after cursor position
582 right_boundary = self.__phrase_separators.search(right_of_cursor)
583 if right_boundary is not None:
584 phrase_end = cursor_pos + (right_boundary.start() - 1)
585 else:
586 phrase_end = len(entire_input) - 1
587 self.right_part = entire_input[phrase_end+1:]
588 self.input2match = entire_input[phrase_start:phrase_end+1]
589
590 # get all currently matching items
591 if self.matcher is not None:
592 matched, self.__current_matches = self.matcher.getMatches(self.input2match)
593 self._picklist.SetItems(self.__current_matches)
594
595 # no matches found: might simply be due to a typo, so spellcheck
596 if len(self.__current_matches) == 0:
597 if self.speller is not None:
598 # filter out the last word
599 word = regex.split(self.__speller_word_separators, self.input2match)[-1]
600 if word.strip() != u'':
601 success = False
602 try:
603 success = self.speller.check(word)
604 except:
605 _log.exception('had to disable enchant spell checker')
606 self.speller = None
607 if success:
608 spells = self.speller.suggest(word)
609 truncated_input2match = self.input2match[:self.input2match.rindex(word)]
610 for spell in spells:
611 self.__current_matches.append({'label': truncated_input2match + spell, 'data': None})
612 self._picklist.SetItems(self.__current_matches)
613 #--------------------------------------------------------
616 #--------------------------------------------------------
617 # internal helpers: GUI
618 #--------------------------------------------------------
620 """Called when the user pressed <ENTER>."""
621 if self.__picklist_dropdown.IsShown():
622 self._on_list_item_selected()
623 else:
624 # FIXME: check for errors before navigation
625 self.Navigate()
626 #--------------------------------------------------------
628
629 if self.__picklist_dropdown.IsShown():
630 selected = self._picklist.GetFirstSelected()
631 if selected < (len(self.__current_matches) - 1):
632 self.__select_picklist_row(selected+1, selected)
633
634 # if we don't yet have a pick list: open new pick list
635 # (this can happen when we TAB into a field pre-filled
636 # with the top-weighted contextual data but want to
637 # select another contextual item)
638 else:
639 self.__timer.Stop()
640 if self.GetValue().strip() == u'':
641 self.__update_matches_in_picklist(val='*')
642 else:
643 self.__update_matches_in_picklist()
644 self._show_picklist()
645 #--------------------------------------------------------
647 if self.__picklist_dropdown.IsShown():
648 selected = self._picklist.GetFirstSelected()
649 if selected > 0:
650 self.__select_picklist_row(selected-1, selected)
651 else:
652 # FIXME: input history ?
653 pass
654 #--------------------------------------------------------
656 """Under certain circumstances takes special action on TAB.
657
658 returns:
659 True: TAB was handled
660 False: TAB was not handled
661 """
662 if not self.__picklist_dropdown.IsShown():
663 return False
664
665 if len(self.__current_matches) != 1:
666 return False
667
668 if not self.selection_only:
669 return False
670
671 self.__select_picklist_row(new_row_idx=0)
672 self._on_list_item_selected()
673
674 return True
675 #--------------------------------------------------------
676 # internal helpers: logic
677 #--------------------------------------------------------
680 #--------------------------------------------------------
684 #--------------------------------------------------------
686 """Calculate dynamic tooltip part based on data item.
687
688 - called via ._set_data() each time property .data (-> .__data) is set
689 - hence also called the first time data is set
690 - the static tooltip can be set any number of ways before that
691 - only when data is first set does the dynamic part become relevant
692 - hence it is sufficient to remember the static part when .data is
693 set for the first time
694 """
695 if self.__static_tt is None:
696 if self.ToolTip is None:
697 self.__static_tt = u''
698 else:
699 self.__static_tt = self.ToolTip.Tip
700
701 dynamic_part = self._get_data_tooltip()
702 if dynamic_part is None:
703 return
704
705 static_part = self.__static_tt
706 if (self.__static_tt_extra) is not None and (self.__static_tt_extra.strip() != u''):
707 static_part = u'%s\n\n%s' % (
708 static_part,
709 self.__static_tt_extra
710 )
711
712 if static_part == u'':
713 tt = dynamic_part
714 else:
715 if dynamic_part.strip() == u'':
716 tt = static_part
717 else:
718 tt = u'%s\n\n%s\n\n%s' % (
719 dynamic_part,
720 gmTools.u_box_horiz_single * 32,
721 static_part
722 )
723 self.SetToolTipString(tt)
724 #--------------------------------------------------------
726 # if undefined accept all chars
727 if self.accepted_chars is None:
728 return True
729 return (self.__accepted_chars.match(char) is not None)
730 #--------------------------------------------------------
731 # properties
732 #--------------------------------------------------------
735
739
740 data = property(_get_data, _set_data)
741 #--------------------------------------------------------
743 if accepted_chars is None:
744 self.__accepted_chars = None
745 else:
746 self.__accepted_chars = regex.compile(accepted_chars)
747
752
753 accepted_chars = property(_get_accepted_chars, _set_accepted_chars)
754 #--------------------------------------------------------
756 self.__final_regex = regex.compile(final_regex, flags = regex.LOCALE | regex.UNICODE)
757
760
761 final_regex = property(_get_final_regex, _set_final_regex)
762 #--------------------------------------------------------
764 self.__final_regex_error_msg = msg % self.final_regex
765
768
769 final_regex_error_msg = property(_get_final_regex_error_msg, _set_final_regex_error_msg)
770 #--------------------------------------------------------
772 if phrase_separators is None:
773 self.__phrase_separators = None
774 else:
775 self.__phrase_separators = regex.compile(phrase_separators, flags = regex.LOCALE | regex.UNICODE)
776
778 if self.__phrase_separators is None:
779 return None
780 return self.__phrase_separators.pattern
781
782 phrase_separators = property(_get_phrase_separators, _set_phrase_separators)
783 #--------------------------------------------------------
785 if word_separators is None:
786 self.__speller_word_separators = regex.compile('[\W\d_]+', flags = regex.LOCALE | regex.UNICODE)
787 else:
788 self.__speller_word_separators = regex.compile(word_separators, flags = regex.LOCALE | regex.UNICODE)
789
792
793 speller_word_separators = property(_get_speller_word_separators, _set_speller_word_separators)
794 #--------------------------------------------------------
797
800
801 static_tooltip_extra = property(_get_static_tt_extra, _set_static_tt_extra)
802 #--------------------------------------------------------
803 # timer code
804 #--------------------------------------------------------
806 self.__timer = _cPRWTimer()
807 self.__timer.callback = self._on_timer_fired
808 # initially stopped
809 self.__timer.Stop()
810 #--------------------------------------------------------
812 """Callback for delayed match retrieval timer.
813
814 if we end up here:
815 - delay has passed without user input
816 - the value in the input field has not changed since the timer started
817 """
818 # update matches according to current input
819 self.__update_matches_in_picklist()
820
821 # we now have either:
822 # - all possible items (within reasonable limits) if input was '*'
823 # - all matching items
824 # - an empty match list if no matches were found
825 # also, our picklist is refilled and sorted according to weight
826
827 wx.CallAfter(self._show_picklist)
828 #--------------------------------------------------------
829 # event handling
830 #--------------------------------------------------------
832 wx.EVT_TEXT(self, self.GetId(), self._on_text_update)
833 wx.EVT_KEY_DOWN (self, self._on_key_down)
834 wx.EVT_SET_FOCUS(self, self._on_set_focus)
835 wx.EVT_KILL_FOCUS(self, self._on_lose_focus)
836 self._picklist.Bind(wx.EVT_LEFT_DCLICK, self._on_list_item_selected)
837 #--------------------------------------------------------
839 """Gets called when user selected a list item."""
840
841 self._hide_picklist()
842 self.display_as_valid(valid = True)
843
844 data = self._picklist.GetSelectedItemData() # just so that _picklist_selection2display_string can use it
845 if data is None:
846 return
847
848 self.data = data
849
850 # update our display
851 self.suppress_text_update_smarts = True
852 if self.__phrase_separators is not None:
853 wx.TextCtrl.SetValue(self, u'%s%s%s' % (self.left_part, self._picklist_selection2display_string(), self.right_part))
854 else:
855 wx.TextCtrl.SetValue(self, self._picklist_selection2display_string())
856
857 self.data = self._picklist.GetSelectedItemData()
858 self.MarkDirty()
859
860 # and tell the listeners about the user's selection
861 for callback in self._on_selection_callbacks:
862 callback(self.data)
863
864 if self.navigate_after_selection:
865 self.Navigate()
866 else:
867 self.SetInsertionPoint(self.GetLastPosition())
868
869 return
870 #--------------------------------------------------------
872 """Is called when a key is pressed."""
873
874 keycode = event.GetKeyCode()
875
876 if keycode == wx.WXK_DOWN:
877 self.__on_cursor_down()
878 return
879
880 if keycode == wx.WXK_UP:
881 self.__on_cursor_up()
882 return
883
884 if keycode == wx.WXK_RETURN:
885 self._on_enter()
886 return
887
888 if keycode == wx.WXK_TAB:
889 if event.ShiftDown():
890 self.Navigate(flags = wx.NavigationKeyEvent.IsBackward)
891 return
892 self.__on_tab()
893 self.Navigate(flags = wx.NavigationKeyEvent.IsForward)
894 return
895
896 # FIXME: need PAGE UP/DOWN//POS1/END here to move in picklist
897 if keycode in [wx.WXK_SHIFT, wx.WXK_BACK, wx.WXK_DELETE, wx.WXK_LEFT, wx.WXK_RIGHT]:
898 pass
899
900 # need to handle all non-character key presses *before* this check
901 elif not self.__char_is_allowed(char = unichr(event.GetUnicodeKey())):
902 # FIXME: configure ?
903 wx.Bell()
904 # FIXME: display error message ? Richard doesn't ...
905 return
906
907 event.Skip()
908 return
909 #--------------------------------------------------------
911 """Internal handler for wx.EVT_TEXT.
912
913 Called when text was changed by user or SetValue().
914 """
915 if self.suppress_text_update_smarts:
916 self.suppress_text_update_smarts = False
917 return
918
919 self.data = None
920 self.__current_matches = []
921
922 # if empty string then hide list dropdown window
923 # we also don't need a timer event then
924 val = self.GetValue().strip()
925 ins_point = self.GetInsertionPoint()
926 if val == u'':
927 self._hide_picklist()
928 self.__timer.Stop()
929 else:
930 new_val = gmTools.capitalize(text = val, mode = self.capitalisation_mode)
931 if new_val != val:
932 self.suppress_text_update_smarts = True
933 wx.TextCtrl.SetValue(self, new_val)
934 if ins_point > len(new_val):
935 self.SetInsertionPointEnd()
936 else:
937 self.SetInsertionPoint(ins_point)
938 # FIXME: SetSelection() ?
939
940 # start timer for delayed match retrieval
941 self.__timer.Start(oneShot = True, milliseconds = self.picklist_delay)
942
943 # notify interested parties
944 for callback in self._on_modified_callbacks:
945 callback()
946
947 return
948 #--------------------------------------------------------
950
951 self._has_focus = True
952 event.Skip()
953
954 self.__non_edit_font = self.GetFont()
955 edit_font = self.GetFont()
956 edit_font.SetPointSize(pointSize = self.__non_edit_font.GetPointSize() + 1)
957 self.SetFont(edit_font)
958 self.Refresh()
959
960 # notify interested parties
961 for callback in self._on_set_focus_callbacks:
962 callback()
963
964 self.__timer.Start(oneShot = True, milliseconds = self.picklist_delay)
965 return True
966 #--------------------------------------------------------
968 """Do stuff when leaving the control.
969
970 The user has had her say, so don't second guess
971 intentions but do report error conditions.
972 """
973 self._has_focus = False
974
975 # don't need timer and pick list anymore
976 self.__timer.Stop()
977 self._hide_picklist()
978
979 # unset selection
980 self.SetSelection(1,1)
981
982 self.SetFont(self.__non_edit_font)
983 self.Refresh()
984
985 is_valid = True
986
987 # the user may have typed a phrase that is an exact match,
988 # however, just typing it won't associate data from the
989 # picklist, so do that now
990 if self.data is None:
991 val = self.GetValue().strip()
992 if val != u'':
993 self.__update_matches_in_picklist()
994 for match in self.__current_matches:
995 if match['label'] == val:
996 self.data = match['data']
997 self.MarkDirty()
998 break
999
1000 # no exact match found
1001 if self.data is None:
1002 if self.selection_only:
1003 wx.lib.pubsub.Publisher().sendMessage (
1004 topic = 'statustext',
1005 data = {'msg': self.selection_only_error_msg}
1006 )
1007 is_valid = False
1008
1009 # check value against final_regex if any given
1010 if self.__final_regex.match(self.GetValue().strip()) is None:
1011 wx.lib.pubsub.Publisher().sendMessage (
1012 topic = 'statustext',
1013 data = {'msg': self.final_regex_error_msg}
1014 )
1015 is_valid = False
1016
1017 self.display_as_valid(valid = is_valid)
1018
1019 # notify interested parties
1020 for callback in self._on_lose_focus_callbacks:
1021 callback()
1022
1023 event.Skip()
1024 return True
1025 #----------------------------------------------------
1029 #--------------------------------------------------------
1030 # MAIN
1031 #--------------------------------------------------------
1032 if __name__ == '__main__':
1033
1034 if len(sys.argv) < 2:
1035 sys.exit()
1036
1037 if sys.argv[1] != u'test':
1038 sys.exit()
1039
1040 from Gnumed.pycommon import gmI18N
1041 gmI18N.activate_locale()
1042 gmI18N.install_domain(domain='gnumed')
1043
1044 from Gnumed.pycommon import gmPG2, gmMatchProvider
1045
1046 prw = None
1047 #--------------------------------------------------------
1049 print "got focus:"
1050 print "value:", prw.GetValue()
1051 print "data :", prw.GetData()
1052 return True
1053 #--------------------------------------------------------
1055 print "lost focus:"
1056 print "value:", prw.GetValue()
1057 print "data :", prw.GetData()
1058 return True
1059 #--------------------------------------------------------
1061 print "modified:"
1062 print "value:", prw.GetValue()
1063 print "data :", prw.GetData()
1064 return True
1065 #--------------------------------------------------------
1067 print "selected:"
1068 print "value:", prw.GetValue()
1069 print "data :", prw.GetData()
1070 return True
1071 #--------------------------------------------------------
1073 app = wx.PyWidgetTester(size = (200, 50))
1074
1075 items = [ {'data':1, 'label':"Bloggs"},
1076 {'data':2, 'label':"Baker"},
1077 {'data':3, 'label':"Jones"},
1078 {'data':4, 'label':"Judson"},
1079 {'data':5, 'label':"Jacobs"},
1080 {'data':6, 'label':"Judson-Jacobs"}
1081 ]
1082
1083 mp = gmMatchProvider.cMatchProvider_FixedList(items)
1084 # do NOT treat "-" as a word separator here as there are names like "asa-sismussen"
1085 mp.word_separators = '[ \t=+&:@]+'
1086 global prw
1087 prw = cPhraseWheel(parent = app.frame, id = -1)
1088 prw.matcher = mp
1089 prw.capitalisation_mode = gmTools.CAPS_NAMES
1090 prw.add_callback_on_set_focus(callback=display_values_set_focus)
1091 prw.add_callback_on_modified(callback=display_values_modified)
1092 prw.add_callback_on_lose_focus(callback=display_values_lose_focus)
1093 prw.add_callback_on_selection(callback=display_values_selected)
1094
1095 app.frame.Show(True)
1096 app.MainLoop()
1097
1098 return True
1099 #--------------------------------------------------------
1101 print "Do you want to test the database connected phrase wheel ?"
1102 yes_no = raw_input('y/n: ')
1103 if yes_no != 'y':
1104 return True
1105
1106 gmPG2.get_connection()
1107 # FIXME: add callbacks
1108 # FIXME: add context
1109 query = u'select code, name from dem.country where _(name) %(fragment_condition)s'
1110 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query])
1111 app = wx.PyWidgetTester(size = (200, 50))
1112 global prw
1113 prw = cPhraseWheel(parent = app.frame, id = -1)
1114 prw.matcher = mp
1115
1116 app.frame.Show(True)
1117 app.MainLoop()
1118
1119 return True
1120 #--------------------------------------------------------
1122 gmPG2.get_connection()
1123 query = u"select pk_identity, firstnames || ' ' || lastnames || ' ' || dob::text as pat_name from dem.v_basic_person where firstnames || lastnames %(fragment_condition)s"
1124
1125 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query])
1126 app = wx.PyWidgetTester(size = (200, 50))
1127 global prw
1128 prw = cPhraseWheel(parent = app.frame, id = -1)
1129 prw.matcher = mp
1130
1131 app.frame.Show(True)
1132 app.MainLoop()
1133
1134 return True
1135 #--------------------------------------------------------
1137 app = wx.PyWidgetTester(size = (200, 50))
1138
1139 global prw
1140 prw = cPhraseWheel(parent = app.frame, id = -1)
1141
1142 prw.add_callback_on_set_focus(callback=display_values_set_focus)
1143 prw.add_callback_on_modified(callback=display_values_modified)
1144 prw.add_callback_on_lose_focus(callback=display_values_lose_focus)
1145 prw.add_callback_on_selection(callback=display_values_selected)
1146
1147 prw.enable_default_spellchecker()
1148
1149 app.frame.Show(True)
1150 app.MainLoop()
1151
1152 return True
1153 #--------------------------------------------------------
1154 # test_prw_fixed_list()
1155 # test_prw_sql2()
1156 test_spell_checking_prw()
1157 # test_prw_patients()
1158
1159 #==================================================
1160
| Home | Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Wed May 4 03:58:44 2011 | http://epydoc.sourceforge.net |