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 return 838 839 # FIXME: check for errors before navigation 840 self.Navigate()
841 842 #--------------------------------------------------------
843 - def __on_cursor_down(self):
844 845 if self._picklist_dropdown.IsShown(): 846 idx_selected = self._picklist.GetFirstSelected() 847 if idx_selected < (len(self._current_match_candidates) - 1): 848 self._select_picklist_row(idx_selected + 1, idx_selected) 849 return 850 851 # if we don't yet have a pick list: open new pick list 852 # (this can happen when we TAB into a field pre-filled 853 # with the top-weighted contextual item but want to 854 # select another contextual item) 855 self.__timer.Stop() 856 if self.GetValue().strip() == '': 857 val = '*' 858 else: 859 val = self._extract_fragment_to_match_on() 860 self._update_candidates_in_picklist(val = val) 861 self._show_picklist(input2match = val)
862 863 #--------------------------------------------------------
864 - def __on_cursor_up(self):
865 if self._picklist_dropdown.IsShown(): 866 selected = self._picklist.GetFirstSelected() 867 if selected > 0: 868 self._select_picklist_row(selected-1, selected)
869 #else: 870 # # FIXME: input history ? 871 872 #--------------------------------------------------------
873 - def __on_tab(self):
874 """Under certain circumstances take special action on <TAB>. 875 876 returns: 877 True: <TAB> was handled 878 False: <TAB> was not handled 879 880 -> can be used to decide whether to do further <TAB> handling outside this class 881 """ 882 # are we seeing the picklist ? 883 if not self._picklist_dropdown.IsShown(): 884 return False 885 886 # with only one candidate ? 887 if len(self._current_match_candidates) != 1: 888 return False 889 890 # and do we require the input to be picked from the candidates ? 891 if not self.selection_only: 892 return False 893 894 # then auto-select that item 895 self._select_picklist_row(new_row_idx = 0) 896 self._on_list_item_selected() 897 898 return True
899 #-------------------------------------------------------- 900 # timer handling 901 #--------------------------------------------------------
902 - def __init_timer(self):
903 self.__timer = _cPRWTimer() 904 self.__timer.callback = self._on_timer_fired 905 # initially stopped 906 self.__timer.Stop()
907 #--------------------------------------------------------
908 - def _on_timer_fired(self):
909 """Callback for delayed match retrieval timer. 910 911 if we end up here: 912 - delay has passed without user input 913 - the value in the input field has not changed since the timer started 914 """ 915 # update matches according to current input 916 val = self._extract_fragment_to_match_on() 917 self._update_candidates_in_picklist(val = val) 918 919 # we now have either: 920 # - all possible items (within reasonable limits) if input was '*' 921 # - all matching items 922 # - an empty match list if no matches were found 923 # also, our picklist is refilled and sorted according to weight 924 wx.CallAfter(self._show_picklist, input2match = val)
925 #---------------------------------------------------- 926 # random helpers and properties 927 #----------------------------------------------------
928 - def __mac_log(self, msg):
929 if self.__use_fake_popup: 930 _log.debug(msg)
931 932 #--------------------------------------------------------
933 - def __char_is_allowed(self, char=None):
934 # if undefined accept all chars 935 if self.accepted_chars is None: 936 return True 937 return (self.__accepted_chars.match(char) is not None)
938 939 #--------------------------------------------------------
940 - def _set_accepted_chars(self, accepted_chars=None):
941 if accepted_chars is None: 942 self.__accepted_chars = None 943 else: 944 self.__accepted_chars = regex.compile(accepted_chars)
945
946 - def _get_accepted_chars(self):
947 if self.__accepted_chars is None: 948 return None 949 return self.__accepted_chars.pattern
950 951 accepted_chars = property(_get_accepted_chars, _set_accepted_chars) 952 953 #--------------------------------------------------------
954 - def _set_final_regex(self, final_regex=r'.*'):
955 self.__final_regex = regex.compile(final_regex, flags = regex.UNICODE)
956
957 - def _get_final_regex(self):
958 return self.__final_regex.pattern
959 960 final_regex = property(_get_final_regex, _set_final_regex) 961 962 #--------------------------------------------------------
963 - def _set_final_regex_error_msg(self, msg):
964 self.__final_regex_error_msg = msg
965
966 - def _get_final_regex_error_msg(self):
967 return self.__final_regex_error_msg
968 969 final_regex_error_msg = property(_get_final_regex_error_msg, _set_final_regex_error_msg) 970 971 #-------------------------------------------------------- 972 # data munging 973 #--------------------------------------------------------
974 - def _set_data_to_first_match(self):
975 return False
976 #--------------------------------------------------------
977 - def _update_data_from_picked_item(self, item):
978 self.data = {item['field_label']: item}
979 #--------------------------------------------------------
980 - def _dictify_data(self, data=None, value=None):
981 raise NotImplementedError('[%s]: _dictify_data()' % self.__class__.__name__)
982 #---------------------------------------------------------
984 raise NotImplementedError('[%s]: cannot adjust data after text update' % self.__class__.__name__)
985 #--------------------------------------------------------
986 - def _data2match(self, data):
987 if self.matcher is None: 988 return None 989 return self.matcher.get_match_by_data(data = data)
990 #--------------------------------------------------------
991 - def _create_data(self):
992 raise NotImplementedError('[%s]: cannot create data object' % self.__class__.__name__)
993 #--------------------------------------------------------
994 - def _get_data(self):
995 return self._data
996
997 - def _set_data(self, data):
998 self._data = data 999 self.__recalculate_tooltip()
1000 1001 data = property(_get_data, _set_data)
1002 1003 #============================================================ 1004 # FIXME: cols in pick list 1005 # FIXME: snap_to_basename+set selection 1006 # FIXME: learn() -> PWL 1007 # FIXME: up-arrow: show recent (in-memory) history 1008 #---------------------------------------------------------- 1009 # ideas 1010 #---------------------------------------------------------- 1011 #- display possible completion but highlighted for deletion 1012 #(- cycle through possible completions) 1013 #- pre-fill selection with SELECT ... LIMIT 25 1014 #- async threads for match retrieval instead of timer 1015 # - on truncated results return item "..." -> selection forcefully retrieves all matches 1016 1017 #- generators/yield() 1018 #- OnChar() - process a char event 1019 1020 # split input into words and match components against known phrases 1021 1022 # make special list window: 1023 # - deletion of items 1024 # - highlight matched parts 1025 # - faster scrolling 1026 # - wxEditableListBox ? 1027 1028 # - if non-learning (i.e. fast select only): autocomplete with match 1029 # and move cursor to end of match 1030 #----------------------------------------------------------------------------------------------- 1031 # darn ! this clever hack won't work since we may have crossed a search location threshold 1032 #---- 1033 # #self.__prevFragment = "***********-very-unlikely--------------***************" 1034 # #self.__prevMatches = [] # a list of tuples (ID, listbox name, weight) 1035 # 1036 # # is the current fragment just a longer version of the previous fragment ? 1037 # if string.find(aFragment, self.__prevFragment) == 0: 1038 # # we then need to search in the previous matches only 1039 # for prevMatch in self.__prevMatches: 1040 # if string.find(prevMatch[1], aFragment) == 0: 1041 # matches.append(prevMatch) 1042 # # remember current matches 1043 # self.__prefMatches = matches 1044 # # no matches found 1045 # if len(matches) == 0: 1046 # return [(1,_('*no matching items found*'),1)] 1047 # else: 1048 # return matches 1049 #---- 1050 #TODO: 1051 # - see spincontrol for list box handling 1052 # stop list (list of negatives): "an" -> "animal" but not "and" 1053 #----- 1054 #> > remember, you should be searching on either weighted data, or in some 1055 #> > situations a start string search on indexed data 1056 #> 1057 #> Can you be a bit more specific on this ? 1058 1059 #seaching ones own previous text entered would usually be instring but 1060 #weighted (ie the phrases you use the most auto filter to the top) 1061 1062 #Searching a drug database for a drug product name is usually more 1063 #functional if it does a start string search, not an instring search which is 1064 #much slower and usually unecesary. There are many other examples but trust 1065 #me one needs both 1066 1067 # FIXME: support selection-only-or-empty 1068 1069 1070 #============================================================
1071 -class cPhraseWheel(cPhraseWheelBase):
1072
1073 - def GetData(self, can_create=False, as_instance=False):
1074 1075 super(cPhraseWheel, self).GetData(can_create = can_create) 1076 1077 if len(self._data) > 0: 1078 if as_instance: 1079 return self._data2instance() 1080 1081 if len(self._data) == 0: 1082 return None 1083 1084 return list(self._data.values())[0]['data']
1085 1086 #---------------------------------------------------------
1087 - def SetData(self, data=None):
1088 """Set the data and thereby set the value, too. if possible. 1089 1090 If you call SetData() you better be prepared 1091 doing a scan of the entire potential match space. 1092 1093 The whole thing will only work if data is found 1094 in the match space anyways. 1095 """ 1096 if data is None: 1097 self._data = {} 1098 return True 1099 1100 # try getting match candidates 1101 self._update_candidates_in_picklist('*') 1102 1103 # do we require a match ? 1104 if self.selection_only: 1105 # yes, but we don't have any candidates 1106 if len(self._current_match_candidates) == 0: 1107 return False 1108 1109 # among candidates look for a match with <data> 1110 for candidate in self._current_match_candidates: 1111 if candidate['data'] == data: 1112 super(cPhraseWheel, self).SetText ( 1113 value = candidate['field_label'], 1114 data = data, 1115 suppress_smarts = True 1116 ) 1117 return True 1118 1119 # no match found in candidates (but needed) ... 1120 if self.selection_only: 1121 self.display_as_valid(valid = False) 1122 return False 1123 1124 self.data = self._dictify_data(data = data) 1125 self.display_as_valid(valid = True) 1126 return True
1127 1128 #-------------------------------------------------------- 1129 # internal API 1130 #--------------------------------------------------------
1131 - def _show_picklist(self, input2match):
1132 1133 # this helps if the current input was already selected from the 1134 # list but still is the substring of another pick list item or 1135 # else the picklist will re-open just after selection 1136 if len(self._data) > 0: 1137 self._picklist_dropdown.Hide() 1138 return 1139 1140 return super(cPhraseWheel, self)._show_picklist(input2match = input2match)
1141 1142 #--------------------------------------------------------
1143 - def _set_data_to_first_match(self):
1144 # data already set ? 1145 if len(self._data) > 0: 1146 return True 1147 1148 # needed ? 1149 val = self.GetValue().strip() 1150 if val == '': 1151 return True 1152 1153 # so try 1154 self._update_candidates_in_picklist(val = val) 1155 for candidate in self._current_match_candidates: 1156 if candidate['field_label'] == val: 1157 self._update_data_from_picked_item(candidate) 1158 self.MarkDirty() 1159 # tell listeners about the user's selection 1160 for callback in self._on_selection_callbacks: 1161 callback(self._data) 1162 return True 1163 1164 # no exact match found 1165 if self.selection_only: 1166 gmDispatcher.send(signal = 'statustext', msg = self.selection_only_error_msg) 1167 is_valid = False 1168 return False 1169 1170 return True
1171 1172 #---------------------------------------------------------
1174 self.data = {}
1175 1176 #---------------------------------------------------------
1178 return self.GetValue().strip()
1179 1180 #---------------------------------------------------------
1181 - def _dictify_data(self, data=None, value=None):
1182 # assume data to always be old style 1183 if value is None: 1184 value = '%s' % data 1185 return {value: {'data': data, 'list_label': value, 'field_label': value}}
1186 1187 #============================================================
1188 -class cMultiPhraseWheel(cPhraseWheelBase):
1189
1190 - def __init__(self, *args, **kwargs):
1191 1192 super(cMultiPhraseWheel, self).__init__(*args, **kwargs) 1193 1194 self.phrase_separators = default_phrase_separators 1195 self.left_part = '' 1196 self.right_part = '' 1197 self.speller = None
1198 #---------------------------------------------------------
1199 - def GetData(self, can_create=False, as_instance=False):
1200 1201 super(cMultiPhraseWheel, self).GetData(can_create = can_create) 1202 1203 if len(self._data) > 0: 1204 if as_instance: 1205 return self._data2instance() 1206 1207 return list(self._data.values())
1208 #---------------------------------------------------------
1210 self.speller = None 1211 return True
1212 #---------------------------------------------------------
1213 - def list2data_dict(self, data_items=None):
1214 1215 data_dict = {} 1216 1217 for item in data_items: 1218 try: 1219 list_label = item['list_label'] 1220 except KeyError: 1221 list_label = item['label'] 1222 try: 1223 field_label = item['field_label'] 1224 except KeyError: 1225 field_label = list_label 1226 data_dict[field_label] = {'data': item['data'], 'list_label': list_label, 'field_label': field_label} 1227 1228 return data_dict
1229 #--------------------------------------------------------- 1230 # internal API 1231 #---------------------------------------------------------
1232 - def _get_suggestions_from_speller(self, val):
1233 return None
1234 #---------------------------------------------------------
1236 # the textctrl display must already be set properly 1237 new_data = {} 1238 # this way of looping automatically removes stale 1239 # data for labels which are no longer displayed 1240 for displayed_label in self.displayed_strings: 1241 try: 1242 new_data[displayed_label] = self._data[displayed_label] 1243 except KeyError: 1244 # this removes stale data for which there 1245 # is no displayed_label anymore 1246 pass 1247 1248 self.data = new_data
1249 #---------------------------------------------------------
1251 1252 cursor_pos = self.GetInsertionPoint() 1253 1254 entire_input = self.GetValue() 1255 if self.__phrase_separators.search(entire_input) is None: 1256 self.left_part = '' 1257 self.right_part = '' 1258 return self.GetValue().strip() 1259 1260 string_left_of_cursor = entire_input[:cursor_pos] 1261 string_right_of_cursor = entire_input[cursor_pos:] 1262 1263 left_parts = [ lp.strip() for lp in self.__phrase_separators.split(string_left_of_cursor) ] 1264 if len(left_parts) == 0: 1265 self.left_part = '' 1266 else: 1267 self.left_part = '%s%s ' % ( 1268 ('%s ' % self.__phrase_separators.pattern[0]).join(left_parts[:-1]), 1269 self.__phrase_separators.pattern[0] 1270 ) 1271 1272 right_parts = [ rp.strip() for rp in self.__phrase_separators.split(string_right_of_cursor) ] 1273 self.right_part = '%s %s' % ( 1274 self.__phrase_separators.pattern[0], 1275 ('%s ' % self.__phrase_separators.pattern[0]).join(right_parts[1:]) 1276 ) 1277 1278 val = (left_parts[-1] + right_parts[0]).strip() 1279 return val
1280 #--------------------------------------------------------
1281 - def _update_display_from_picked_item(self, item):
1282 val = ('%s%s%s' % ( 1283 self.left_part, 1284 self._picklist_item2display_string(item = item), 1285 self.right_part 1286 )).lstrip().lstrip(';').strip() 1287 self.suppress_text_update_smarts = True 1288 super(cMultiPhraseWheel, self).SetValue(val) 1289 # find item end and move cursor to that place: 1290 item_end = val.index(item['field_label']) + len(item['field_label']) 1291 self.SetInsertionPoint(item_end) 1292 return
1293 #--------------------------------------------------------
1294 - def _update_data_from_picked_item(self, item):
1295 1296 # add item to the data 1297 self._data[item['field_label']] = item 1298 1299 # the textctrl display must already be set properly 1300 field_labels = [ p.strip() for p in self.__phrase_separators.split(self.GetValue().strip()) ] 1301 new_data = {} 1302 # this way of looping automatically removes stale 1303 # data for labels which are no longer displayed 1304 for field_label in field_labels: 1305 try: 1306 new_data[field_label] = self._data[field_label] 1307 except KeyError: 1308 # this removes stale data for which there 1309 # is no displayed_label anymore 1310 pass 1311 1312 self.data = new_data
1313 #---------------------------------------------------------
1314 - def _dictify_data(self, data=None, value=None):
1315 if type(data) == type([]): 1316 # useful because self.GetData() returns just such a list 1317 return self.list2data_dict(data_items = data) 1318 # else assume new-style already-dictified data 1319 return data
1320 #-------------------------------------------------------- 1321 # properties 1322 #--------------------------------------------------------
1323 - def _set_phrase_separators(self, phrase_separators):
1324 """Set phrase separators. 1325 1326 - must be a valid regular expression pattern 1327 1328 input is split into phrases at boundaries defined by 1329 this regex and matching is performed on the phrase 1330 the cursor is in only, 1331 1332 after selection from picklist phrase_separators[0] is 1333 added to the end of the match in the PRW 1334 """ 1335 self.__phrase_separators = regex.compile(phrase_separators, flags = regex.UNICODE)
1336
1337 - def _get_phrase_separators(self):
1338 return self.__phrase_separators.pattern
1339 1340 phrase_separators = property(_get_phrase_separators, _set_phrase_separators) 1341 #--------------------------------------------------------
1342 - def _get_displayed_strings(self):
1343 return [ p.strip() for p in self.__phrase_separators.split(self.GetValue().strip()) if p.strip() != '' ]
1344 1345 displayed_strings = property(_get_displayed_strings, lambda x:x)
1346 #============================================================ 1347 # main 1348 #------------------------------------------------------------ 1349 if __name__ == '__main__': 1350 1351 if len(sys.argv) < 2: 1352 sys.exit() 1353 1354 if sys.argv[1] != 'test': 1355 sys.exit() 1356 1357 from Gnumed.pycommon import gmI18N 1358 gmI18N.activate_locale() 1359 gmI18N.install_domain(domain='gnumed') 1360 1361 from Gnumed.pycommon import gmPG2, gmMatchProvider 1362 1363 prw = None # used for access from display_values_* 1364 #--------------------------------------------------------
1365 - def display_values_set_focus(*args, **kwargs):
1366 print("got focus:") 1367 print("value:", prw.GetValue()) 1368 print("data :", prw.GetData()) 1369 return True
1370 #--------------------------------------------------------
1371 - def display_values_lose_focus(*args, **kwargs):
1372 print("lost focus:") 1373 print("value:", prw.GetValue()) 1374 print("data :", prw.GetData()) 1375 return True
1376 #--------------------------------------------------------
1377 - def display_values_modified(*args, **kwargs):
1378 print("modified:") 1379 print("value:", prw.GetValue()) 1380 print("data :", prw.GetData()) 1381 return True
1382 #--------------------------------------------------------
1383 - def display_values_selected(*args, **kwargs):
1384 print("selected:") 1385 print("value:", prw.GetValue()) 1386 print("data :", prw.GetData()) 1387 return True
1388 #-------------------------------------------------------- 1389 #--------------------------------------------------------
1390 - def test_prw_fixed_list():
1391 app = wx.PyWidgetTester(size = (200, 50)) 1392 1393 items = [ {'data': 1, 'list_label': "Bloggs", 'field_label': "Bloggs", 'weight': 0}, 1394 {'data': 2, 'list_label': "Baker", 'field_label': "Baker", 'weight': 0}, 1395 {'data': 3, 'list_label': "Jones", 'field_label': "Jones", 'weight': 0}, 1396 {'data': 4, 'list_label': "Judson", 'field_label': "Judson", 'weight': 0}, 1397 {'data': 5, 'list_label': "Jacobs", 'field_label': "Jacobs", 'weight': 0}, 1398 {'data': 6, 'list_label': "Judson-Jacobs", 'field_label': "Judson-Jacobs", 'weight': 0} 1399 ] 1400 1401 mp = gmMatchProvider.cMatchProvider_FixedList(items) 1402 # do NOT treat "-" as a word separator here as there are names like "asa-sismussen" 1403 mp.word_separators = '[ \t=+&:@]+' 1404 global prw 1405 prw = cPhraseWheel(app.frame, -1) 1406 prw.matcher = mp 1407 prw.capitalisation_mode = gmTools.CAPS_NAMES 1408 prw.add_callback_on_set_focus(callback=display_values_set_focus) 1409 prw.add_callback_on_modified(callback=display_values_modified) 1410 prw.add_callback_on_lose_focus(callback=display_values_lose_focus) 1411 prw.add_callback_on_selection(callback=display_values_selected) 1412 1413 app.frame.Show(True) 1414 app.MainLoop() 1415 1416 return True
1417 #--------------------------------------------------------
1418 - def test_prw_sql2():
1419 print("Do you want to test the database connected phrase wheel ?") 1420 yes_no = input('y/n: ') 1421 if yes_no != 'y': 1422 return True 1423 1424 gmPG2.get_connection() 1425 query = """SELECT code, code || ': ' || _(name), _(name) FROM dem.country WHERE _(name) %(fragment_condition)s""" 1426 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query]) 1427 app = wx.PyWidgetTester(size = (400, 50)) 1428 global prw 1429 #prw = cPhraseWheel(app.frame, -1) 1430 prw = cMultiPhraseWheel(app.frame, -1) 1431 prw.matcher = mp 1432 1433 app.frame.Show(True) 1434 app.MainLoop() 1435 1436 return True
1437 #--------------------------------------------------------
1438 - def test_prw_patients():
1439 gmPG2.get_connection() 1440 query = """ 1441 select 1442 pk_identity, 1443 firstnames || ' ' || lastnames || ', ' || to_char(dob, 'YYYY-MM-DD'), 1444 firstnames || ' ' || lastnames 1445 from 1446 dem.v_active_persons 1447 where 1448 firstnames || lastnames %(fragment_condition)s 1449 """ 1450 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query]) 1451 app = wx.PyWidgetTester(size = (500, 50)) 1452 global prw 1453 prw = cPhraseWheel(app.frame, -1) 1454 prw.matcher = mp 1455 prw.selection_only = True 1456 1457 app.frame.Show(True) 1458 app.MainLoop() 1459 1460 return True
1461 #--------------------------------------------------------
1462 - def test_spell_checking_prw():
1463 app = wx.PyWidgetTester(size = (200, 50)) 1464 1465 global prw 1466 prw = cPhraseWheel(app.frame, -1) 1467 1468 prw.add_callback_on_set_focus(callback=display_values_set_focus) 1469 prw.add_callback_on_modified(callback=display_values_modified) 1470 prw.add_callback_on_lose_focus(callback=display_values_lose_focus) 1471 prw.add_callback_on_selection(callback=display_values_selected) 1472 1473 prw.enable_default_spellchecker() 1474 1475 app.frame.Show(True) 1476 app.MainLoop() 1477 1478 return True
1479 #-------------------------------------------------------- 1480 #test_prw_fixed_list() 1481 #test_prw_sql2() 1482 #test_spell_checking_prw() 1483 test_prw_patients() 1484 1485 #================================================== 1486