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(candidate) 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 # FIXME: cols in pick list 927 # FIXME: snap_to_basename+set selection 928 # FIXME: learn() -> PWL 929 # FIXME: up-arrow: show recent (in-memory) history 930 #---------------------------------------------------------- 931 # ideas 932 #---------------------------------------------------------- 933 #- display possible completion but highlighted for deletion 934 #(- cycle through possible completions) 935 #- pre-fill selection with SELECT ... LIMIT 25 936 #- async threads for match retrieval instead of timer 937 # - on truncated results return item "..." -> selection forcefully retrieves all matches 938 939 #- generators/yield() 940 #- OnChar() - process a char event 941 942 # split input into words and match components against known phrases 943 944 # make special list window: 945 # - deletion of items 946 # - highlight matched parts 947 # - faster scrolling 948 # - wxEditableListBox ? 949 950 # - if non-learning (i.e. fast select only): autocomplete with match 951 # and move cursor to end of match 952 #----------------------------------------------------------------------------------------------- 953 # darn ! this clever hack won't work since we may have crossed a search location threshold 954 #---- 955 # #self.__prevFragment = "***********-very-unlikely--------------***************" 956 # #self.__prevMatches = [] # a list of tuples (ID, listbox name, weight) 957 # 958 # # is the current fragment just a longer version of the previous fragment ? 959 # if string.find(aFragment, self.__prevFragment) == 0: 960 # # we then need to search in the previous matches only 961 # for prevMatch in self.__prevMatches: 962 # if string.find(prevMatch[1], aFragment) == 0: 963 # matches.append(prevMatch) 964 # # remember current matches 965 # self.__prefMatches = matches 966 # # no matches found 967 # if len(matches) == 0: 968 # return [(1,_('*no matching items found*'),1)] 969 # else: 970 # return matches 971 #---- 972 #TODO: 973 # - see spincontrol for list box handling 974 # stop list (list of negatives): "an" -> "animal" but not "and" 975 #----- 976 #> > remember, you should be searching on either weighted data, or in some 977 #> > situations a start string search on indexed data 978 #> 979 #> Can you be a bit more specific on this ? 980 981 #seaching ones own previous text entered would usually be instring but 982 #weighted (ie the phrases you use the most auto filter to the top) 983 984 #Searching a drug database for a drug brand name is usually more 985 #functional if it does a start string search, not an instring search which is 986 #much slower and usually unecesary. There are many other examples but trust 987 #me one needs both 988 989 # FIXME: support selection-only-or-empty 990 991 992 #============================================================
993 -class cPhraseWheel(cPhraseWheelBase):
994
995 - def GetData(self, can_create=False, as_instance=False):
996 997 super(cPhraseWheel, self).GetData(can_create = can_create) 998 999 if len(self._data) > 0: 1000 if as_instance: 1001 return self._data2instance() 1002 1003 if len(self._data) == 0: 1004 return None 1005 1006 return self._data.values()[0]['data']
1007 #---------------------------------------------------------
1008 - def SetData(self, data=None):
1009 """Set the data and thereby set the value, too. if possible. 1010 1011 If you call SetData() you better be prepared 1012 doing a scan of the entire potential match space. 1013 1014 The whole thing will only work if data is found 1015 in the match space anyways. 1016 """ 1017 # try getting match candidates 1018 self._update_candidates_in_picklist(u'*') 1019 1020 # do we require a match ? 1021 if self.selection_only: 1022 # yes, but we don't have any candidates 1023 if len(self._current_match_candidates) == 0: 1024 return False 1025 1026 # among candidates look for a match with <data> 1027 for candidate in self._current_match_candidates: 1028 if candidate['data'] == data: 1029 super(cPhraseWheel, self).SetText ( 1030 value = candidate['field_label'], 1031 data = data, 1032 suppress_smarts = True 1033 ) 1034 return True 1035 1036 # no match found in candidates (but needed) ... 1037 if self.selection_only: 1038 self.display_as_valid(valid = False) 1039 return False 1040 1041 self.data = self._dictify_data(data = data) 1042 self.display_as_valid(valid = True) 1043 return True
1044 #-------------------------------------------------------- 1045 # internal API 1046 #--------------------------------------------------------
1047 - def _show_picklist(self, input2match):
1048 1049 # this helps if the current input was already selected from the 1050 # list but still is the substring of another pick list item or 1051 # else the picklist will re-open just after selection 1052 if len(self._data) > 0: 1053 self._picklist_dropdown.Hide() 1054 return 1055 1056 return super(cPhraseWheel, self)._show_picklist(input2match = input2match)
1057 #--------------------------------------------------------
1058 - def _set_data_to_first_match(self):
1059 # data already set ? 1060 if len(self._data) > 0: 1061 return True 1062 1063 # needed ? 1064 val = self.GetValue().strip() 1065 if val == u'': 1066 return True 1067 1068 # so try 1069 self._update_candidates_in_picklist(val = val) 1070 for candidate in self._current_match_candidates: 1071 if candidate['field_label'] == val: 1072 self.data = {candidate['field_label']: candidate} 1073 self.MarkDirty() 1074 return True 1075 1076 # no exact match found 1077 if self.selection_only: 1078 wx.lib.pubsub.Publisher().sendMessage ( 1079 topic = 'statustext', 1080 data = {'msg': self.selection_only_error_msg} 1081 ) 1082 is_valid = False 1083 return False 1084 1085 return True
1086 #---------------------------------------------------------
1088 self.data = {}
1089 #---------------------------------------------------------
1091 return self.GetValue().strip()
1092 #---------------------------------------------------------
1093 - def _dictify_data(self, data=None, value=None):
1094 # assume data to always be old style 1095 if value is None: 1096 value = u'%s' % data 1097 return {value: {'data': data, 'list_label': value, 'field_label': value}}
1098 #============================================================
1099 -class cMultiPhraseWheel(cPhraseWheelBase):
1100
1101 - def __init__(self, *args, **kwargs):
1102 1103 super(cMultiPhraseWheel, self).__init__(*args, **kwargs) 1104 1105 self.phrase_separators = default_phrase_separators 1106 self.left_part = u'' 1107 self.right_part = u'' 1108 self.speller = None
1109 #---------------------------------------------------------
1110 - def GetData(self, can_create=False, as_instance=False):
1111 1112 super(cMultiPhraseWheel, self).GetData(can_create = can_create) 1113 1114 if len(self._data) > 0: 1115 if as_instance: 1116 return self._data2instance() 1117 1118 return self._data.values()
1119 #---------------------------------------------------------
1121 self.speller = None 1122 return True
1123 #---------------------------------------------------------
1124 - def list2data_dict(self, data_items=None):
1125 1126 data_dict = {} 1127 1128 for item in data_items: 1129 try: 1130 list_label = item['list_label'] 1131 except KeyError: 1132 list_label = item['label'] 1133 try: 1134 field_label = item['field_label'] 1135 except KeyError: 1136 field_label = list_label 1137 data_dict[field_label] = {'data': item['data'], 'list_label': list_label, 'field_label': field_label} 1138 1139 return data_dict
1140 #--------------------------------------------------------- 1141 # internal API 1142 #---------------------------------------------------------
1143 - def _get_suggestions_from_speller(self, val):
1144 return None
1145 #---------------------------------------------------------
1147 # the textctrl display must already be set properly 1148 displayed_labels = [ p.strip() for p in self.__phrase_separators.split(self.GetValue().strip()) ] 1149 new_data = {} 1150 # this way of looping automatically removes stale 1151 # data for labels which are no longer displayed 1152 for displayed_label in displayed_labels: 1153 try: 1154 new_data[displayed_label] = self._data[displayed_label] 1155 except KeyError: 1156 # this removes stale data for which there 1157 # is no displayed_label anymore 1158 pass 1159 1160 self.data = new_data
1161 #---------------------------------------------------------
1163 1164 cursor_pos = self.GetInsertionPoint() 1165 1166 entire_input = self.GetValue() 1167 if self.__phrase_separators.search(entire_input) is None: 1168 self.left_part = u'' 1169 self.right_part = u'' 1170 return self.GetValue().strip() 1171 1172 string_left_of_cursor = entire_input[:cursor_pos] 1173 string_right_of_cursor = entire_input[cursor_pos:] 1174 1175 left_parts = [ lp.strip() for lp in self.__phrase_separators.split(string_left_of_cursor) ] 1176 if len(left_parts) == 0: 1177 self.left_part = u'' 1178 else: 1179 self.left_part = u'%s%s ' % ( 1180 (u'%s ' % self.__phrase_separators.pattern[0]).join(left_parts[:-1]), 1181 self.__phrase_separators.pattern[0] 1182 ) 1183 1184 right_parts = [ rp.strip() for rp in self.__phrase_separators.split(string_right_of_cursor) ] 1185 self.right_part = u'%s %s' % ( 1186 self.__phrase_separators.pattern[0], 1187 (u'%s ' % self.__phrase_separators.pattern[0]).join(right_parts[1:]) 1188 ) 1189 1190 val = (left_parts[-1] + right_parts[0]).strip() 1191 return val
1192 #--------------------------------------------------------
1193 - def _update_display_from_picked_item(self, item):
1194 val = (u'%s%s%s' % ( 1195 self.left_part, 1196 self._picklist_item2display_string(item = item), 1197 self.right_part 1198 )).lstrip().lstrip(';').strip() 1199 self.suppress_text_update_smarts = True 1200 super(cMultiPhraseWheel, self).SetValue(val) 1201 # find item end and move cursor to that place: 1202 item_end = val.index(item['field_label']) + len(item['field_label']) 1203 self.SetInsertionPoint(item_end) 1204 return
1205 #--------------------------------------------------------
1206 - def _update_data_from_picked_item(self, item):
1207 1208 # add item to the data 1209 self._data[item['field_label']] = item 1210 1211 # the textctrl display must already be set properly 1212 field_labels = [ p.strip() for p in self.__phrase_separators.split(self.GetValue().strip()) ] 1213 new_data = {} 1214 # this way of looping automatically removes stale 1215 # data for labels which are no longer displayed 1216 for field_label in field_labels: 1217 try: 1218 new_data[field_label] = self._data[field_label] 1219 except KeyError: 1220 # this removes stale data for which there 1221 # is no displayed_label anymore 1222 pass 1223 1224 self.data = new_data
1225 #---------------------------------------------------------
1226 - def _dictify_data(self, data=None, value=None):
1227 if type(data) == type([]): 1228 # useful because self.GetData() returns just such a list 1229 return self.list2data_dict(data_items = data) 1230 # else assume new-style already-dictified data 1231 return data
1232 #-------------------------------------------------------- 1233 # properties 1234 #--------------------------------------------------------
1235 - def _set_phrase_separators(self, phrase_separators):
1236 """Set phrase separators. 1237 1238 - must be a valid regular expression pattern 1239 1240 input is split into phrases at boundaries defined by 1241 this regex and matching is performed on the phrase 1242 the cursor is in only, 1243 1244 after selection from picklist phrase_separators[0] is 1245 added to the end of the match in the PRW 1246 """ 1247 self.__phrase_separators = regex.compile(phrase_separators, flags = regex.LOCALE | regex.UNICODE)
1248
1249 - def _get_phrase_separators(self):
1250 return self.__phrase_separators.pattern
1251 1252 phrase_separators = property(_get_phrase_separators, _set_phrase_separators)
1253 1254 #============================================================ 1255 # main 1256 #------------------------------------------------------------ 1257 if __name__ == '__main__': 1258 1259 if len(sys.argv) < 2: 1260 sys.exit() 1261 1262 if sys.argv[1] != u'test': 1263 sys.exit() 1264 1265 from Gnumed.pycommon import gmI18N 1266 gmI18N.activate_locale() 1267 gmI18N.install_domain(domain='gnumed') 1268 1269 from Gnumed.pycommon import gmPG2, gmMatchProvider 1270 1271 prw = None # used for access from display_values_* 1272 #--------------------------------------------------------
1273 - def display_values_set_focus(*args, **kwargs):
1274 print "got focus:" 1275 print "value:", prw.GetValue() 1276 print "data :", prw.GetData() 1277 return True
1278 #--------------------------------------------------------
1279 - def display_values_lose_focus(*args, **kwargs):
1280 print "lost focus:" 1281 print "value:", prw.GetValue() 1282 print "data :", prw.GetData() 1283 return True
1284 #--------------------------------------------------------
1285 - def display_values_modified(*args, **kwargs):
1286 print "modified:" 1287 print "value:", prw.GetValue() 1288 print "data :", prw.GetData() 1289 return True
1290 #--------------------------------------------------------
1291 - def display_values_selected(*args, **kwargs):
1292 print "selected:" 1293 print "value:", prw.GetValue() 1294 print "data :", prw.GetData() 1295 return True
1296 #-------------------------------------------------------- 1297 #--------------------------------------------------------
1298 - def test_prw_fixed_list():
1299 app = wx.PyWidgetTester(size = (200, 50)) 1300 1301 items = [ {'data': 1, 'list_label': "Bloggs", 'field_label': "Bloggs", 'weight': 0}, 1302 {'data': 2, 'list_label': "Baker", 'field_label': "Baker", 'weight': 0}, 1303 {'data': 3, 'list_label': "Jones", 'field_label': "Jones", 'weight': 0}, 1304 {'data': 4, 'list_label': "Judson", 'field_label': "Judson", 'weight': 0}, 1305 {'data': 5, 'list_label': "Jacobs", 'field_label': "Jacobs", 'weight': 0}, 1306 {'data': 6, 'list_label': "Judson-Jacobs", 'field_label': "Judson-Jacobs", 'weight': 0} 1307 ] 1308 1309 mp = gmMatchProvider.cMatchProvider_FixedList(items) 1310 # do NOT treat "-" as a word separator here as there are names like "asa-sismussen" 1311 mp.word_separators = '[ \t=+&:@]+' 1312 global prw 1313 prw = cPhraseWheel(parent = app.frame, id = -1) 1314 prw.matcher = mp 1315 prw.capitalisation_mode = gmTools.CAPS_NAMES 1316 prw.add_callback_on_set_focus(callback=display_values_set_focus) 1317 prw.add_callback_on_modified(callback=display_values_modified) 1318 prw.add_callback_on_lose_focus(callback=display_values_lose_focus) 1319 prw.add_callback_on_selection(callback=display_values_selected) 1320 1321 app.frame.Show(True) 1322 app.MainLoop() 1323 1324 return True
1325 #--------------------------------------------------------
1326 - def test_prw_sql2():
1327 print "Do you want to test the database connected phrase wheel ?" 1328 yes_no = raw_input('y/n: ') 1329 if yes_no != 'y': 1330 return True 1331 1332 gmPG2.get_connection() 1333 query = u"""SELECT code, code || ': ' || _(name), _(name) FROM dem.country WHERE _(name) %(fragment_condition)s""" 1334 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query]) 1335 app = wx.PyWidgetTester(size = (400, 50)) 1336 global prw 1337 #prw = cPhraseWheel(parent = app.frame, id = -1) 1338 prw = cMultiPhraseWheel(parent = app.frame, id = -1) 1339 prw.matcher = mp 1340 1341 app.frame.Show(True) 1342 app.MainLoop() 1343 1344 return True
1345 #--------------------------------------------------------
1346 - def test_prw_patients():
1347 gmPG2.get_connection() 1348 query = u""" 1349 select 1350 pk_identity, 1351 firstnames || ' ' || lastnames || ', ' || to_char(dob, 'YYYY-MM-DD'), 1352 firstnames || ' ' || lastnames 1353 from 1354 dem.v_basic_person 1355 where 1356 firstnames || lastnames %(fragment_condition)s 1357 """ 1358 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query]) 1359 app = wx.PyWidgetTester(size = (500, 50)) 1360 global prw 1361 prw = cPhraseWheel(parent = app.frame, id = -1) 1362 prw.matcher = mp 1363 prw.selection_only = True 1364 1365 app.frame.Show(True) 1366 app.MainLoop() 1367 1368 return True
1369 #--------------------------------------------------------
1370 - def test_spell_checking_prw():
1371 app = wx.PyWidgetTester(size = (200, 50)) 1372 1373 global prw 1374 prw = cPhraseWheel(parent = app.frame, id = -1) 1375 1376 prw.add_callback_on_set_focus(callback=display_values_set_focus) 1377 prw.add_callback_on_modified(callback=display_values_modified) 1378 prw.add_callback_on_lose_focus(callback=display_values_lose_focus) 1379 prw.add_callback_on_selection(callback=display_values_selected) 1380 1381 prw.enable_default_spellchecker() 1382 1383 app.frame.Show(True) 1384 app.MainLoop() 1385 1386 return True
1387 #-------------------------------------------------------- 1388 #test_prw_fixed_list() 1389 #test_prw_sql2() 1390 #test_spell_checking_prw() 1391 test_prw_patients() 1392 1393 #================================================== 1394