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