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