Package Gnumed :: Package wxpython :: Module gmPhraseWheel
[frames] | no frames]

Source Code for Module Gnumed.wxpython.gmPhraseWheel

   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  #============================================================ 
49 -def shutdown():
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 #------------------------------------------------------------
58 -class _cPRWTimer(wx.Timer):
59
60 - def __init__(self, *args, **kwargs):
61 wx.Timer.__init__(self, *args, **kwargs) 62 self.callback = lambda x:x 63 global _timers 64 _timers.append(self)
65
66 - def Notify(self):
67 self.callback()
68 #============================================================ 69 # FIXME: merge with gmListWidgets
70 -class cPhraseWheelListCtrl(wx.ListCtrl, listmixins.ListCtrlAutoWidthMixin):
71 - def __init__(self, *args, **kwargs):
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 #--------------------------------------------------------
78 - def SetItems(self, items):
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 #--------------------------------------------------------
85 - def GetSelectedItemData(self):
86 sel_idx = self.GetFirstSelected() 87 if sel_idx == -1: 88 return None 89 return self.__data[sel_idx]['data']
90 #--------------------------------------------------------
91 - def get_selected_item_label(self):
92 sel_idx = self.GetFirstSelected() 93 if sel_idx == -1: 94 return None 95 return self.__data[sel_idx]['label']
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 dynamic tooltip part based on selection 162 # FIXME: support selection-only-or-empty
163 -class cPhraseWheel(wx.TextCtrl):
164 """Widget for smart guessing of user fields, after Richard Terry's interface. 165 166 - VB implementation by Richard Terry 167 - Python port by Ian Haywood for GNUmed 168 - enhanced by Karsten Hilbert for GNUmed 169 - enhanced by Ian Haywood for aumed 170 - enhanced by Karsten Hilbert for GNUmed 171 172 @param matcher: a class used to find matches for the current input 173 @type matcher: a L{match provider<Gnumed.pycommon.gmMatchProvider.cMatchProvider>} 174 instance or C{None} 175 176 @param selection_only: whether free-text can be entered without associated data 177 @type selection_only: boolean 178 179 @param capitalisation_mode: how to auto-capitalize input, valid values 180 are found in L{capitalize()<Gnumed.pycommon.gmTools.capitalize>} 181 @type capitalisation_mode: integer 182 183 @param accepted_chars: a regex pattern defining the characters 184 acceptable in the input string, if None no checking is performed 185 @type accepted_chars: None or a string holding a valid regex pattern 186 187 @param final_regex: when the control loses focus the input is 188 checked against this regular expression 189 @type final_regex: a string holding a valid regex pattern 190 191 @param phrase_separators: if not None, input is split into phrases 192 at boundaries defined by this regex and matching/spellchecking 193 is performed on the phrase the cursor is in only 194 @type phrase_separators: None or a string holding a valid regex pattern 195 196 @param navigate_after_selection: whether or not to immediately 197 navigate to the widget next-in-tab-order after selecting an 198 item from the dropdown picklist 199 @type navigate_after_selection: boolean 200 201 @param speller: if not None used to spellcheck the current input 202 and to retrieve suggested replacements/completions 203 @type speller: None or a L{enchant Dict<enchant>} descendant 204 205 @param picklist_delay: this much time of user inactivity must have 206 passed before the input related smarts kick in and the drop 207 down pick list is shown 208 @type picklist_delay: integer (milliseconds) 209 """
210 - def __init__ (self, parent=None, id=-1, value='', *args, **kwargs):
211 212 # behaviour 213 self.matcher = None 214 self.selection_only = False 215 self.selection_only_error_msg = _('You must select a value from the picklist or type an exact match.') 216 self.capitalisation_mode = gmTools.CAPS_NONE 217 self.accepted_chars = None 218 self.final_regex = '.*' 219 self.final_regex_error_msg = _('The content is invalid. It must match the regular expression: [%%s]. <%s>') % self.__class__.__name__ 220 self.phrase_separators = default_phrase_separators 221 self.navigate_after_selection = False 222 self.speller = None 223 self.speller_word_separators = default_spelling_word_separators 224 self.picklist_delay = 150 # milliseconds 225 226 # state tracking 227 self._has_focus = False 228 self.suppress_text_update_smarts = False 229 self.__current_matches = [] 230 self._screenheight = wx.SystemSettings.GetMetric(wx.SYS_SCREEN_Y) 231 self.input2match = '' 232 self.left_part = '' 233 self.right_part = '' 234 self.data = None 235 236 self._on_selection_callbacks = [] 237 self._on_lose_focus_callbacks = [] 238 self._on_set_focus_callbacks = [] 239 self._on_modified_callbacks = [] 240 241 try: 242 kwargs['style'] = kwargs['style'] | wx.TE_PROCESS_TAB 243 except KeyError: 244 kwargs['style'] = wx.TE_PROCESS_TAB 245 wx.TextCtrl.__init__(self, parent, id, **kwargs) 246 247 self.__non_edit_font = self.GetFont() 248 self.__color_valid = self.GetBackgroundColour() 249 global color_prw_valid 250 if color_prw_valid is None: 251 color_prw_valid = wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW) 252 253 self.__init_dropdown(parent = parent) 254 self.__register_events() 255 self.__init_timer()
256 #-------------------------------------------------------- 257 # external API 258 #--------------------------------------------------------
259 - def add_callback_on_selection(self, callback=None):
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 #---------------------------------------------------------
274 - def add_callback_on_set_focus(self, callback=None):
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 #---------------------------------------------------------
283 - def add_callback_on_lose_focus(self, callback=None):
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 #---------------------------------------------------------
292 - def add_callback_on_modified(self, callback=None):
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 #---------------------------------------------------------
301 - def SetData(self, data=None):
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 # no match found ... 329 if self.selection_only: 330 return False 331 332 self.data = data 333 self.display_as_valid(valid = True) 334 return True
335 #---------------------------------------------------------
336 - def GetData(self, can_create=False, as_instance=False):
337 """Retrieve the data associated with the displayed string. 338 339 _create_data() must set self.data if possible 340 """ 341 if self.data is None: 342 if can_create: 343 self._create_data() 344 345 if self.data is not None: 346 if as_instance: 347 return self._data2instance() 348 349 return self.data
350 #---------------------------------------------------------
351 - def SetText(self, value=u'', data=None, suppress_smarts=False):
352 353 self.suppress_text_update_smarts = suppress_smarts 354 355 if data is not None: 356 self.suppress_text_update_smarts = True 357 self.data = data 358 if value is None: 359 value = u'' 360 wx.TextCtrl.SetValue(self, value) 361 self.display_as_valid(valid = True) 362 363 # if data already available 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 # or try to find data from matches 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 # not found 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 # FIXME: use Debian's wgerman-medical as "personal" wordlist if available 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 #--------------------------------------------------------
410 - def display_as_valid(self, valid=None):
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 # internal API 420 #-------------------------------------------------------- 421 # picklist handling 422 #--------------------------------------------------------
423 - def __init_dropdown(self, parent = None):
424 szr_dropdown = None 425 try: 426 #raise NotImplementedError # for testing 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 # on MacOSX wx.PopupWindow is not implemented, so emulate it 435 add_picklist_to_sizer = True 436 szr_dropdown = wx.BoxSizer(wx.VERTICAL) 437 438 # using wx.MiniFrame 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 # using wx.Window 450 #self.__dropdown_needs_relative_position = True 451 #self.__picklist_dropdown = wx.ScrolledWindow(parent=parent, style = wx.RAISED_BORDER) 452 #self.__picklist_dropdown.SetSizer(szr_dropdown) 453 #list_parent = self.__picklist_dropdown 454 455 self.mac_log('dropdown parent: %s' % self.__picklist_dropdown.GetParent()) 456 457 # FIXME: support optional headers 458 # if kwargs['show_list_headers']: 459 # flags = 0 460 # else: 461 # flags = wx.LC_NO_HEADER 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 #--------------------------------------------------------
473 - def _show_picklist(self):
474 """Display the pick list.""" 475 476 border_width = 4 477 extra_height = 25 478 479 self.__picklist_dropdown.Hide() 480 481 # this helps if the current input was already selected from the 482 # list but still is the substring of another pick list item 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 # if only one match and text == match 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 # recalculate size 499 rows = len(self.__current_matches) 500 if rows < 2: # 2 rows minimum 501 rows = 2 502 if rows > 20: # 20 rows maximum 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 # recalculate position 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 # reaches beyond screen ? 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 # now set dimensions 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 # select first value 541 self._picklist.Select(0) 542 543 # and show it 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 #--------------------------------------------------------
551 - def _hide_picklist(self):
552 """Hide the pick list.""" 553 self.__picklist_dropdown.Hide() # dismiss the dropdown list window
554 #--------------------------------------------------------
555 - def __select_picklist_row(self, new_row_idx=None, old_row_idx=None):
556 if old_row_idx is not None: 557 pass # FIXME: do we need unselect here ? Select() should do it for us 558 self._picklist.Select(new_row_idx) 559 self._picklist.EnsureVisible(new_row_idx)
560 #---------------------------------------------------------
561 - def __update_matches_in_picklist(self, val=None):
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 # get current(ly relevant part of) input 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 # find next phrase separator after cursor position 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 # get all currently matching items 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 # no matches found: might simply be due to a typo, so spellcheck 595 if len(self.__current_matches) == 0: 596 if self.speller is not None: 597 # filter out the last word 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 # internal helpers: GUI 617 #--------------------------------------------------------
618 - def _on_enter(self):
619 """Called when the user pressed <ENTER>.""" 620 if self.__picklist_dropdown.IsShown(): 621 self._on_list_item_selected() 622 else: 623 # FIXME: check for errors before navigation 624 self.Navigate()
625 #--------------------------------------------------------
626 - def __on_cursor_down(self):
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 # if we don't yet have a pick list: open new pick list 634 # (this can happen when we TAB into a field pre-filled 635 # with the top-weighted contextual data but want to 636 # select another contextual item) 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 #--------------------------------------------------------
645 - def __on_cursor_up(self):
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 # FIXME: input history ? 652 pass
653 #--------------------------------------------------------
654 - def __on_tab(self):
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 # internal helpers: logic 676 #--------------------------------------------------------
677 - def _create_data(self):
678 raise NotImplementedError('[%s]: cannot create data object' % self.__class__.__name__)
679 #--------------------------------------------------------
680 - def __char_is_allowed(self, char=None):
681 # if undefined accept all chars 682 if self.accepted_chars is None: 683 return True 684 return (self.__accepted_chars.match(char) is not None)
685 #--------------------------------------------------------
686 - def _set_accepted_chars(self, accepted_chars=None):
687 if accepted_chars is None: 688 self.__accepted_chars = None 689 else: 690 self.__accepted_chars = regex.compile(accepted_chars)
691
692 - def _get_accepted_chars(self):
693 if self.__accepted_chars is None: 694 return None 695 return self.__accepted_chars.pattern
696 697 accepted_chars = property(_get_accepted_chars, _set_accepted_chars) 698 #--------------------------------------------------------
699 - def _set_final_regex(self, final_regex='.*'):
700 self.__final_regex = regex.compile(final_regex, flags = regex.LOCALE | regex.UNICODE)
701
702 - def _get_final_regex(self):
703 return self.__final_regex.pattern
704 705 final_regex = property(_get_final_regex, _set_final_regex) 706 #--------------------------------------------------------
707 - def _set_final_regex_error_msg(self, msg):
708 self.__final_regex_error_msg = msg % self.final_regex
709
710 - def _get_final_regex_error_msg(self):
711 return self.__final_regex_error_msg
712 713 final_regex_error_msg = property(_get_final_regex_error_msg, _set_final_regex_error_msg) 714 #--------------------------------------------------------
715 - def _set_phrase_separators(self, phrase_separators):
716 if phrase_separators is None: 717 self.__phrase_separators = None 718 else: 719 self.__phrase_separators = regex.compile(phrase_separators, flags = regex.LOCALE | regex.UNICODE)
720
721 - def _get_phrase_separators(self):
722 if self.__phrase_separators is None: 723 return None 724 return self.__phrase_separators.pattern
725 726 phrase_separators = property(_get_phrase_separators, _set_phrase_separators) 727 #--------------------------------------------------------
728 - def _set_speller_word_separators(self, word_separators):
729 if word_separators is None: 730 self.__speller_word_separators = regex.compile('[\W\d_]+', flags = regex.LOCALE | regex.UNICODE) 731 else: 732 self.__speller_word_separators = regex.compile(word_separators, flags = regex.LOCALE | regex.UNICODE)
733
735 return self.__speller_word_separators.pattern
736 737 speller_word_separators = property(_get_speller_word_separators, _set_speller_word_separators) 738 #--------------------------------------------------------
739 - def __init_timer(self):
740 self.__timer = _cPRWTimer() 741 self.__timer.callback = self._on_timer_fired 742 # initially stopped 743 self.__timer.Stop()
744 #--------------------------------------------------------
745 - def _on_timer_fired(self):
746 """Callback for delayed match retrieval timer. 747 748 if we end up here: 749 - delay has passed without user input 750 - the value in the input field has not changed since the timer started 751 """ 752 # update matches according to current input 753 self.__update_matches_in_picklist() 754 755 # we now have either: 756 # - all possible items (within reasonable limits) if input was '*' 757 # - all matching items 758 # - an empty match list if no matches were found 759 # also, our picklist is refilled and sorted according to weight 760 761 wx.CallAfter(self._show_picklist)
762 #-------------------------------------------------------- 763 # event handling 764 #--------------------------------------------------------
765 - def __register_events(self):
766 wx.EVT_TEXT(self, self.GetId(), self._on_text_update) 767 wx.EVT_KEY_DOWN (self, self._on_key_down) 768 wx.EVT_SET_FOCUS(self, self._on_set_focus) 769 wx.EVT_KILL_FOCUS(self, self._on_lose_focus) 770 self._picklist.Bind(wx.EVT_LEFT_DCLICK, self._on_list_item_selected)
771 #--------------------------------------------------------
772 - def _on_list_item_selected(self, *args, **kwargs):
773 """Gets called when user selected a list item.""" 774 775 self._hide_picklist() 776 self.display_as_valid(valid = True) 777 778 data = self._picklist.GetSelectedItemData() # just so that _picklist_selection2display_string can use it 779 if data is None: 780 return 781 782 self.data = data 783 784 # update our display 785 self.suppress_text_update_smarts = True 786 if self.__phrase_separators is not None: 787 wx.TextCtrl.SetValue(self, u'%s%s%s' % (self.left_part, self._picklist_selection2display_string(), self.right_part)) 788 else: 789 wx.TextCtrl.SetValue(self, self._picklist_selection2display_string()) 790 791 self.data = self._picklist.GetSelectedItemData() 792 self.MarkDirty() 793 794 # and tell the listeners about the user's selection 795 for callback in self._on_selection_callbacks: 796 callback(self.data) 797 798 if self.navigate_after_selection: 799 self.Navigate() 800 else: 801 self.SetInsertionPoint(self.GetLastPosition()) 802 803 return
804 #--------------------------------------------------------
805 - def _on_key_down(self, event):
806 """Is called when a key is pressed.""" 807 808 keycode = event.GetKeyCode() 809 810 if keycode == wx.WXK_DOWN: 811 self.__on_cursor_down() 812 return 813 814 if keycode == wx.WXK_UP: 815 self.__on_cursor_up() 816 return 817 818 if keycode == wx.WXK_RETURN: 819 self._on_enter() 820 return 821 822 if keycode == wx.WXK_TAB: 823 if event.ShiftDown(): 824 self.Navigate(flags = wx.NavigationKeyEvent.IsBackward) 825 return 826 self.__on_tab() 827 self.Navigate(flags = wx.NavigationKeyEvent.IsForward) 828 return 829 830 # FIXME: need PAGE UP/DOWN//POS1/END here to move in picklist 831 if keycode in [wx.WXK_SHIFT, wx.WXK_BACK, wx.WXK_DELETE, wx.WXK_LEFT, wx.WXK_RIGHT]: 832 pass 833 834 # need to handle all non-character key presses *before* this check 835 elif not self.__char_is_allowed(char = unichr(event.GetUnicodeKey())): 836 # FIXME: configure ? 837 wx.Bell() 838 # FIXME: display error message ? Richard doesn't ... 839 return 840 841 event.Skip() 842 return
843 #--------------------------------------------------------
844 - def _on_text_update (self, event):
845 """Internal handler for wx.EVT_TEXT. 846 847 Called when text was changed by user or SetValue(). 848 """ 849 if self.suppress_text_update_smarts: 850 self.suppress_text_update_smarts = False 851 return 852 853 self.data = None 854 self.__current_matches = [] 855 856 # if empty string then hide list dropdown window 857 # we also don't need a timer event then 858 val = self.GetValue().strip() 859 ins_point = self.GetInsertionPoint() 860 if val == u'': 861 self._hide_picklist() 862 self.__timer.Stop() 863 else: 864 new_val = gmTools.capitalize(text = val, mode = self.capitalisation_mode) 865 if new_val != val: 866 self.suppress_text_update_smarts = True 867 wx.TextCtrl.SetValue(self, new_val) 868 if ins_point > len(new_val): 869 self.SetInsertionPointEnd() 870 else: 871 self.SetInsertionPoint(ins_point) 872 # FIXME: SetSelection() ? 873 874 # start timer for delayed match retrieval 875 self.__timer.Start(oneShot = True, milliseconds = self.picklist_delay) 876 877 # notify interested parties 878 for callback in self._on_modified_callbacks: 879 callback() 880 881 return
882 #--------------------------------------------------------
883 - def _on_set_focus(self, event):
884 885 self._has_focus = True 886 event.Skip() 887 888 self.__non_edit_font = self.GetFont() 889 edit_font = self.GetFont() 890 edit_font.SetPointSize(pointSize = self.__non_edit_font.GetPointSize() + 1) 891 self.SetFont(edit_font) 892 self.Refresh() 893 894 # notify interested parties 895 for callback in self._on_set_focus_callbacks: 896 callback() 897 898 self.__timer.Start(oneShot = True, milliseconds = self.picklist_delay) 899 return True
900 #--------------------------------------------------------
901 - def _on_lose_focus(self, event):
902 """Do stuff when leaving the control. 903 904 The user has had her say, so don't second guess 905 intentions but do report error conditions. 906 """ 907 self._has_focus = False 908 909 # don't need timer and pick list anymore 910 self.__timer.Stop() 911 self._hide_picklist() 912 913 # unset selection 914 self.SetSelection(1,1) 915 916 self.SetFont(self.__non_edit_font) 917 self.Refresh() 918 919 is_valid = True 920 921 # the user may have typed a phrase that is an exact match, 922 # however, just typing it won't associate data from the 923 # picklist, so do that now 924 if self.data is None: 925 val = self.GetValue().strip() 926 if val != u'': 927 self.__update_matches_in_picklist() 928 for match in self.__current_matches: 929 if match['label'] == val: 930 self.data = match['data'] 931 self.MarkDirty() 932 break 933 934 # no exact match found 935 if self.data is None: 936 if self.selection_only: 937 wx.lib.pubsub.Publisher().sendMessage ( 938 topic = 'statustext', 939 data = {'msg': self.selection_only_error_msg} 940 ) 941 is_valid = False 942 943 # check value against final_regex if any given 944 if self.__final_regex.match(self.GetValue().strip()) is None: 945 wx.lib.pubsub.Publisher().sendMessage ( 946 topic = 'statustext', 947 data = {'msg': self.final_regex_error_msg} 948 ) 949 is_valid = False 950 951 self.display_as_valid(valid = is_valid) 952 953 # notify interested parties 954 for callback in self._on_lose_focus_callbacks: 955 callback() 956 957 event.Skip() 958 return True
959 #----------------------------------------------------
960 - def mac_log(self, msg):
961 if self.__use_fake_popup: 962 _log.debug(msg)
963 #-------------------------------------------------------- 964 # MAIN 965 #-------------------------------------------------------- 966 if __name__ == '__main__': 967 968 if len(sys.argv) < 2: 969 sys.exit() 970 971 if sys.argv[1] != u'test': 972 sys.exit() 973 974 from Gnumed.pycommon import gmI18N 975 gmI18N.activate_locale() 976 gmI18N.install_domain(domain='gnumed') 977 978 from Gnumed.pycommon import gmPG2, gmMatchProvider 979 980 prw = None 981 #--------------------------------------------------------
982 - def display_values_set_focus(*args, **kwargs):
983 print "got focus:" 984 print "value:", prw.GetValue() 985 print "data :", prw.GetData() 986 return True
987 #--------------------------------------------------------
988 - def display_values_lose_focus(*args, **kwargs):
989 print "lost focus:" 990 print "value:", prw.GetValue() 991 print "data :", prw.GetData() 992 return True
993 #--------------------------------------------------------
994 - def display_values_modified(*args, **kwargs):
995 print "modified:" 996 print "value:", prw.GetValue() 997 print "data :", prw.GetData() 998 return True
999 #--------------------------------------------------------
1000 - def display_values_selected(*args, **kwargs):
1001 print "selected:" 1002 print "value:", prw.GetValue() 1003 print "data :", prw.GetData() 1004 return True
1005 #--------------------------------------------------------
1006 - def test_prw_fixed_list():
1007 app = wx.PyWidgetTester(size = (200, 50)) 1008 1009 items = [ {'data':1, 'label':"Bloggs"}, 1010 {'data':2, 'label':"Baker"}, 1011 {'data':3, 'label':"Jones"}, 1012 {'data':4, 'label':"Judson"}, 1013 {'data':5, 'label':"Jacobs"}, 1014 {'data':6, 'label':"Judson-Jacobs"} 1015 ] 1016 1017 mp = gmMatchProvider.cMatchProvider_FixedList(items) 1018 # do NOT treat "-" as a word separator here as there are names like "asa-sismussen" 1019 mp.word_separators = '[ \t=+&:@]+' 1020 global prw 1021 prw = cPhraseWheel(parent = app.frame, id = -1) 1022 prw.matcher = mp 1023 prw.capitalisation_mode = gmTools.CAPS_NAMES 1024 prw.add_callback_on_set_focus(callback=display_values_set_focus) 1025 prw.add_callback_on_modified(callback=display_values_modified) 1026 prw.add_callback_on_lose_focus(callback=display_values_lose_focus) 1027 prw.add_callback_on_selection(callback=display_values_selected) 1028 1029 app.frame.Show(True) 1030 app.MainLoop() 1031 1032 return True
1033 #--------------------------------------------------------
1034 - def test_prw_sql2():
1035 print "Do you want to test the database connected phrase wheel ?" 1036 yes_no = raw_input('y/n: ') 1037 if yes_no != 'y': 1038 return True 1039 1040 gmPG2.get_connection() 1041 # FIXME: add callbacks 1042 # FIXME: add context 1043 query = u'select code, name from dem.country where _(name) %(fragment_condition)s' 1044 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query]) 1045 app = wx.PyWidgetTester(size = (200, 50)) 1046 global prw 1047 prw = cPhraseWheel(parent = app.frame, id = -1) 1048 prw.matcher = mp 1049 1050 app.frame.Show(True) 1051 app.MainLoop() 1052 1053 return True
1054 #--------------------------------------------------------
1055 - def test_prw_patients():
1056 gmPG2.get_connection() 1057 query = u"select pk_identity, firstnames || ' ' || lastnames || ' ' || dob::text as pat_name from dem.v_basic_person where firstnames || lastnames %(fragment_condition)s" 1058 1059 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query]) 1060 app = wx.PyWidgetTester(size = (200, 50)) 1061 global prw 1062 prw = cPhraseWheel(parent = app.frame, id = -1) 1063 prw.matcher = mp 1064 1065 app.frame.Show(True) 1066 app.MainLoop() 1067 1068 return True
1069 #--------------------------------------------------------
1070 - def test_spell_checking_prw():
1071 app = wx.PyWidgetTester(size = (200, 50)) 1072 1073 global prw 1074 prw = cPhraseWheel(parent = app.frame, id = -1) 1075 1076 prw.add_callback_on_set_focus(callback=display_values_set_focus) 1077 prw.add_callback_on_modified(callback=display_values_modified) 1078 prw.add_callback_on_lose_focus(callback=display_values_lose_focus) 1079 prw.add_callback_on_selection(callback=display_values_selected) 1080 1081 prw.enable_default_spellchecker() 1082 1083 app.frame.Show(True) 1084 app.MainLoop() 1085 1086 return True
1087 #-------------------------------------------------------- 1088 # test_prw_fixed_list() 1089 # test_prw_sql2() 1090 test_spell_checking_prw() 1091 # test_prw_patients() 1092 1093 #================================================== 1094