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