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