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