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 selection-only-or-empty
162 -class cPhraseWheel(wx.TextCtrl):
163 """Widget for smart guessing of user fields, after Richard Terry's interface. 164 165 - VB implementation by Richard Terry 166 - Python port by Ian Haywood for GNUmed 167 - enhanced by Karsten Hilbert for GNUmed 168 - enhanced by Ian Haywood for aumed 169 - enhanced by Karsten Hilbert for GNUmed 170 171 @param matcher: a class used to find matches for the current input 172 @type matcher: a L{match provider<Gnumed.pycommon.gmMatchProvider.cMatchProvider>} 173 instance or C{None} 174 175 @param selection_only: whether free-text can be entered without associated data 176 @type selection_only: boolean 177 178 @param capitalisation_mode: how to auto-capitalize input, valid values 179 are found in L{capitalize()<Gnumed.pycommon.gmTools.capitalize>} 180 @type capitalisation_mode: integer 181 182 @param accepted_chars: a regex pattern defining the characters 183 acceptable in the input string, if None no checking is performed 184 @type accepted_chars: None or a string holding a valid regex pattern 185 186 @param final_regex: when the control loses focus the input is 187 checked against this regular expression 188 @type final_regex: a string holding a valid regex pattern 189 190 @param phrase_separators: if not None, input is split into phrases 191 at boundaries defined by this regex and matching/spellchecking 192 is performed on the phrase the cursor is in only 193 @type phrase_separators: None or a string holding a valid regex pattern 194 195 @param navigate_after_selection: whether or not to immediately 196 navigate to the widget next-in-tab-order after selecting an 197 item from the dropdown picklist 198 @type navigate_after_selection: boolean 199 200 @param speller: if not None used to spellcheck the current input 201 and to retrieve suggested replacements/completions 202 @type speller: None or a L{enchant Dict<enchant>} descendant 203 204 @param picklist_delay: this much time of user inactivity must have 205 passed before the input related smarts kick in and the drop 206 down pick list is shown 207 @type picklist_delay: integer (milliseconds) 208 """
209 - def __init__ (self, parent=None, id=-1, *args, **kwargs):
210 211 # 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.__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 (successful) 340 """ 341 if self.data is None: 342 if can_create: 343 self._create_data() 344 345 if self.data is not None: 346 if as_instance: 347 return self._data2instance() 348 349 return self.data
350 #---------------------------------------------------------
351 - def SetText(self, value=u'', data=None, suppress_smarts=False):
352 353 self.suppress_text_update_smarts = suppress_smarts 354 355 if data is not None: 356 self.suppress_text_update_smarts = True 357 self.data = data 358 if value is None: 359 value = u'' 360 wx.TextCtrl.SetValue(self, value) 361 self.display_as_valid(valid = True) 362 363 # 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 _get_data_tooltip(self):
681 # by default do not support dynamic tooltip parts 682 return None
683 #--------------------------------------------------------
684 - def __reset_tooltip(self):
685 """Calculate dynamic tooltip part based on data item. 686 687 - called via ._set_data() each time property .data (-> .__data) is set 688 - hence also called the first time data is set 689 - the static tooltip can be set any number of ways before that 690 - only when data is first set does the dynamic part become relevant 691 - hence it is sufficient to remember the static part when .data is 692 set for the first time 693 """ 694 if self.__static_tt is None: 695 if self.ToolTip is None: 696 self.__static_tt = u'' 697 else: 698 self.__static_tt = self.ToolTip.Tip 699 700 data_tt = self._get_data_tooltip() 701 if data_tt is None: 702 return 703 704 if self.__static_tt == u'': 705 tt = data_tt 706 else: 707 if data_tt.strip() == u'': 708 tt = self.__static_tt 709 else: 710 tt = u'%s\n\n--------------------------------\n\n%s' % ( 711 data_tt, 712 self.__static_tt 713 ) 714 self.SetToolTipString(tt)
715 #--------------------------------------------------------
716 - def __char_is_allowed(self, char=None):
717 # if undefined accept all chars 718 if self.accepted_chars is None: 719 return True 720 return (self.__accepted_chars.match(char) is not None)
721 #-------------------------------------------------------- 722 # properties 723 #--------------------------------------------------------
724 - def _get_data(self):
725 return self.__data
726
727 - def _set_data(self, data):
728 self.__data = data 729 self.__reset_tooltip()
730 731 data = property(_get_data, _set_data) 732 #--------------------------------------------------------
733 - def _set_accepted_chars(self, accepted_chars=None):
734 if accepted_chars is None: 735 self.__accepted_chars = None 736 else: 737 self.__accepted_chars = regex.compile(accepted_chars)
738
739 - def _get_accepted_chars(self):
740 if self.__accepted_chars is None: 741 return None 742 return self.__accepted_chars.pattern
743 744 accepted_chars = property(_get_accepted_chars, _set_accepted_chars) 745 #--------------------------------------------------------
746 - def _set_final_regex(self, final_regex='.*'):
747 self.__final_regex = regex.compile(final_regex, flags = regex.LOCALE | regex.UNICODE)
748
749 - def _get_final_regex(self):
750 return self.__final_regex.pattern
751 752 final_regex = property(_get_final_regex, _set_final_regex) 753 #--------------------------------------------------------
754 - def _set_final_regex_error_msg(self, msg):
755 self.__final_regex_error_msg = msg % self.final_regex
756
757 - def _get_final_regex_error_msg(self):
758 return self.__final_regex_error_msg
759 760 final_regex_error_msg = property(_get_final_regex_error_msg, _set_final_regex_error_msg) 761 #--------------------------------------------------------
762 - def _set_phrase_separators(self, phrase_separators):
763 if phrase_separators is None: 764 self.__phrase_separators = None 765 else: 766 self.__phrase_separators = regex.compile(phrase_separators, flags = regex.LOCALE | regex.UNICODE)
767
768 - def _get_phrase_separators(self):
769 if self.__phrase_separators is None: 770 return None 771 return self.__phrase_separators.pattern
772 773 phrase_separators = property(_get_phrase_separators, _set_phrase_separators) 774 #--------------------------------------------------------
775 - def _set_speller_word_separators(self, word_separators):
776 if word_separators is None: 777 self.__speller_word_separators = regex.compile('[\W\d_]+', flags = regex.LOCALE | regex.UNICODE) 778 else: 779 self.__speller_word_separators = regex.compile(word_separators, flags = regex.LOCALE | regex.UNICODE)
780
782 return self.__speller_word_separators.pattern
783 784 speller_word_separators = property(_get_speller_word_separators, _set_speller_word_separators) 785 #--------------------------------------------------------
786 - def __init_timer(self):
787 self.__timer = _cPRWTimer() 788 self.__timer.callback = self._on_timer_fired 789 # initially stopped 790 self.__timer.Stop()
791 #--------------------------------------------------------
792 - def _on_timer_fired(self):
793 """Callback for delayed match retrieval timer. 794 795 if we end up here: 796 - delay has passed without user input 797 - the value in the input field has not changed since the timer started 798 """ 799 # update matches according to current input 800 self.__update_matches_in_picklist() 801 802 # we now have either: 803 # - all possible items (within reasonable limits) if input was '*' 804 # - all matching items 805 # - an empty match list if no matches were found 806 # also, our picklist is refilled and sorted according to weight 807 808 wx.CallAfter(self._show_picklist)
809 #-------------------------------------------------------- 810 # event handling 811 #--------------------------------------------------------
812 - def __register_events(self):
813 wx.EVT_TEXT(self, self.GetId(), self._on_text_update) 814 wx.EVT_KEY_DOWN (self, self._on_key_down) 815 wx.EVT_SET_FOCUS(self, self._on_set_focus) 816 wx.EVT_KILL_FOCUS(self, self._on_lose_focus) 817 self._picklist.Bind(wx.EVT_LEFT_DCLICK, self._on_list_item_selected)
818 #--------------------------------------------------------
819 - def _on_list_item_selected(self, *args, **kwargs):
820 """Gets called when user selected a list item.""" 821 822 self._hide_picklist() 823 self.display_as_valid(valid = True) 824 825 data = self._picklist.GetSelectedItemData() # just so that _picklist_selection2display_string can use it 826 if data is None: 827 return 828 829 self.data = data 830 831 # update our display 832 self.suppress_text_update_smarts = True 833 if self.__phrase_separators is not None: 834 wx.TextCtrl.SetValue(self, u'%s%s%s' % (self.left_part, self._picklist_selection2display_string(), self.right_part)) 835 else: 836 wx.TextCtrl.SetValue(self, self._picklist_selection2display_string()) 837 838 self.data = self._picklist.GetSelectedItemData() 839 self.MarkDirty() 840 841 # and tell the listeners about the user's selection 842 for callback in self._on_selection_callbacks: 843 callback(self.data) 844 845 if self.navigate_after_selection: 846 self.Navigate() 847 else: 848 self.SetInsertionPoint(self.GetLastPosition()) 849 850 return
851 #--------------------------------------------------------
852 - def _on_key_down(self, event):
853 """Is called when a key is pressed.""" 854 855 keycode = event.GetKeyCode() 856 857 if keycode == wx.WXK_DOWN: 858 self.__on_cursor_down() 859 return 860 861 if keycode == wx.WXK_UP: 862 self.__on_cursor_up() 863 return 864 865 if keycode == wx.WXK_RETURN: 866 self._on_enter() 867 return 868 869 if keycode == wx.WXK_TAB: 870 if event.ShiftDown(): 871 self.Navigate(flags = wx.NavigationKeyEvent.IsBackward) 872 return 873 self.__on_tab() 874 self.Navigate(flags = wx.NavigationKeyEvent.IsForward) 875 return 876 877 # FIXME: need PAGE UP/DOWN//POS1/END here to move in picklist 878 if keycode in [wx.WXK_SHIFT, wx.WXK_BACK, wx.WXK_DELETE, wx.WXK_LEFT, wx.WXK_RIGHT]: 879 pass 880 881 # need to handle all non-character key presses *before* this check 882 elif not self.__char_is_allowed(char = unichr(event.GetUnicodeKey())): 883 # FIXME: configure ? 884 wx.Bell() 885 # FIXME: display error message ? Richard doesn't ... 886 return 887 888 event.Skip() 889 return
890 #--------------------------------------------------------
891 - def _on_text_update (self, event):
892 """Internal handler for wx.EVT_TEXT. 893 894 Called when text was changed by user or SetValue(). 895 """ 896 if self.suppress_text_update_smarts: 897 self.suppress_text_update_smarts = False 898 return 899 900 self.data = None 901 self.__current_matches = [] 902 903 # if empty string then hide list dropdown window 904 # we also don't need a timer event then 905 val = self.GetValue().strip() 906 ins_point = self.GetInsertionPoint() 907 if val == u'': 908 self._hide_picklist() 909 self.__timer.Stop() 910 else: 911 new_val = gmTools.capitalize(text = val, mode = self.capitalisation_mode) 912 if new_val != val: 913 self.suppress_text_update_smarts = True 914 wx.TextCtrl.SetValue(self, new_val) 915 if ins_point > len(new_val): 916 self.SetInsertionPointEnd() 917 else: 918 self.SetInsertionPoint(ins_point) 919 # FIXME: SetSelection() ? 920 921 # start timer for delayed match retrieval 922 self.__timer.Start(oneShot = True, milliseconds = self.picklist_delay) 923 924 # notify interested parties 925 for callback in self._on_modified_callbacks: 926 callback() 927 928 return
929 #--------------------------------------------------------
930 - def _on_set_focus(self, event):
931 932 self._has_focus = True 933 event.Skip() 934 935 self.__non_edit_font = self.GetFont() 936 edit_font = self.GetFont() 937 edit_font.SetPointSize(pointSize = self.__non_edit_font.GetPointSize() + 1) 938 self.SetFont(edit_font) 939 self.Refresh() 940 941 # notify interested parties 942 for callback in self._on_set_focus_callbacks: 943 callback() 944 945 self.__timer.Start(oneShot = True, milliseconds = self.picklist_delay) 946 return True
947 #--------------------------------------------------------
948 - def _on_lose_focus(self, event):
949 """Do stuff when leaving the control. 950 951 The user has had her say, so don't second guess 952 intentions but do report error conditions. 953 """ 954 self._has_focus = False 955 956 # don't need timer and pick list anymore 957 self.__timer.Stop() 958 self._hide_picklist() 959 960 # unset selection 961 self.SetSelection(1,1) 962 963 self.SetFont(self.__non_edit_font) 964 self.Refresh() 965 966 is_valid = True 967 968 # the user may have typed a phrase that is an exact match, 969 # however, just typing it won't associate data from the 970 # picklist, so do that now 971 if self.data is None: 972 val = self.GetValue().strip() 973 if val != u'': 974 self.__update_matches_in_picklist() 975 for match in self.__current_matches: 976 if match['label'] == val: 977 self.data = match['data'] 978 self.MarkDirty() 979 break 980 981 # no exact match found 982 if self.data is None: 983 if self.selection_only: 984 wx.lib.pubsub.Publisher().sendMessage ( 985 topic = 'statustext', 986 data = {'msg': self.selection_only_error_msg} 987 ) 988 is_valid = False 989 990 # check value against final_regex if any given 991 if self.__final_regex.match(self.GetValue().strip()) is None: 992 wx.lib.pubsub.Publisher().sendMessage ( 993 topic = 'statustext', 994 data = {'msg': self.final_regex_error_msg} 995 ) 996 is_valid = False 997 998 self.display_as_valid(valid = is_valid) 999 1000 # notify interested parties 1001 for callback in self._on_lose_focus_callbacks: 1002 callback() 1003 1004 event.Skip() 1005 return True
1006 #----------------------------------------------------
1007 - def mac_log(self, msg):
1008 if self.__use_fake_popup: 1009 _log.debug(msg)
1010 #-------------------------------------------------------- 1011 # MAIN 1012 #-------------------------------------------------------- 1013 if __name__ == '__main__': 1014 1015 if len(sys.argv) < 2: 1016 sys.exit() 1017 1018 if sys.argv[1] != u'test': 1019 sys.exit() 1020 1021 from Gnumed.pycommon import gmI18N 1022 gmI18N.activate_locale() 1023 gmI18N.install_domain(domain='gnumed') 1024 1025 from Gnumed.pycommon import gmPG2, gmMatchProvider 1026 1027 prw = None 1028 #--------------------------------------------------------
1029 - def display_values_set_focus(*args, **kwargs):
1030 print "got focus:" 1031 print "value:", prw.GetValue() 1032 print "data :", prw.GetData() 1033 return True
1034 #--------------------------------------------------------
1035 - def display_values_lose_focus(*args, **kwargs):
1036 print "lost focus:" 1037 print "value:", prw.GetValue() 1038 print "data :", prw.GetData() 1039 return True
1040 #--------------------------------------------------------
1041 - def display_values_modified(*args, **kwargs):
1042 print "modified:" 1043 print "value:", prw.GetValue() 1044 print "data :", prw.GetData() 1045 return True
1046 #--------------------------------------------------------
1047 - def display_values_selected(*args, **kwargs):
1048 print "selected:" 1049 print "value:", prw.GetValue() 1050 print "data :", prw.GetData() 1051 return True
1052 #--------------------------------------------------------
1053 - def test_prw_fixed_list():
1054 app = wx.PyWidgetTester(size = (200, 50)) 1055 1056 items = [ {'data':1, 'label':"Bloggs"}, 1057 {'data':2, 'label':"Baker"}, 1058 {'data':3, 'label':"Jones"}, 1059 {'data':4, 'label':"Judson"}, 1060 {'data':5, 'label':"Jacobs"}, 1061 {'data':6, 'label':"Judson-Jacobs"} 1062 ] 1063 1064 mp = gmMatchProvider.cMatchProvider_FixedList(items) 1065 # do NOT treat "-" as a word separator here as there are names like "asa-sismussen" 1066 mp.word_separators = '[ \t=+&:@]+' 1067 global prw 1068 prw = cPhraseWheel(parent = app.frame, id = -1) 1069 prw.matcher = mp 1070 prw.capitalisation_mode = gmTools.CAPS_NAMES 1071 prw.add_callback_on_set_focus(callback=display_values_set_focus) 1072 prw.add_callback_on_modified(callback=display_values_modified) 1073 prw.add_callback_on_lose_focus(callback=display_values_lose_focus) 1074 prw.add_callback_on_selection(callback=display_values_selected) 1075 1076 app.frame.Show(True) 1077 app.MainLoop() 1078 1079 return True
1080 #--------------------------------------------------------
1081 - def test_prw_sql2():
1082 print "Do you want to test the database connected phrase wheel ?" 1083 yes_no = raw_input('y/n: ') 1084 if yes_no != 'y': 1085 return True 1086 1087 gmPG2.get_connection() 1088 # FIXME: add callbacks 1089 # FIXME: add context 1090 query = u'select code, name from dem.country where _(name) %(fragment_condition)s' 1091 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query]) 1092 app = wx.PyWidgetTester(size = (200, 50)) 1093 global prw 1094 prw = cPhraseWheel(parent = app.frame, id = -1) 1095 prw.matcher = mp 1096 1097 app.frame.Show(True) 1098 app.MainLoop() 1099 1100 return True
1101 #--------------------------------------------------------
1102 - def test_prw_patients():
1103 gmPG2.get_connection() 1104 query = u"select pk_identity, firstnames || ' ' || lastnames || ' ' || dob::text as pat_name from dem.v_basic_person where firstnames || lastnames %(fragment_condition)s" 1105 1106 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query]) 1107 app = wx.PyWidgetTester(size = (200, 50)) 1108 global prw 1109 prw = cPhraseWheel(parent = app.frame, id = -1) 1110 prw.matcher = mp 1111 1112 app.frame.Show(True) 1113 app.MainLoop() 1114 1115 return True
1116 #--------------------------------------------------------
1117 - def test_spell_checking_prw():
1118 app = wx.PyWidgetTester(size = (200, 50)) 1119 1120 global prw 1121 prw = cPhraseWheel(parent = app.frame, id = -1) 1122 1123 prw.add_callback_on_set_focus(callback=display_values_set_focus) 1124 prw.add_callback_on_modified(callback=display_values_modified) 1125 prw.add_callback_on_lose_focus(callback=display_values_lose_focus) 1126 prw.add_callback_on_selection(callback=display_values_selected) 1127 1128 prw.enable_default_spellchecker() 1129 1130 app.frame.Show(True) 1131 app.MainLoop() 1132 1133 return True
1134 #-------------------------------------------------------- 1135 # test_prw_fixed_list() 1136 # test_prw_sql2() 1137 test_spell_checking_prw() 1138 # test_prw_patients() 1139 1140 #================================================== 1141