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