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 -class cPhraseWheel(wx.TextCtrl):
161 """Widget for smart guessing of user fields, after Richard Terry's interface. 162 163 - VB implementation by Richard Terry 164 - Python port by Ian Haywood for GNUmed 165 - enhanced by Karsten Hilbert for GNUmed 166 - enhanced by Ian Haywood for aumed 167 - enhanced by Karsten Hilbert for GNUmed 168 169 @param matcher: a class used to find matches for the current input 170 @type matcher: a L{match provider<Gnumed.pycommon.gmMatchProvider.cMatchProvider>} 171 instance or C{None} 172 173 @param selection_only: whether free-text can be entered without associated data 174 @type selection_only: boolean 175 176 @param capitalisation_mode: how to auto-capitalize input, valid values 177 are found in L{capitalize()<Gnumed.pycommon.gmTools.capitalize>} 178 @type capitalisation_mode: integer 179 180 @param accepted_chars: a regex pattern defining the characters 181 acceptable in the input string, if None no checking is performed 182 @type accepted_chars: None or a string holding a valid regex pattern 183 184 @param final_regex: when the control loses focus the input is 185 checked against this regular expression 186 @type final_regex: a string holding a valid regex pattern 187 188 @param phrase_separators: if not None, input is split into phrases 189 at boundaries defined by this regex and matching/spellchecking 190 is performed on the phrase the cursor is in only 191 @type phrase_separators: None or a string holding a valid regex pattern 192 193 @param navigate_after_selection: whether or not to immediately 194 navigate to the widget next-in-tab-order after selecting an 195 item from the dropdown picklist 196 @type navigate_after_selection: boolean 197 198 @param speller: if not None used to spellcheck the current input 199 and to retrieve suggested replacements/completions 200 @type speller: None or a L{enchant Dict<enchant>} descendant 201 202 @param picklist_delay: this much time of user inactivity must have 203 passed before the input related smarts kick in and the drop 204 down pick list is shown 205 @type picklist_delay: integer (milliseconds) 206 """
207 - def __init__ (self, parent=None, id=-1, value='', *args, **kwargs):
208 209 # behaviour 210 self.matcher = None 211 self.selection_only = False 212 self.selection_only_error_msg = _('You must select a value from the picklist or type an exact match.') 213 self.capitalisation_mode = gmTools.CAPS_NONE 214 self.accepted_chars = None 215 self.final_regex = '.*' 216 self.final_regex_error_msg = _('The content is invalid. It must match the regular expression: [%%s]. <%s>') % self.__class__.__name__ 217 self.phrase_separators = default_phrase_separators 218 self.navigate_after_selection = False 219 self.speller = None 220 self.speller_word_separators = default_spelling_word_separators 221 self.picklist_delay = 150 # milliseconds 222 223 # state tracking 224 self._has_focus = False 225 self.suppress_text_update_smarts = False 226 self.__current_matches = [] 227 self._screenheight = wx.SystemSettings.GetMetric(wx.SYS_SCREEN_Y) 228 self.input2match = '' 229 self.left_part = '' 230 self.right_part = '' 231 self.data = None 232 233 self._on_selection_callbacks = [] 234 self._on_lose_focus_callbacks = [] 235 self._on_set_focus_callbacks = [] 236 self._on_modified_callbacks = [] 237 238 try: 239 kwargs['style'] = kwargs['style'] | wx.TE_PROCESS_TAB 240 except KeyError: 241 kwargs['style'] = wx.TE_PROCESS_TAB 242 wx.TextCtrl.__init__(self, parent, id, **kwargs) 243 244 self.__non_edit_font = self.GetFont() 245 self.__color_valid = self.GetBackgroundColour() 246 global color_prw_valid 247 if color_prw_valid is None: 248 color_prw_valid = wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW) 249 250 self.__init_dropdown(parent = parent) 251 self.__register_events() 252 self.__init_timer()
253 #-------------------------------------------------------- 254 # external API 255 #--------------------------------------------------------
256 - def add_callback_on_selection(self, callback=None):
257 """ 258 Add a callback for invocation when a picklist item is selected. 259 260 The callback will be invoked whenever an item is selected 261 from the picklist. The associated data is passed in as 262 a single parameter. Callbacks must be able to cope with 263 None as the data parameter as that is sent whenever the 264 user changes a previously selected value. 265 """ 266 if not callable(callback): 267 raise ValueError('[add_callback_on_selection]: ignoring callback [%s], it is not callable' % callback) 268 269 self._on_selection_callbacks.append(callback)
270 #---------------------------------------------------------
271 - def add_callback_on_set_focus(self, callback=None):
272 """ 273 Add a callback for invocation when getting focus. 274 """ 275 if not callable(callback): 276 raise ValueError('[add_callback_on_set_focus]: ignoring callback [%s] - not callable' % callback) 277 278 self._on_set_focus_callbacks.append(callback)
279 #---------------------------------------------------------
280 - def add_callback_on_lose_focus(self, callback=None):
281 """ 282 Add a callback for invocation when losing focus. 283 """ 284 if not callable(callback): 285 raise ValueError('[add_callback_on_lose_focus]: ignoring callback [%s] - not callable' % callback) 286 287 self._on_lose_focus_callbacks.append(callback)
288 #---------------------------------------------------------
289 - def add_callback_on_modified(self, callback=None):
290 """ 291 Add a callback for invocation when the content is modified. 292 """ 293 if not callable(callback): 294 raise ValueError('[add_callback_on_modified]: ignoring callback [%s] - not callable' % callback) 295 296 self._on_modified_callbacks.append(callback)
297 #---------------------------------------------------------
298 - def SetData(self, data=None):
299 """ 300 Set the data and thereby set the value, too. 301 302 If you call SetData() you better be prepared 303 doing a scan of the entire potential match space. 304 305 The whole thing will only work if data is found 306 in the match space anyways. 307 """ 308 if self.matcher is None: 309 matched, matches = (False, []) 310 else: 311 matched, matches = self.matcher.getMatches('*') 312 313 if self.selection_only: 314 if not matched or (len(matches) == 0): 315 return False 316 317 for match in matches: 318 if match['data'] == data: 319 self.display_as_valid(valid = True) 320 self.suppress_text_update_smarts = True 321 wx.TextCtrl.SetValue(self, match['label']) 322 self.data = data 323 return True 324 325 # no match found ... 326 if self.selection_only: 327 return False 328 329 self.data = data 330 self.display_as_valid(valid = True) 331 return True
332 #---------------------------------------------------------
333 - def GetData(self, can_create=False, as_instance=False):
334 """Retrieve the data associated with the displayed string. 335 336 _create_data() must set self.data if possible 337 """ 338 if self.data is None: 339 if can_create: 340 self._create_data() 341 342 if self.data is not None: 343 if as_instance: 344 return self._data2instance() 345 346 return self.data
347 #---------------------------------------------------------
348 - def SetText(self, value=u'', data=None, suppress_smarts=False):
349 350 self.suppress_text_update_smarts = suppress_smarts 351 352 if data is not None: 353 self.suppress_text_update_smarts = True 354 self.data = data 355 if value is None: 356 value = u'' 357 wx.TextCtrl.SetValue(self, value) 358 self.display_as_valid(valid = True) 359 360 # if data already available 361 if self.data is not None: 362 return True 363 364 if value == u'' and not self.selection_only: 365 return True 366 367 # or try to find data from matches 368 if self.matcher is None: 369 stat, matches = (False, []) 370 else: 371 stat, matches = self.matcher.getMatches(aFragment = value) 372 373 for match in matches: 374 if match['label'] == value: 375 self.data = match['data'] 376 return True 377 378 # not found 379 if self.selection_only: 380 self.display_as_valid(valid = False) 381 return False 382 383 return True
384 #--------------------------------------------------------
385 - def set_context(self, context=None, val=None):
386 if self.matcher is not None: 387 self.matcher.set_context(context=context, val=val)
388 #---------------------------------------------------------
389 - def unset_context(self, context=None):
390 if self.matcher is not None: 391 self.matcher.unset_context(context=context)
392 #--------------------------------------------------------
394 # FIXME: use Debian's wgerman-medical as "personal" wordlist if available 395 try: 396 import enchant 397 except ImportError: 398 self.speller = None 399 return False 400 try: 401 self.speller = enchant.DictWithPWL(None, os.path.expanduser(os.path.join('~', '.gnumed', 'spellcheck', 'wordlist.pwl'))) 402 except enchant.DictNotFoundError: 403 self.speller = None 404 return False 405 return True
406 #--------------------------------------------------------
407 - def display_as_valid(self, valid=None):
408 if valid is True: 409 self.SetBackgroundColour(self.__color_valid) 410 elif valid is False: 411 self.SetBackgroundColour(color_prw_invalid) 412 else: 413 raise ArgumentError(u'<valid> must be True or False') 414 self.Refresh()
415 #-------------------------------------------------------- 416 # internal API 417 #-------------------------------------------------------- 418 # picklist handling 419 #--------------------------------------------------------
420 - def __init_dropdown(self, parent = None):
421 szr_dropdown = None 422 try: 423 #raise NotImplementedError # for testing 424 self.__dropdown_needs_relative_position = False 425 self.__picklist_dropdown = wx.PopupWindow(parent) 426 list_parent = self.__picklist_dropdown 427 self.__use_fake_popup = False 428 except NotImplementedError: 429 self.__use_fake_popup = True 430 431 # on MacOSX wx.PopupWindow is not implemented, so emulate it 432 add_picklist_to_sizer = True 433 szr_dropdown = wx.BoxSizer(wx.VERTICAL) 434 435 # using wx.MiniFrame 436 self.__dropdown_needs_relative_position = False 437 self.__picklist_dropdown = wx.MiniFrame ( 438 parent = parent, 439 id = -1, 440 style = wx.SIMPLE_BORDER | wx.FRAME_FLOAT_ON_PARENT | wx.FRAME_NO_TASKBAR | wx.POPUP_WINDOW 441 ) 442 scroll_win = wx.ScrolledWindow(parent = self.__picklist_dropdown, style = wx.NO_BORDER) 443 scroll_win.SetSizer(szr_dropdown) 444 list_parent = scroll_win 445 446 # using wx.Window 447 #self.__dropdown_needs_relative_position = True 448 #self.__picklist_dropdown = wx.ScrolledWindow(parent=parent, style = wx.RAISED_BORDER) 449 #self.__picklist_dropdown.SetSizer(szr_dropdown) 450 #list_parent = self.__picklist_dropdown 451 452 self.mac_log('dropdown parent: %s' % self.__picklist_dropdown.GetParent()) 453 454 # FIXME: support optional headers 455 # if kwargs['show_list_headers']: 456 # flags = 0 457 # else: 458 # flags = wx.LC_NO_HEADER 459 self._picklist = cPhraseWheelListCtrl ( 460 list_parent, 461 style = wx.LC_NO_HEADER 462 ) 463 self._picklist.InsertColumn(0, '') 464 465 if szr_dropdown is not None: 466 szr_dropdown.Add(self._picklist, 1, wx.EXPAND) 467 468 self.__picklist_dropdown.Hide()
469 #--------------------------------------------------------
470 - def _show_picklist(self):
471 """Display the pick list.""" 472 473 border_width = 4 474 extra_height = 25 475 476 self.__picklist_dropdown.Hide() 477 478 # this helps if the current input was already selected from the 479 # list but still is the substring of another pick list item 480 if self.data is not None: 481 return 482 483 if not self._has_focus: 484 return 485 486 if len(self.__current_matches) == 0: 487 return 488 489 # if only one match and text == match 490 if len(self.__current_matches) == 1: 491 if self.__current_matches[0]['label'] == self.input2match: 492 self.data = self.__current_matches[0]['data'] 493 return 494 495 # recalculate size 496 rows = len(self.__current_matches) 497 if rows < 2: # 2 rows minimum 498 rows = 2 499 if rows > 20: # 20 rows maximum 500 rows = 20 501 self.mac_log('dropdown needs rows: %s' % rows) 502 dropdown_size = self.__picklist_dropdown.GetSize() 503 pw_size = self.GetSize() 504 dropdown_size.SetWidth(pw_size.width) 505 dropdown_size.SetHeight ( 506 (pw_size.height * rows) 507 + border_width 508 + extra_height 509 ) 510 511 # recalculate position 512 (pw_x_abs, pw_y_abs) = self.ClientToScreenXY(0,0) 513 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))) 514 dropdown_new_x = pw_x_abs 515 dropdown_new_y = pw_y_abs + pw_size.height 516 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))) 517 self.mac_log('desired dropdown size: %s' % dropdown_size) 518 519 # reaches beyond screen ? 520 if (dropdown_new_y + dropdown_size.height) > self._screenheight: 521 self.mac_log('dropdown extends offscreen (screen max y: %s)' % self._screenheight) 522 max_height = self._screenheight - dropdown_new_y - 4 523 self.mac_log('max dropdown height would be: %s' % max_height) 524 if max_height > ((pw_size.height * 2) + 4): 525 dropdown_size.SetHeight(max_height) 526 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))) 527 self.mac_log('possible dropdown size: %s' % dropdown_size) 528 529 # now set dimensions 530 self.__picklist_dropdown.SetSize(dropdown_size) 531 self._picklist.SetSize(self.__picklist_dropdown.GetClientSize()) 532 self.mac_log('pick list size set to: %s' % self.__picklist_dropdown.GetSize()) 533 if self.__dropdown_needs_relative_position: 534 dropdown_new_x, dropdown_new_y = self.__picklist_dropdown.GetParent().ScreenToClientXY(dropdown_new_x, dropdown_new_y) 535 self.__picklist_dropdown.MoveXY(dropdown_new_x, dropdown_new_y) 536 537 # select first value 538 self._picklist.Select(0) 539 540 # and show it 541 self.__picklist_dropdown.Show(True) 542 543 dd_tl = self.__picklist_dropdown.ClientToScreenXY(0,0) 544 dd_size = self.__picklist_dropdown.GetSize() 545 dd_br = self.__picklist_dropdown.ClientToScreenXY(dd_size.width, dd_size.height) 546 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]))
547 #--------------------------------------------------------
548 - def _hide_picklist(self):
549 """Hide the pick list.""" 550 self.__picklist_dropdown.Hide() # dismiss the dropdown list window
551 #--------------------------------------------------------
552 - def __select_picklist_row(self, new_row_idx=None, old_row_idx=None):
553 if old_row_idx is not None: 554 pass # FIXME: do we need unselect here ? Select() should do it for us 555 self._picklist.Select(new_row_idx) 556 self._picklist.EnsureVisible(new_row_idx)
557 #---------------------------------------------------------
558 - def __update_matches_in_picklist(self, val=None):
559 """Get the matches for the currently typed input fragment.""" 560 561 self.input2match = val 562 if self.input2match is None: 563 if self.__phrase_separators is None: 564 self.input2match = self.GetValue().strip() 565 else: 566 # get current(ly relevant part of) input 567 entire_input = self.GetValue() 568 cursor_pos = self.GetInsertionPoint() 569 left_of_cursor = entire_input[:cursor_pos] 570 right_of_cursor = entire_input[cursor_pos:] 571 left_boundary = self.__phrase_separators.search(left_of_cursor) 572 if left_boundary is not None: 573 phrase_start = left_boundary.end() 574 else: 575 phrase_start = 0 576 self.left_part = entire_input[:phrase_start] 577 # find next phrase separator after cursor position 578 right_boundary = self.__phrase_separators.search(right_of_cursor) 579 if right_boundary is not None: 580 phrase_end = cursor_pos + (right_boundary.start() - 1) 581 else: 582 phrase_end = len(entire_input) - 1 583 self.right_part = entire_input[phrase_end+1:] 584 self.input2match = entire_input[phrase_start:phrase_end+1] 585 586 # get all currently matching items 587 if self.matcher is not None: 588 matched, self.__current_matches = self.matcher.getMatches(self.input2match) 589 self._picklist.SetItems(self.__current_matches) 590 591 # no matches found: might simply be due to a typo, so spellcheck 592 if len(self.__current_matches) == 0: 593 if self.speller is not None: 594 # filter out the last word 595 word = regex.split(self.__speller_word_separators, self.input2match)[-1] 596 if word.strip() != u'': 597 success = False 598 try: 599 success = self.speller.check(word) 600 except: 601 _log.exception('had to disable enchant spell checker') 602 self.speller = None 603 if success: 604 spells = self.speller.suggest(word) 605 truncated_input2match = self.input2match[:self.input2match.rindex(word)] 606 for spell in spells: 607 self.__current_matches.append({'label': truncated_input2match + spell, 'data': None}) 608 self._picklist.SetItems(self.__current_matches)
609 #--------------------------------------------------------
611 return self._picklist.GetItemText(self._picklist.GetFirstSelected())
612 #-------------------------------------------------------- 613 # internal helpers: GUI 614 #--------------------------------------------------------
615 - def _on_enter(self):
616 """Called when the user pressed <ENTER>.""" 617 if self.__picklist_dropdown.IsShown(): 618 self._on_list_item_selected() 619 else: 620 # FIXME: check for errors before navigation 621 self.Navigate()
622 #--------------------------------------------------------
623 - def __on_cursor_down(self):
624 625 if self.__picklist_dropdown.IsShown(): 626 selected = self._picklist.GetFirstSelected() 627 if selected < (len(self.__current_matches) - 1): 628 self.__select_picklist_row(selected+1, selected) 629 630 # if we don't yet have a pick list: open new pick list 631 # (this can happen when we TAB into a field pre-filled 632 # with the top-weighted contextual data but want to 633 # select another contextual item) 634 else: 635 self.__timer.Stop() 636 if self.GetValue().strip() == u'': 637 self.__update_matches_in_picklist(val='*') 638 else: 639 self.__update_matches_in_picklist() 640 self._show_picklist()
641 #--------------------------------------------------------
642 - def __on_cursor_up(self):
643 if self.__picklist_dropdown.IsShown(): 644 selected = self._picklist.GetFirstSelected() 645 if selected > 0: 646 self.__select_picklist_row(selected-1, selected) 647 else: 648 # FIXME: input history ? 649 pass
650 #--------------------------------------------------------
651 - def __on_tab(self):
652 """Under certain circumstances takes special action on TAB. 653 654 returns: 655 True: TAB was handled 656 False: TAB was not handled 657 """ 658 if not self.__picklist_dropdown.IsShown(): 659 return False 660 661 if len(self.__current_matches) != 1: 662 return False 663 664 if not self.selection_only: 665 return False 666 667 self.__select_picklist_row(new_row_idx=0) 668 self._on_list_item_selected() 669 670 return True
671 #-------------------------------------------------------- 672 # internal helpers: logic 673 #--------------------------------------------------------
674 - def _create_data(self):
675 raise NotImplementedError('[%s]: cannot create data object' % self.__class__.__name__)
676 #--------------------------------------------------------
677 - def __char_is_allowed(self, char=None):
678 # if undefined accept all chars 679 if self.accepted_chars is None: 680 return True 681 return (self.__accepted_chars.match(char) is not None)
682 #--------------------------------------------------------
683 - def _set_accepted_chars(self, accepted_chars=None):
684 if accepted_chars is None: 685 self.__accepted_chars = None 686 else: 687 self.__accepted_chars = regex.compile(accepted_chars)
688
689 - def _get_accepted_chars(self):
690 if self.__accepted_chars is None: 691 return None 692 return self.__accepted_chars.pattern
693 694 accepted_chars = property(_get_accepted_chars, _set_accepted_chars) 695 #--------------------------------------------------------
696 - def _set_final_regex(self, final_regex='.*'):
697 self.__final_regex = regex.compile(final_regex, flags = regex.LOCALE | regex.UNICODE)
698
699 - def _get_final_regex(self):
700 return self.__final_regex.pattern
701 702 final_regex = property(_get_final_regex, _set_final_regex) 703 #--------------------------------------------------------
704 - def _set_final_regex_error_msg(self, msg):
705 self.__final_regex_error_msg = msg % self.final_regex
706
707 - def _get_final_regex_error_msg(self):
708 return self.__final_regex_error_msg
709 710 final_regex_error_msg = property(_get_final_regex_error_msg, _set_final_regex_error_msg) 711 #--------------------------------------------------------
712 - def _set_phrase_separators(self, phrase_separators):
713 if phrase_separators is None: 714 self.__phrase_separators = None 715 else: 716 self.__phrase_separators = regex.compile(phrase_separators, flags = regex.LOCALE | regex.UNICODE)
717
718 - def _get_phrase_separators(self):
719 if self.__phrase_separators is None: 720 return None 721 return self.__phrase_separators.pattern
722 723 phrase_separators = property(_get_phrase_separators, _set_phrase_separators) 724 #--------------------------------------------------------
725 - def _set_speller_word_separators(self, word_separators):
726 if word_separators is None: 727 self.__speller_word_separators = regex.compile('[\W\d_]+', flags = regex.LOCALE | regex.UNICODE) 728 else: 729 self.__speller_word_separators = regex.compile(word_separators, flags = regex.LOCALE | regex.UNICODE)
730
732 return self.__speller_word_separators.pattern
733 734 speller_word_separators = property(_get_speller_word_separators, _set_speller_word_separators) 735 #--------------------------------------------------------
736 - def __init_timer(self):
737 self.__timer = _cPRWTimer() 738 self.__timer.callback = self._on_timer_fired 739 # initially stopped 740 self.__timer.Stop()
741 #--------------------------------------------------------
742 - def _on_timer_fired(self):
743 """Callback for delayed match retrieval timer. 744 745 if we end up here: 746 - delay has passed without user input 747 - the value in the input field has not changed since the timer started 748 """ 749 # update matches according to current input 750 self.__update_matches_in_picklist() 751 752 # we now have either: 753 # - all possible items (within reasonable limits) if input was '*' 754 # - all matching items 755 # - an empty match list if no matches were found 756 # also, our picklist is refilled and sorted according to weight 757 758 wx.CallAfter(self._show_picklist)
759 #-------------------------------------------------------- 760 # event handling 761 #--------------------------------------------------------
762 - def __register_events(self):
763 wx.EVT_TEXT(self, self.GetId(), self._on_text_update) 764 wx.EVT_KEY_DOWN (self, self._on_key_down) 765 wx.EVT_SET_FOCUS(self, self._on_set_focus) 766 wx.EVT_KILL_FOCUS(self, self._on_lose_focus) 767 self._picklist.Bind(wx.EVT_LEFT_DCLICK, self._on_list_item_selected)
768 #--------------------------------------------------------
769 - def _on_list_item_selected(self, *args, **kwargs):
770 """Gets called when user selected a list item.""" 771 772 self._hide_picklist() 773 self.display_as_valid(valid = True) 774 775 data = self._picklist.GetSelectedItemData() # just so that _picklist_selection2display_string can use it 776 if data is None: 777 return 778 779 self.data = data 780 781 # update our display 782 self.suppress_text_update_smarts = True 783 if self.__phrase_separators is not None: 784 wx.TextCtrl.SetValue(self, u'%s%s%s' % (self.left_part, self._picklist_selection2display_string(), self.right_part)) 785 else: 786 wx.TextCtrl.SetValue(self, self._picklist_selection2display_string()) 787 788 self.data = self._picklist.GetSelectedItemData() 789 self.MarkDirty() 790 791 # and tell the listeners about the user's selection 792 for callback in self._on_selection_callbacks: 793 callback(self.data) 794 795 if self.navigate_after_selection: 796 self.Navigate() 797 else: 798 self.SetInsertionPoint(self.GetLastPosition()) 799 800 return
801 #--------------------------------------------------------
802 - def _on_key_down(self, event):
803 """Is called when a key is pressed.""" 804 805 keycode = event.GetKeyCode() 806 807 if keycode == wx.WXK_DOWN: 808 self.__on_cursor_down() 809 return 810 811 if keycode == wx.WXK_UP: 812 self.__on_cursor_up() 813 return 814 815 if keycode == wx.WXK_RETURN: 816 self._on_enter() 817 return 818 819 if keycode == wx.WXK_TAB: 820 if event.ShiftDown(): 821 self.Navigate(flags = wx.NavigationKeyEvent.IsBackward) 822 return 823 self.__on_tab() 824 self.Navigate(flags = wx.NavigationKeyEvent.IsForward) 825 return 826 827 # FIXME: need PAGE UP/DOWN//POS1/END here to move in picklist 828 if keycode in [wx.WXK_SHIFT, wx.WXK_BACK, wx.WXK_DELETE, wx.WXK_LEFT, wx.WXK_RIGHT]: 829 pass 830 831 # need to handle all non-character key presses *before* this check 832 elif not self.__char_is_allowed(char = unichr(event.GetUnicodeKey())): 833 # FIXME: configure ? 834 wx.Bell() 835 # FIXME: display error message ? Richard doesn't ... 836 return 837 838 event.Skip() 839 return
840 #--------------------------------------------------------
841 - def _on_text_update (self, event):
842 """Internal handler for wx.EVT_TEXT. 843 844 Called when text was changed by user or SetValue(). 845 """ 846 if self.suppress_text_update_smarts: 847 self.suppress_text_update_smarts = False 848 return 849 850 self.data = None 851 self.__current_matches = [] 852 853 # if empty string then hide list dropdown window 854 # we also don't need a timer event then 855 val = self.GetValue().strip() 856 ins_point = self.GetInsertionPoint() 857 if val == u'': 858 self._hide_picklist() 859 self.__timer.Stop() 860 else: 861 new_val = gmTools.capitalize(text = val, mode = self.capitalisation_mode) 862 if new_val != val: 863 self.suppress_text_update_smarts = True 864 wx.TextCtrl.SetValue(self, new_val) 865 if ins_point > len(new_val): 866 self.SetInsertionPointEnd() 867 else: 868 self.SetInsertionPoint(ins_point) 869 # FIXME: SetSelection() ? 870 871 # start timer for delayed match retrieval 872 self.__timer.Start(oneShot = True, milliseconds = self.picklist_delay) 873 874 # notify interested parties 875 for callback in self._on_modified_callbacks: 876 callback() 877 878 return
879 #--------------------------------------------------------
880 - def _on_set_focus(self, event):
881 882 self._has_focus = True 883 event.Skip() 884 885 self.__non_edit_font = self.GetFont() 886 edit_font = self.GetFont() 887 edit_font.SetPointSize(pointSize = self.__non_edit_font.GetPointSize() + 1) 888 self.SetFont(edit_font) 889 self.Refresh() 890 891 # notify interested parties 892 for callback in self._on_set_focus_callbacks: 893 callback() 894 895 self.__timer.Start(oneShot = True, milliseconds = self.picklist_delay) 896 return True
897 #--------------------------------------------------------
898 - def _on_lose_focus(self, event):
899 """Do stuff when leaving the control. 900 901 The user has had her say, so don't second guess 902 intentions but do report error conditions. 903 """ 904 self._has_focus = False 905 906 # don't need timer and pick list anymore 907 self.__timer.Stop() 908 self._hide_picklist() 909 910 # unset selection 911 self.SetSelection(1,1) 912 913 self.SetFont(self.__non_edit_font) 914 self.Refresh() 915 916 is_valid = True 917 918 # the user may have typed a phrase that is an exact match, 919 # however, just typing it won't associate data from the 920 # picklist, so do that now 921 if self.data is None: 922 val = self.GetValue().strip() 923 if val != u'': 924 self.__update_matches_in_picklist() 925 for match in self.__current_matches: 926 if match['label'] == val: 927 self.data = match['data'] 928 self.MarkDirty() 929 break 930 931 # no exact match found 932 if self.data is None: 933 if self.selection_only: 934 wx.lib.pubsub.Publisher().sendMessage ( 935 topic = 'statustext', 936 data = {'msg': self.selection_only_error_msg} 937 ) 938 is_valid = False 939 940 # check value against final_regex if any given 941 if self.__final_regex.match(self.GetValue().strip()) is None: 942 wx.lib.pubsub.Publisher().sendMessage ( 943 topic = 'statustext', 944 data = {'msg': self.final_regex_error_msg} 945 ) 946 is_valid = False 947 948 self.display_as_valid(valid = is_valid) 949 950 # notify interested parties 951 for callback in self._on_lose_focus_callbacks: 952 callback() 953 954 event.Skip() 955 return True
956 #----------------------------------------------------
957 - def mac_log(self, msg):
958 if self.__use_fake_popup: 959 _log.debug(msg)
960 #-------------------------------------------------------- 961 # MAIN 962 #-------------------------------------------------------- 963 if __name__ == '__main__': 964 965 if len(sys.argv) < 2: 966 sys.exit() 967 968 if sys.argv[1] != u'test': 969 sys.exit() 970 971 from Gnumed.pycommon import gmI18N 972 gmI18N.activate_locale() 973 gmI18N.install_domain(domain='gnumed') 974 975 from Gnumed.pycommon import gmPG2, gmMatchProvider 976 977 prw = None 978 #--------------------------------------------------------
979 - def display_values_set_focus(*args, **kwargs):
980 print "got focus:" 981 print "value:", prw.GetValue() 982 print "data :", prw.GetData() 983 return True
984 #--------------------------------------------------------
985 - def display_values_lose_focus(*args, **kwargs):
986 print "lost focus:" 987 print "value:", prw.GetValue() 988 print "data :", prw.GetData() 989 return True
990 #--------------------------------------------------------
991 - def display_values_modified(*args, **kwargs):
992 print "modified:" 993 print "value:", prw.GetValue() 994 print "data :", prw.GetData() 995 return True
996 #--------------------------------------------------------
997 - def display_values_selected(*args, **kwargs):
998 print "selected:" 999 print "value:", prw.GetValue() 1000 print "data :", prw.GetData() 1001 return True
1002 #--------------------------------------------------------
1003 - def test_prw_fixed_list():
1004 app = wx.PyWidgetTester(size = (200, 50)) 1005 1006 items = [ {'data':1, 'label':"Bloggs"}, 1007 {'data':2, 'label':"Baker"}, 1008 {'data':3, 'label':"Jones"}, 1009 {'data':4, 'label':"Judson"}, 1010 {'data':5, 'label':"Jacobs"}, 1011 {'data':6, 'label':"Judson-Jacobs"} 1012 ] 1013 1014 mp = gmMatchProvider.cMatchProvider_FixedList(items) 1015 # do NOT treat "-" as a word separator here as there are names like "asa-sismussen" 1016 mp.word_separators = '[ \t=+&:@]+' 1017 global prw 1018 prw = cPhraseWheel(parent = app.frame, id = -1) 1019 prw.matcher = mp 1020 prw.capitalisation_mode = gmTools.CAPS_NAMES 1021 prw.add_callback_on_set_focus(callback=display_values_set_focus) 1022 prw.add_callback_on_modified(callback=display_values_modified) 1023 prw.add_callback_on_lose_focus(callback=display_values_lose_focus) 1024 prw.add_callback_on_selection(callback=display_values_selected) 1025 1026 app.frame.Show(True) 1027 app.MainLoop() 1028 1029 return True
1030 #--------------------------------------------------------
1031 - def test_prw_sql2():
1032 print "Do you want to test the database connected phrase wheel ?" 1033 yes_no = raw_input('y/n: ') 1034 if yes_no != 'y': 1035 return True 1036 1037 gmPG2.get_connection() 1038 # FIXME: add callbacks 1039 # FIXME: add context 1040 query = u'select code, name from dem.country where _(name) %(fragment_condition)s' 1041 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query]) 1042 app = wx.PyWidgetTester(size = (200, 50)) 1043 global prw 1044 prw = cPhraseWheel(parent = app.frame, id = -1) 1045 prw.matcher = mp 1046 1047 app.frame.Show(True) 1048 app.MainLoop() 1049 1050 return True
1051 #--------------------------------------------------------
1052 - def test_prw_patients():
1053 gmPG2.get_connection() 1054 query = u"select pk_identity, firstnames || ' ' || lastnames || ' ' || dob::text as pat_name from dem.v_basic_person where firstnames || lastnames %(fragment_condition)s" 1055 1056 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query]) 1057 app = wx.PyWidgetTester(size = (200, 50)) 1058 global prw 1059 prw = cPhraseWheel(parent = app.frame, id = -1) 1060 prw.matcher = mp 1061 1062 app.frame.Show(True) 1063 app.MainLoop() 1064 1065 return True
1066 #--------------------------------------------------------
1067 - def test_spell_checking_prw():
1068 app = wx.PyWidgetTester(size = (200, 50)) 1069 1070 global prw 1071 prw = cPhraseWheel(parent = app.frame, id = -1) 1072 1073 prw.add_callback_on_set_focus(callback=display_values_set_focus) 1074 prw.add_callback_on_modified(callback=display_values_modified) 1075 prw.add_callback_on_lose_focus(callback=display_values_lose_focus) 1076 prw.add_callback_on_selection(callback=display_values_selected) 1077 1078 prw.enable_default_spellchecker() 1079 1080 app.frame.Show(True) 1081 app.MainLoop() 1082 1083 return True
1084 #-------------------------------------------------------- 1085 # test_prw_fixed_list() 1086 # test_prw_sql2() 1087 test_spell_checking_prw() 1088 # test_prw_patients() 1089 1090 #================================================== 1091