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  # $Source: /cvsroot/gnumed/gnumed/gnumed/client/wxpython/gmPhraseWheel.py,v $ 
  11  # $Id: gmPhraseWheel.py,v 1.136 2010/02/02 13:55:59 ncq Exp $ 
  12  __version__ = "$Revision: 1.136 $" 
  13  __author__  = "K.Hilbert <Karsten.Hilbert@gmx.net>, I.Haywood, S.J.Tan <sjtan@bigpond.com>" 
  14  __license__ = "GPL" 
  15   
  16  # stdlib 
  17  import string, types, time, sys, re as regex, os.path 
  18   
  19   
  20  # 3rd party 
  21  import wx 
  22  import wx.lib.mixins.listctrl as listmixins 
  23  import wx.lib.pubsub 
  24   
  25   
  26  # GNUmed specific 
  27  if __name__ == '__main__': 
  28          sys.path.insert(0, '../../') 
  29  from Gnumed.pycommon import gmTools 
  30   
  31   
  32  import logging 
  33  _log = logging.getLogger('macosx') 
  34   
  35   
  36  color_prw_invalid = 'pink' 
  37  color_prw_valid = None                          # this is used by code outside this module 
  38   
  39  default_phrase_separators = '[;/|]+' 
  40  default_spelling_word_separators = '[\W\d_]+' 
  41   
  42  # those can be used by the <accepted_chars> phrasewheel parameter 
  43  NUMERIC = '0-9' 
  44  ALPHANUMERIC = 'a-zA-Z0-9' 
  45  EMAIL_CHARS = "a-zA-Z0-9\-_@\." 
  46  WEB_CHARS = "a-zA-Z0-9\.\-_/:" 
  47   
  48   
  49  _timers = [] 
  50  #============================================================ 
51 -def shutdown():
52 """It can be useful to call this early from your shutdown code to avoid hangs on Notify().""" 53 global _timers 54 _log.info('shutting down %s pending timers', len(_timers)) 55 for timer in _timers: 56 _log.debug('timer [%s]', timer) 57 timer.Stop() 58 _timers = []
59 #------------------------------------------------------------
60 -class _cPRWTimer(wx.Timer):
61
62 - def __init__(self, *args, **kwargs):
63 wx.Timer.__init__(self, *args, **kwargs) 64 self.callback = lambda x:x 65 global _timers 66 _timers.append(self)
67
68 - def Notify(self):
69 self.callback()
70 #============================================================ 71 # FIXME: merge with gmListWidgets
72 -class cPhraseWheelListCtrl(wx.ListCtrl, listmixins.ListCtrlAutoWidthMixin):
73 - 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['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_label(self):
94 sel_idx = self.GetFirstSelected() 95 if sel_idx == -1: 96 return None 97 return self.__data[sel_idx]['label']
98 #============================================================ 99 # FIXME: cols in pick list 100 # FIXME: snap_to_basename+set selection 101 # FIXME: learn() -> PWL 102 # FIXME: up-arrow: show recent (in-memory) history 103 #---------------------------------------------------------- 104 # ideas 105 #---------------------------------------------------------- 106 #- display possible completion but highlighted for deletion 107 #(- cycle through possible completions) 108 #- pre-fill selection with SELECT ... LIMIT 25 109 #- async threads for match retrieval instead of timer 110 # - on truncated results return item "..." -> selection forcefully retrieves all matches 111 112 #- generators/yield() 113 #- OnChar() - process a char event 114 115 # split input into words and match components against known phrases 116 117 # make special list window: 118 # - deletion of items 119 # - highlight matched parts 120 # - faster scrolling 121 # - wxEditableListBox ? 122 123 # - if non-learning (i.e. fast select only): autocomplete with match 124 # and move cursor to end of match 125 #----------------------------------------------------------------------------------------------- 126 # darn ! this clever hack won't work since we may have crossed a search location threshold 127 #---- 128 # #self.__prevFragment = "XXXXXXXXXXXXXXXXXX-very-unlikely--------------XXXXXXXXXXXXXXX" 129 # #self.__prevMatches = [] # a list of tuples (ID, listbox name, weight) 130 # 131 # # is the current fragment just a longer version of the previous fragment ? 132 # if string.find(aFragment, self.__prevFragment) == 0: 133 # # we then need to search in the previous matches only 134 # for prevMatch in self.__prevMatches: 135 # if string.find(prevMatch[1], aFragment) == 0: 136 # matches.append(prevMatch) 137 # # remember current matches 138 # self.__prefMatches = matches 139 # # no matches found 140 # if len(matches) == 0: 141 # return [(1,_('*no matching items found*'),1)] 142 # else: 143 # return matches 144 #---- 145 #TODO: 146 # - see spincontrol for list box handling 147 # stop list (list of negatives): "an" -> "animal" but not "and" 148 #----- 149 #> > remember, you should be searching on either weighted data, or in some 150 #> > situations a start string search on indexed data 151 #> 152 #> Can you be a bit more specific on this ? 153 154 #seaching ones own previous text entered would usually be instring but 155 #weighted (ie the phrases you use the most auto filter to the top) 156 157 #Searching a drug database for a drug brand name is usually more 158 #functional if it does a start string search, not an instring search which is 159 #much slower and usually unecesary. There are many other examples but trust 160 #me one needs both 161 #-----
162 -class cPhraseWheel(wx.TextCtrl):
163 """Widget for smart guessing of user fields, after Richard Terry's interface. 164 165 - VB implementation by Richard Terry 166 - Python port by Ian Haywood for GNUmed 167 - enhanced by Karsten Hilbert for GNUmed 168 - enhanced by Ian Haywood for aumed 169 - enhanced by Karsten Hilbert for GNUmed 170 171 @param matcher: a class used to find matches for the current input 172 @type matcher: a L{match provider<Gnumed.pycommon.gmMatchProvider.cMatchProvider>} 173 instance or C{None} 174 175 @param selection_only: whether free-text can be entered without associated data 176 @type selection_only: boolean 177 178 @param capitalisation_mode: how to auto-capitalize input, valid values 179 are found in L{capitalize()<Gnumed.pycommon.gmTools.capitalize>} 180 @type capitalisation_mode: integer 181 182 @param accepted_chars: a regex pattern defining the characters 183 acceptable in the input string, if None no checking is performed 184 @type accepted_chars: None or a string holding a valid regex pattern 185 186 @param final_regex: when the control loses focus the input is 187 checked against this regular expression 188 @type final_regex: a string holding a valid regex pattern 189 190 @param phrase_separators: if not None, input is split into phrases 191 at boundaries defined by this regex and matching/spellchecking 192 is performed on the phrase the cursor is in only 193 @type phrase_separators: None or a string holding a valid regex pattern 194 195 @param navigate_after_selection: whether or not to immediately 196 navigate to the widget next-in-tab-order after selecting an 197 item from the dropdown picklist 198 @type navigate_after_selection: boolean 199 200 @param speller: if not None used to spellcheck the current input 201 and to retrieve suggested replacements/completions 202 @type speller: None or a L{enchant Dict<enchant>} descendant 203 204 @param picklist_delay: this much time of user inactivity must have 205 passed before the input related smarts kick in and the drop 206 down pick list is shown 207 @type picklist_delay: integer (milliseconds) 208 """
209 - def __init__ (self, parent=None, id=-1, value='', *args, **kwargs):
210 211 # behaviour 212 self.matcher = None 213 self.selection_only = False 214 self.selection_only_error_msg = _('You must select a value from the picklist or type an exact match.') 215 self.capitalisation_mode = gmTools.CAPS_NONE 216 self.accepted_chars = None 217 self.final_regex = '.*' 218 self.final_regex_error_msg = _('The content is invalid. It must match the regular expression: [%%s]. <%s>') % self.__class__.__name__ 219 self.phrase_separators = default_phrase_separators 220 self.navigate_after_selection = False 221 self.speller = None 222 self.speller_word_separators = default_spelling_word_separators 223 self.picklist_delay = 150 # milliseconds 224 225 # state tracking 226 self._has_focus = False 227 self.suppress_text_update_smarts = False 228 self.__current_matches = [] 229 self._screenheight = wx.SystemSettings.GetMetric(wx.SYS_SCREEN_Y) 230 self.input2match = '' 231 self.left_part = '' 232 self.right_part = '' 233 self.data = None 234 235 self._on_selection_callbacks = [] 236 self._on_lose_focus_callbacks = [] 237 self._on_set_focus_callbacks = [] 238 self._on_modified_callbacks = [] 239 240 try: 241 kwargs['style'] = kwargs['style'] | wx.TE_PROCESS_TAB 242 except KeyError: 243 kwargs['style'] = wx.TE_PROCESS_TAB 244 wx.TextCtrl.__init__(self, parent, id, **kwargs) 245 246 self.__non_edit_font = self.GetFont() 247 self.__color_valid = self.GetBackgroundColour() 248 global color_prw_valid 249 if color_prw_valid is None: 250 color_prw_valid = wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW) 251 252 self.__init_dropdown(parent = parent) 253 self.__register_events() 254 self.__init_timer()
255 #-------------------------------------------------------- 256 # external API 257 #--------------------------------------------------------
258 - def add_callback_on_selection(self, callback=None):
259 """ 260 Add a callback for invocation when a picklist item is selected. 261 262 The callback will be invoked whenever an item is selected 263 from the picklist. The associated data is passed in as 264 a single parameter. Callbacks must be able to cope with 265 None as the data parameter as that is sent whenever the 266 user changes a previously selected value. 267 """ 268 if not callable(callback): 269 raise ValueError('[add_callback_on_selection]: ignoring callback [%s], it is not callable' % callback) 270 271 self._on_selection_callbacks.append(callback)
272 #---------------------------------------------------------
273 - def add_callback_on_set_focus(self, callback=None):
274 """ 275 Add a callback for invocation when getting focus. 276 """ 277 if not callable(callback): 278 raise ValueError('[add_callback_on_set_focus]: ignoring callback [%s] - not callable' % callback) 279 280 self._on_set_focus_callbacks.append(callback)
281 #---------------------------------------------------------
282 - def add_callback_on_lose_focus(self, callback=None):
283 """ 284 Add a callback for invocation when losing focus. 285 """ 286 if not callable(callback): 287 raise ValueError('[add_callback_on_lose_focus]: ignoring callback [%s] - not callable' % callback) 288 289 self._on_lose_focus_callbacks.append(callback)
290 #---------------------------------------------------------
291 - def add_callback_on_modified(self, callback=None):
292 """ 293 Add a callback for invocation when the content is modified. 294 """ 295 if not callable(callback): 296 raise ValueError('[add_callback_on_modified]: ignoring callback [%s] - not callable' % callback) 297 298 self._on_modified_callbacks.append(callback)
299 #---------------------------------------------------------
300 - def SetData(self, data=None):
301 """ 302 Set the data and thereby set the value, too. 303 304 If you call SetData() you better be prepared 305 doing a scan of the entire potential match space. 306 307 The whole thing will only work if data is found 308 in the match space anyways. 309 """ 310 if self.matcher is None: 311 matched, matches = (False, []) 312 else: 313 matched, matches = self.matcher.getMatches('*') 314 315 if self.selection_only: 316 if not matched or (len(matches) == 0): 317 return False 318 319 for match in matches: 320 if match['data'] == data: 321 self.display_as_valid(valid = True) 322 self.suppress_text_update_smarts = True 323 wx.TextCtrl.SetValue(self, match['label']) 324 self.data = data 325 return True 326 327 # no match found ... 328 if self.selection_only: 329 return False 330 331 self.data = data 332 self.display_as_valid(valid = True) 333 return True
334 #---------------------------------------------------------
335 - def GetData(self, can_create=False):
336 """Retrieve the data associated with the displayed string. 337 """ 338 if self.data is None: 339 if can_create: 340 self._create_data() 341 return self.data
342 #---------------------------------------------------------
343 - def SetText(self, value=u'', data=None, suppress_smarts=False):
344 345 self.suppress_text_update_smarts = suppress_smarts 346 347 if data is not None: 348 self.suppress_text_update_smarts = True 349 self.data = data 350 wx.TextCtrl.SetValue(self, value) 351 self.display_as_valid(valid = True) 352 353 # if data already available 354 if self.data is not None: 355 return True 356 357 if value == u'' and not self.selection_only: 358 return True 359 360 # or try to find data from matches 361 if self.matcher is None: 362 stat, matches = (False, []) 363 else: 364 stat, matches = self.matcher.getMatches(aFragment = value) 365 366 for match in matches: 367 if match['label'] == value: 368 self.data = match['data'] 369 return True 370 371 # not found 372 if self.selection_only: 373 self.display_as_valid(valid = False) 374 return False 375 376 return True
377 #--------------------------------------------------------
378 - def set_context(self, context=None, val=None):
379 if self.matcher is not None: 380 self.matcher.set_context(context=context, val=val)
381 #---------------------------------------------------------
382 - def unset_context(self, context=None):
383 if self.matcher is not None: 384 self.matcher.unset_context(context=context)
385 #--------------------------------------------------------
387 # FIXME: use Debian's wgerman-medical as "personal" wordlist if available 388 try: 389 import enchant 390 except ImportError: 391 self.speller = None 392 return False 393 try: 394 self.speller = enchant.DictWithPWL(None, os.path.expanduser(os.path.join('~', '.gnumed', 'spellcheck', 'wordlist.pwl'))) 395 except enchant.DictNotFoundError: 396 self.speller = None 397 return False 398 return True
399 #--------------------------------------------------------
400 - def display_as_valid(self, valid=None):
401 if valid is True: 402 self.SetBackgroundColour(self.__color_valid) 403 elif valid is False: 404 self.SetBackgroundColour(color_prw_invalid) 405 else: 406 raise ArgumentError(u'<valid> must be True or False') 407 self.Refresh()
408 #-------------------------------------------------------- 409 # internal API 410 #-------------------------------------------------------- 411 # picklist handling 412 #--------------------------------------------------------
413 - def __init_dropdown(self, parent = None):
414 szr_dropdown = None 415 try: 416 #raise NotImplementedError # for testing 417 self.__dropdown_needs_relative_position = False 418 self.__picklist_dropdown = wx.PopupWindow(parent) 419 list_parent = self.__picklist_dropdown 420 self.__use_fake_popup = False 421 except NotImplementedError: 422 self.__use_fake_popup = True 423 424 # on MacOSX wx.PopupWindow is not implemented, so emulate it 425 add_picklist_to_sizer = True 426 szr_dropdown = wx.BoxSizer(wx.VERTICAL) 427 428 # using wx.MiniFrame 429 self.__dropdown_needs_relative_position = False 430 self.__picklist_dropdown = wx.MiniFrame ( 431 parent = parent, 432 id = -1, 433 style = wx.SIMPLE_BORDER | wx.FRAME_FLOAT_ON_PARENT | wx.FRAME_NO_TASKBAR | wx.POPUP_WINDOW 434 ) 435 scroll_win = wx.ScrolledWindow(parent = self.__picklist_dropdown, style = wx.NO_BORDER) 436 scroll_win.SetSizer(szr_dropdown) 437 list_parent = scroll_win 438 439 # using wx.Window 440 #self.__dropdown_needs_relative_position = True 441 #self.__picklist_dropdown = wx.ScrolledWindow(parent=parent, style = wx.RAISED_BORDER) 442 #self.__picklist_dropdown.SetSizer(szr_dropdown) 443 #list_parent = self.__picklist_dropdown 444 445 self.mac_log('dropdown parent: %s' % self.__picklist_dropdown.GetParent()) 446 447 # FIXME: support optional headers 448 # if kwargs['show_list_headers']: 449 # flags = 0 450 # else: 451 # flags = wx.LC_NO_HEADER 452 self._picklist = cPhraseWheelListCtrl ( 453 list_parent, 454 style = wx.LC_NO_HEADER 455 ) 456 self._picklist.InsertColumn(0, '') 457 458 if szr_dropdown is not None: 459 szr_dropdown.Add(self._picklist, 1, wx.EXPAND) 460 461 self.__picklist_dropdown.Hide()
462 #--------------------------------------------------------
463 - def _show_picklist(self):
464 """Display the pick list.""" 465 466 border_width = 4 467 extra_height = 25 468 469 self.__picklist_dropdown.Hide() 470 471 # this helps if the current input was already selected from the 472 # list but still is the substring of another pick list item 473 if self.data is not None: 474 return 475 476 if not self._has_focus: 477 return 478 479 if len(self.__current_matches) == 0: 480 return 481 482 # if only one match and text == match 483 if len(self.__current_matches) == 1: 484 if self.__current_matches[0]['label'] == self.input2match: 485 self.data = self.__current_matches[0]['data'] 486 return 487 488 # recalculate size 489 rows = len(self.__current_matches) 490 if rows < 2: # 2 rows minimum 491 rows = 2 492 if rows > 20: # 20 rows maximum 493 rows = 20 494 self.mac_log('dropdown needs rows: %s' % rows) 495 dropdown_size = self.__picklist_dropdown.GetSize() 496 pw_size = self.GetSize() 497 dropdown_size.SetWidth(pw_size.width) 498 dropdown_size.SetHeight ( 499 (pw_size.height * rows) 500 + border_width 501 + extra_height 502 ) 503 504 # recalculate position 505 (pw_x_abs, pw_y_abs) = self.ClientToScreenXY(0,0) 506 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))) 507 dropdown_new_x = pw_x_abs 508 dropdown_new_y = pw_y_abs + pw_size.height 509 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))) 510 self.mac_log('desired dropdown size: %s' % dropdown_size) 511 512 # reaches beyond screen ? 513 if (dropdown_new_y + dropdown_size.height) > self._screenheight: 514 self.mac_log('dropdown extends offscreen (screen max y: %s)' % self._screenheight) 515 max_height = self._screenheight - dropdown_new_y - 4 516 self.mac_log('max dropdown height would be: %s' % max_height) 517 if max_height > ((pw_size.height * 2) + 4): 518 dropdown_size.SetHeight(max_height) 519 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))) 520 self.mac_log('possible dropdown size: %s' % dropdown_size) 521 522 # now set dimensions 523 self.__picklist_dropdown.SetSize(dropdown_size) 524 self._picklist.SetSize(self.__picklist_dropdown.GetClientSize()) 525 self.mac_log('pick list size set to: %s' % self.__picklist_dropdown.GetSize()) 526 if self.__dropdown_needs_relative_position: 527 dropdown_new_x, dropdown_new_y = self.__picklist_dropdown.GetParent().ScreenToClientXY(dropdown_new_x, dropdown_new_y) 528 self.__picklist_dropdown.MoveXY(dropdown_new_x, dropdown_new_y) 529 530 # select first value 531 self._picklist.Select(0) 532 533 # and show it 534 self.__picklist_dropdown.Show(True) 535 536 dd_tl = self.__picklist_dropdown.ClientToScreenXY(0,0) 537 dd_size = self.__picklist_dropdown.GetSize() 538 dd_br = self.__picklist_dropdown.ClientToScreenXY(dd_size.width, dd_size.height) 539 self.mac_log('dropdown placement now (on screen): x:%s-%s, y:%s-%s' % (dd_tl[0], dd_br[0], dd_tl[1], dd_br[1]))
540 #--------------------------------------------------------
541 - def _hide_picklist(self):
542 """Hide the pick list.""" 543 self.__picklist_dropdown.Hide() # dismiss the dropdown list window
544 #--------------------------------------------------------
545 - def __select_picklist_row(self, new_row_idx=None, old_row_idx=None):
546 if old_row_idx is not None: 547 pass # FIXME: do we need unselect here ? Select() should do it for us 548 self._picklist.Select(new_row_idx) 549 self._picklist.EnsureVisible(new_row_idx)
550 #---------------------------------------------------------
551 - def __update_matches_in_picklist(self, val=None):
552 """Get the matches for the currently typed input fragment.""" 553 554 self.input2match = val 555 if self.input2match is None: 556 if self.__phrase_separators is None: 557 self.input2match = self.GetValue().strip() 558 else: 559 # get current(ly relevant part of) input 560 entire_input = self.GetValue() 561 cursor_pos = self.GetInsertionPoint() 562 left_of_cursor = entire_input[:cursor_pos] 563 right_of_cursor = entire_input[cursor_pos:] 564 left_boundary = self.__phrase_separators.search(left_of_cursor) 565 if left_boundary is not None: 566 phrase_start = left_boundary.end() 567 else: 568 phrase_start = 0 569 self.left_part = entire_input[:phrase_start] 570 # find next phrase separator after cursor position 571 right_boundary = self.__phrase_separators.search(right_of_cursor) 572 if right_boundary is not None: 573 phrase_end = cursor_pos + (right_boundary.start() - 1) 574 else: 575 phrase_end = len(entire_input) - 1 576 self.right_part = entire_input[phrase_end+1:] 577 self.input2match = entire_input[phrase_start:phrase_end+1] 578 579 # get all currently matching items 580 if self.matcher is not None: 581 matched, self.__current_matches = self.matcher.getMatches(self.input2match) 582 self._picklist.SetItems(self.__current_matches) 583 584 # no matches found: might simply be due to a typo, so spellcheck 585 if len(self.__current_matches) == 0: 586 if self.speller is not None: 587 # filter out the last word 588 word = regex.split(self.__speller_word_separators, self.input2match)[-1] 589 if word.strip() != u'': 590 success = False 591 try: 592 success = self.speller.check(word) 593 except: 594 _log.exception('had to disable enchant spell checker') 595 self.speller = None 596 if success: 597 spells = self.speller.suggest(word) 598 truncated_input2match = self.input2match[:self.input2match.rindex(word)] 599 for spell in spells: 600 self.__current_matches.append({'label': truncated_input2match + spell, 'data': None}) 601 self._picklist.SetItems(self.__current_matches)
602 #--------------------------------------------------------
604 return self._picklist.GetItemText(self._picklist.GetFirstSelected())
605 #-------------------------------------------------------- 606 # internal helpers: GUI 607 #--------------------------------------------------------
608 - def _on_enter(self):
609 """Called when the user pressed <ENTER>.""" 610 if self.__picklist_dropdown.IsShown(): 611 self._on_list_item_selected() 612 else: 613 # FIXME: check for errors before navigation 614 self.Navigate()
615 #--------------------------------------------------------
616 - def __on_cursor_down(self):
617 618 if self.__picklist_dropdown.IsShown(): 619 selected = self._picklist.GetFirstSelected() 620 if selected < (len(self.__current_matches) - 1): 621 self.__select_picklist_row(selected+1, selected) 622 623 # if we don't yet have a pick list: open new pick list 624 # (this can happen when we TAB into a field pre-filled 625 # with the top-weighted contextual data but want to 626 # select another contextual item) 627 else: 628 self.__timer.Stop() 629 if self.GetValue().strip() == u'': 630 self.__update_matches_in_picklist(val='*') 631 else: 632 self.__update_matches_in_picklist() 633 self._show_picklist()
634 #--------------------------------------------------------
635 - def __on_cursor_up(self):
636 if self.__picklist_dropdown.IsShown(): 637 selected = self._picklist.GetFirstSelected() 638 if selected > 0: 639 self.__select_picklist_row(selected-1, selected) 640 else: 641 # FIXME: input history ? 642 pass
643 #--------------------------------------------------------
644 - def __on_tab(self):
645 """Under certain circumstances takes special action on TAB. 646 647 returns: 648 True: TAB was handled 649 False: TAB was not handled 650 """ 651 if not self.__picklist_dropdown.IsShown(): 652 return False 653 654 if len(self.__current_matches) != 1: 655 return False 656 657 if not self.selection_only: 658 return False 659 660 self.__select_picklist_row(new_row_idx=0) 661 self._on_list_item_selected() 662 663 return True
664 #-------------------------------------------------------- 665 # internal helpers: logic 666 #--------------------------------------------------------
667 - def _create_data(self):
668 raise NotImplementedError('[%s]: cannot create data object' % self.__class__.__name__)
669 #--------------------------------------------------------
670 - def __char_is_allowed(self, char=None):
671 # if undefined accept all chars 672 if self.accepted_chars is None: 673 return True 674 return (self.__accepted_chars.match(char) is not None)
675 #--------------------------------------------------------
676 - def _set_accepted_chars(self, accepted_chars=None):
677 if accepted_chars is None: 678 self.__accepted_chars = None 679 else: 680 self.__accepted_chars = regex.compile(accepted_chars)
681
682 - def _get_accepted_chars(self):
683 if self.__accepted_chars is None: 684 return None 685 return self.__accepted_chars.pattern
686 687 accepted_chars = property(_get_accepted_chars, _set_accepted_chars) 688 #--------------------------------------------------------
689 - def _set_final_regex(self, final_regex='.*'):
690 self.__final_regex = regex.compile(final_regex, flags = regex.LOCALE | regex.UNICODE)
691
692 - def _get_final_regex(self):
693 return self.__final_regex.pattern
694 695 final_regex = property(_get_final_regex, _set_final_regex) 696 #--------------------------------------------------------
697 - def _set_final_regex_error_msg(self, msg):
698 self.__final_regex_error_msg = msg % self.final_regex
699
700 - def _get_final_regex_error_msg(self):
701 return self.__final_regex_error_msg
702 703 final_regex_error_msg = property(_get_final_regex_error_msg, _set_final_regex_error_msg) 704 #--------------------------------------------------------
705 - def _set_phrase_separators(self, phrase_separators):
706 if phrase_separators is None: 707 self.__phrase_separators = None 708 else: 709 self.__phrase_separators = regex.compile(phrase_separators, flags = regex.LOCALE | regex.UNICODE)
710
711 - def _get_phrase_separators(self):
712 if self.__phrase_separators is None: 713 return None 714 return self.__phrase_separators.pattern
715 716 phrase_separators = property(_get_phrase_separators, _set_phrase_separators) 717 #--------------------------------------------------------
718 - def _set_speller_word_separators(self, word_separators):
719 if word_separators is None: 720 self.__speller_word_separators = regex.compile('[\W\d_]+', flags = regex.LOCALE | regex.UNICODE) 721 else: 722 self.__speller_word_separators = regex.compile(word_separators, flags = regex.LOCALE | regex.UNICODE)
723
725 return self.__speller_word_separators.pattern
726 727 speller_word_separators = property(_get_speller_word_separators, _set_speller_word_separators) 728 #--------------------------------------------------------
729 - def __init_timer(self):
730 self.__timer = _cPRWTimer() 731 self.__timer.callback = self._on_timer_fired 732 # initially stopped 733 self.__timer.Stop()
734 #--------------------------------------------------------
735 - def _on_timer_fired(self):
736 """Callback for delayed match retrieval timer. 737 738 if we end up here: 739 - delay has passed without user input 740 - the value in the input field has not changed since the timer started 741 """ 742 # update matches according to current input 743 self.__update_matches_in_picklist() 744 745 # we now have either: 746 # - all possible items (within reasonable limits) if input was '*' 747 # - all matching items 748 # - an empty match list if no matches were found 749 # also, our picklist is refilled and sorted according to weight 750 751 wx.CallAfter(self._show_picklist)
752 #-------------------------------------------------------- 753 # event handling 754 #--------------------------------------------------------
755 - def __register_events(self):
756 wx.EVT_TEXT(self, self.GetId(), self._on_text_update) 757 wx.EVT_KEY_DOWN (self, self._on_key_down) 758 wx.EVT_SET_FOCUS(self, self._on_set_focus) 759 wx.EVT_KILL_FOCUS(self, self._on_lose_focus) 760 self._picklist.Bind(wx.EVT_LEFT_DCLICK, self._on_list_item_selected)
761 #--------------------------------------------------------
762 - def _on_list_item_selected(self, *args, **kwargs):
763 """Gets called when user selected a list item.""" 764 765 self._hide_picklist() 766 self.display_as_valid(valid = True) 767 768 data = self._picklist.GetSelectedItemData() # just so that _picklist_selection2display_string can use it 769 if data is None: 770 return 771 772 self.data = data 773 774 # update our display 775 self.suppress_text_update_smarts = True 776 if self.__phrase_separators is not None: 777 wx.TextCtrl.SetValue(self, u'%s%s%s' % (self.left_part, self._picklist_selection2display_string(), self.right_part)) 778 else: 779 wx.TextCtrl.SetValue(self, self._picklist_selection2display_string()) 780 781 self.data = self._picklist.GetSelectedItemData() 782 self.MarkDirty() 783 784 # and tell the listeners about the user's selection 785 for callback in self._on_selection_callbacks: 786 callback(self.data) 787 788 if self.navigate_after_selection: 789 self.Navigate() 790 else: 791 self.SetInsertionPoint(self.GetLastPosition()) 792 793 return
794 #--------------------------------------------------------
795 - def _on_key_down(self, event):
796 """Is called when a key is pressed.""" 797 798 keycode = event.GetKeyCode() 799 800 if keycode == wx.WXK_DOWN: 801 self.__on_cursor_down() 802 return 803 804 if keycode == wx.WXK_UP: 805 self.__on_cursor_up() 806 return 807 808 if keycode == wx.WXK_RETURN: 809 self._on_enter() 810 return 811 812 if keycode == wx.WXK_TAB: 813 if event.ShiftDown(): 814 self.Navigate(flags = wx.NavigationKeyEvent.IsBackward) 815 return 816 self.__on_tab() 817 self.Navigate(flags = wx.NavigationKeyEvent.IsForward) 818 return 819 820 # FIXME: need PAGE UP/DOWN//POS1/END here to move in picklist 821 if keycode in [wx.WXK_SHIFT, wx.WXK_BACK, wx.WXK_DELETE, wx.WXK_LEFT, wx.WXK_RIGHT]: 822 pass 823 824 # need to handle all non-character key presses *before* this check 825 elif not self.__char_is_allowed(char = unichr(event.GetUnicodeKey())): 826 # FIXME: configure ? 827 wx.Bell() 828 # FIXME: display error message ? Richard doesn't ... 829 return 830 831 event.Skip() 832 return
833 #--------------------------------------------------------
834 - def _on_text_update (self, event):
835 """Internal handler for wx.EVT_TEXT. 836 837 Called when text was changed by user or SetValue(). 838 """ 839 if self.suppress_text_update_smarts: 840 self.suppress_text_update_smarts = False 841 return 842 843 self.data = None 844 self.__current_matches = [] 845 846 # if empty string then hide list dropdown window 847 # we also don't need a timer event then 848 val = self.GetValue().strip() 849 ins_point = self.GetInsertionPoint() 850 if val == u'': 851 self._hide_picklist() 852 self.__timer.Stop() 853 else: 854 new_val = gmTools.capitalize(text = val, mode = self.capitalisation_mode) 855 if new_val != val: 856 self.suppress_text_update_smarts = True 857 wx.TextCtrl.SetValue(self, new_val) 858 if ins_point > len(new_val): 859 self.SetInsertionPointEnd() 860 else: 861 self.SetInsertionPoint(ins_point) 862 # FIXME: SetSelection() ? 863 864 # start timer for delayed match retrieval 865 self.__timer.Start(oneShot = True, milliseconds = self.picklist_delay) 866 867 # notify interested parties 868 for callback in self._on_modified_callbacks: 869 callback() 870 871 return
872 #--------------------------------------------------------
873 - def _on_set_focus(self, event):
874 875 self._has_focus = True 876 event.Skip() 877 878 self.__non_edit_font = self.GetFont() 879 edit_font = self.GetFont() 880 edit_font.SetPointSize(pointSize = self.__non_edit_font.GetPointSize() + 1) 881 self.SetFont(edit_font) 882 self.Refresh() 883 884 # notify interested parties 885 for callback in self._on_set_focus_callbacks: 886 callback() 887 888 self.__timer.Start(oneShot = True, milliseconds = self.picklist_delay) 889 return True
890 #--------------------------------------------------------
891 - def _on_lose_focus(self, event):
892 """Do stuff when leaving the control. 893 894 The user has had her say, so don't second guess 895 intentions but do report error conditions. 896 """ 897 self._has_focus = False 898 899 # don't need timer and pick list anymore 900 self.__timer.Stop() 901 self._hide_picklist() 902 903 # unset selection 904 self.SetSelection(1,1) 905 906 self.SetFont(self.__non_edit_font) 907 self.Refresh() 908 909 is_valid = True 910 911 # the user may have typed a phrase that is an exact match, 912 # however, just typing it won't associate data from the 913 # picklist, so do that now 914 if self.data is None: 915 val = self.GetValue().strip() 916 if val != u'': 917 self.__update_matches_in_picklist() 918 for match in self.__current_matches: 919 if match['label'] == val: 920 self.data = match['data'] 921 self.MarkDirty() 922 break 923 924 # no exact match found 925 if self.data is None: 926 if self.selection_only: 927 wx.lib.pubsub.Publisher().sendMessage ( 928 topic = 'statustext', 929 data = {'msg': self.selection_only_error_msg} 930 ) 931 is_valid = False 932 933 # check value against final_regex if any given 934 if self.__final_regex.match(self.GetValue().strip()) is None: 935 wx.lib.pubsub.Publisher().sendMessage ( 936 topic = 'statustext', 937 data = {'msg': self.final_regex_error_msg} 938 ) 939 is_valid = False 940 941 self.display_as_valid(valid = is_valid) 942 943 # notify interested parties 944 for callback in self._on_lose_focus_callbacks: 945 callback() 946 947 event.Skip() 948 return True
949 #----------------------------------------------------
950 - def mac_log(self, msg):
951 if self.__use_fake_popup: 952 _log.debug(msg)
953 #-------------------------------------------------------- 954 # MAIN 955 #-------------------------------------------------------- 956 if __name__ == '__main__': 957 from Gnumed.pycommon import gmI18N 958 gmI18N.activate_locale() 959 gmI18N.install_domain(domain='gnumed') 960 961 from Gnumed.pycommon import gmPG2, gmMatchProvider 962 963 prw = None 964 #--------------------------------------------------------
965 - def display_values_set_focus(*args, **kwargs):
966 print "got focus:" 967 print "value:", prw.GetValue() 968 print "data :", prw.GetData() 969 return True
970 #--------------------------------------------------------
971 - def display_values_lose_focus(*args, **kwargs):
972 print "lost focus:" 973 print "value:", prw.GetValue() 974 print "data :", prw.GetData() 975 return True
976 #--------------------------------------------------------
977 - def display_values_modified(*args, **kwargs):
978 print "modified:" 979 print "value:", prw.GetValue() 980 print "data :", prw.GetData() 981 return True
982 #--------------------------------------------------------
983 - def display_values_selected(*args, **kwargs):
984 print "selected:" 985 print "value:", prw.GetValue() 986 print "data :", prw.GetData() 987 return True
988 #--------------------------------------------------------
989 - def test_prw_fixed_list():
990 app = wx.PyWidgetTester(size = (200, 50)) 991 992 items = [ {'data':1, 'label':"Bloggs"}, 993 {'data':2, 'label':"Baker"}, 994 {'data':3, 'label':"Jones"}, 995 {'data':4, 'label':"Judson"}, 996 {'data':5, 'label':"Jacobs"}, 997 {'data':6, 'label':"Judson-Jacobs"} 998 ] 999 1000 mp = gmMatchProvider.cMatchProvider_FixedList(items) 1001 # do NOT treat "-" as a word separator here as there are names like "asa-sismussen" 1002 mp.word_separators = '[ \t=+&:@]+' 1003 global prw 1004 prw = cPhraseWheel(parent = app.frame, id = -1) 1005 prw.matcher = mp 1006 prw.capitalisation_mode = gmTools.CAPS_NAMES 1007 prw.add_callback_on_set_focus(callback=display_values_set_focus) 1008 prw.add_callback_on_modified(callback=display_values_modified) 1009 prw.add_callback_on_lose_focus(callback=display_values_lose_focus) 1010 prw.add_callback_on_selection(callback=display_values_selected) 1011 1012 app.frame.Show(True) 1013 app.MainLoop() 1014 1015 return True
1016 #--------------------------------------------------------
1017 - def test_prw_sql2():
1018 print "Do you want to test the database connected phrase wheel ?" 1019 yes_no = raw_input('y/n: ') 1020 if yes_no != 'y': 1021 return True 1022 1023 gmPG2.get_connection() 1024 # FIXME: add callbacks 1025 # FIXME: add context 1026 query = u'select code, name from dem.country where _(name) %(fragment_condition)s' 1027 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query]) 1028 app = wx.PyWidgetTester(size = (200, 50)) 1029 global prw 1030 prw = cPhraseWheel(parent = app.frame, id = -1) 1031 prw.matcher = mp 1032 1033 app.frame.Show(True) 1034 app.MainLoop() 1035 1036 return True
1037 #--------------------------------------------------------
1038 - def test_prw_patients():
1039 gmPG2.get_connection() 1040 query = u"select pk_identity, firstnames || ' ' || lastnames || ' ' || dob::text as pat_name from dem.v_basic_person where firstnames || lastnames %(fragment_condition)s" 1041 1042 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query]) 1043 app = wx.PyWidgetTester(size = (200, 50)) 1044 global prw 1045 prw = cPhraseWheel(parent = app.frame, id = -1) 1046 prw.matcher = mp 1047 1048 app.frame.Show(True) 1049 app.MainLoop() 1050 1051 return True
1052 #--------------------------------------------------------
1053 - def test_spell_checking_prw():
1054 app = wx.PyWidgetTester(size = (200, 50)) 1055 1056 global prw 1057 prw = cPhraseWheel(parent = app.frame, id = -1) 1058 1059 prw.add_callback_on_set_focus(callback=display_values_set_focus) 1060 prw.add_callback_on_modified(callback=display_values_modified) 1061 prw.add_callback_on_lose_focus(callback=display_values_lose_focus) 1062 prw.add_callback_on_selection(callback=display_values_selected) 1063 1064 prw.enable_default_spellchecker() 1065 1066 app.frame.Show(True) 1067 app.MainLoop() 1068 1069 return True
1070 #-------------------------------------------------------- 1071 # test_prw_fixed_list() 1072 # test_prw_sql2() 1073 test_spell_checking_prw() 1074 # test_prw_patients() 1075 1076 #================================================== 1077 # $Log: gmPhraseWheel.py,v $ 1078 # Revision 1.136 2010/02/02 13:55:59 ncq 1079 # - add generic support for can_create in GetData() 1080 # 1081 # Revision 1.135 2009/11/15 01:10:53 ncq 1082 # - cleanup 1083 # 1084 # Revision 1.134 2009/07/23 16:41:42 ncq 1085 # - support custom error msg on final regex mismatch 1086 # 1087 # Revision 1.133 2009/04/24 12:06:01 ncq 1088 # - fix application of final regex: if it was compiled with LOCALE/UNICODE 1089 # one *cannot* apply those flags again on matching ! 1090 # 1091 # Revision 1.132 2009/04/19 22:27:36 ncq 1092 # - enlarge edit font by 1 point only 1093 # 1094 # Revision 1.131 2009/04/03 09:52:10 ncq 1095 # - add explicit shutdown for timers 1096 # - self-handle timers 1097 # - a bit of cleanup 1098 # 1099 # Revision 1.130 2009/03/31 15:08:09 ncq 1100 # - removed gmTimer dependancy 1101 # 1102 # Revision 1.129 2009/03/31 14:38:13 ncq 1103 # - rip out gmDispatcher and use wx.lib.pubsub 1104 # 1105 # Revision 1.128 2009/03/01 18:18:50 ncq 1106 # - factor out default phrase separators/spelling word separators 1107 # 1108 # Revision 1.127 2009/02/24 10:16:48 ncq 1109 # - don't hiccup when spell checker hiccups, simply disable it 1110 # 1111 # Revision 1.126 2009/02/04 21:47:54 ncq 1112 # - cleanup 1113 # 1114 # Revision 1.125 2008/10/12 16:32:40 ncq 1115 # - make more robust when getting data of selected item 1116 # 1117 # Revision 1.124 2008/08/15 15:57:37 ncq 1118 # - enchant doesn't like spellchecking '' anymore 1119 # 1120 # Revision 1.123 2008/07/13 16:14:00 ncq 1121 # - outside code uses color_prw_valid, so leave it and add a comment 1122 # 1123 # Revision 1.122 2008/07/07 11:39:21 ncq 1124 # - separate fake_popup from needs_relative_pos flag 1125 # 1126 # Revision 1.121 2008/06/26 17:03:53 ncq 1127 # - use a wxMiniFrame instead of a wx.Window when emulating wx.PopupWindow 1128 # - adjust for some extra space needed by the wx.MiniFrame 1129 # 1130 # Revision 1.120 2008/06/18 15:48:21 ncq 1131 # - support valid/invalid coloring via display_as_valid 1132 # - cleanup init flow 1133 # 1134 # Revision 1.119 2008/06/15 20:40:43 ncq 1135 # - adjust test suite to match provider properties 1136 # 1137 # Revision 1.118 2008/06/09 15:36:39 ncq 1138 # - increase font size by 2 points when editing 1139 # 1140 # Revision 1.117 2008/05/14 13:46:37 ncq 1141 # - better logging 1142 # 1143 # Revision 1.116 2008/05/13 14:15:16 ncq 1144 # - TAB = select-single-match only when selection_only True 1145 # - improve wxPopupWindow emulation 1146 # 1147 # Revision 1.115 2008/05/07 15:21:44 ncq 1148 # - support suppress smarts argument to SetText 1149 # 1150 # Revision 1.114 2008/04/26 16:29:15 ncq 1151 # - missing if 1152 # 1153 # Revision 1.113 2008/04/26 10:06:37 ncq 1154 # - on MacOSX use relative position for popup window 1155 # 1156 # Revision 1.112 2008/04/26 09:30:28 ncq 1157 # - instrument phrasewheel to exhibit Mac problem 1158 # with dropdown placement 1159 # 1160 # Revision 1.111 2008/01/30 14:03:42 ncq 1161 # - use signal names directly 1162 # - switch to std lib logging 1163 # 1164 # Revision 1.110 2007/10/29 11:30:21 ncq 1165 # - rephrase TODOs 1166 # 1167 # Revision 1.109 2007/09/02 20:56:30 ncq 1168 # - cleanup 1169 # 1170 # Revision 1.108 2007/08/12 00:12:41 ncq 1171 # - no more gmSignals.py 1172 # 1173 # Revision 1.107 2007/07/10 20:27:27 ncq 1174 # - install_domain() arg consolidation 1175 # 1176 # Revision 1.106 2007/07/03 16:03:04 ncq 1177 # - cleanup 1178 # - compile final_regex_error_msg just before using it 1179 # since self.final_regex can have changed 1180 # 1181 # Revision 1.105 2007/05/14 14:43:11 ncq 1182 # - allow TAB to select item from picklist if only one match available 1183 # 1184 # Revision 1.104 2007/05/14 13:11:25 ncq 1185 # - use statustext() signal 1186 # 1187 # Revision 1.103 2007/04/19 13:14:30 ncq 1188 # - don't fail input if enchant/aspell installed but no dict available ... 1189 # 1190 # Revision 1.102 2007/04/02 15:16:55 ncq 1191 # - make spell checker act on last word of phrase only 1192 # - to that end add property speller_word_separators, a 1193 # regex which defaults to standard word boundaries + digits + _ 1194 # 1195 # Revision 1.101 2007/04/02 14:31:35 ncq 1196 # - cleanup 1197 # 1198 # Revision 1.100 2007/04/01 16:33:47 ncq 1199 # - try another parent for the MacOSX popup window 1200 # 1201 # Revision 1.99 2007/03/31 20:09:06 ncq 1202 # - make enchant optional 1203 # 1204 # Revision 1.98 2007/03/27 10:29:49 ncq 1205 # - better placement for default word list 1206 # 1207 # Revision 1.97 2007/03/27 09:59:26 ncq 1208 # - enable_default_spellchecker() 1209 # 1210 # Revision 1.96 2007/02/16 10:22:09 ncq 1211 # - _calc_display_string -> _picklist_selection2display_string to better reflect its use 1212 # 1213 # Revision 1.95 2007/02/06 13:45:39 ncq 1214 # - much improved docs 1215 # - remove aDelay from __init__ and make it a class variable 1216 # - thereby we can now dynamically adjust it at runtime :-) 1217 # - add patient searcher phrasewheel example 1218 # 1219 # Revision 1.94 2007/02/05 12:11:17 ncq 1220 # - put GPL into __license__ 1221 # - code and layout cleanup 1222 # - remove dependancy on gmLog 1223 # - cleanup __init__ interface: 1224 # - remove selection_only 1225 # - remove aMatchProvider 1226 # - set both directly on instance members now 1227 # - implement spell checking plus test case for it 1228 # - implement configurable error messages 1229 # 1230 # Revision 1.93 2007/02/04 18:50:12 ncq 1231 # - capitalisation_mode is now instance variable 1232 # 1233 # Revision 1.92 2007/02/04 16:04:03 ncq 1234 # - reduce imports 1235 # - add accepted_chars constants 1236 # - enhance phrasewheel: 1237 # - better credits 1238 # - cleaner __init__ signature 1239 # - user properties 1240 # - code layout/naming cleanup 1241 # - no more snap_to_first_match for now 1242 # - add capitalisation mode 1243 # - add accepted chars checking 1244 # - add final regex matching 1245 # - allow suppressing recursive _on_text_update() 1246 # - always use time, even in slave mode 1247 # - lots of logic consolidation 1248 # - add SetText() and favour it over SetValue() 1249 # 1250 # Revision 1.91 2007/01/20 22:52:27 ncq 1251 # - .KeyCode -> GetKeyCode() 1252 # 1253 # Revision 1.90 2007/01/18 22:07:52 ncq 1254 # - (Get)KeyCode() -> KeyCode so 2.8 can do 1255 # 1256 # Revision 1.89 2007/01/06 23:44:19 ncq 1257 # - explicitely unset selection on lose focus 1258 # 1259 # Revision 1.88 2006/11/28 20:51:13 ncq 1260 # - a missing self 1261 # - remove some prints 1262 # 1263 # Revision 1.87 2006/11/27 23:08:36 ncq 1264 # - add snap_to_first_match 1265 # - add on_modified callbacks 1266 # - set background in lose_focus in some cases 1267 # - improve test suite 1268 # 1269 # Revision 1.86 2006/11/27 12:42:31 ncq 1270 # - somewhat improved dropdown picklist on Mac, not properly positioned yet 1271 # 1272 # Revision 1.85 2006/11/26 21:42:47 ncq 1273 # - don't use wx.ScrolledWindow or we suffer double-scrollers 1274 # 1275 # Revision 1.84 2006/11/26 20:58:20 ncq 1276 # - try working around lacking wx.PopupWindow 1277 # 1278 # Revision 1.83 2006/11/26 14:51:19 ncq 1279 # - cleanup/improve test suite so we can get MacOSX nailed (down) 1280 # 1281 # Revision 1.82 2006/11/26 14:09:59 ncq 1282 # - fix sys.path when running standalone for test suite 1283 # - fix test suite 1284 # 1285 # Revision 1.81 2006/11/24 09:58:39 ncq 1286 # - cleanup 1287 # - make it really work when matcher is None 1288 # 1289 # Revision 1.80 2006/11/19 11:16:02 ncq 1290 # - remove self._input_was_selected 1291 # 1292 # Revision 1.79 2006/11/06 12:54:00 ncq 1293 # - we don't actually need self._input_was_selected thanks to self.data 1294 # 1295 # Revision 1.78 2006/11/05 16:10:11 ncq 1296 # - cleanup 1297 # - now really handle context 1298 # - add unset_context() 1299 # - stop timer in __init__() 1300 # - start timer in _on_set_focus() 1301 # - some u''-ification 1302 # 1303 # Revision 1.77 2006/10/25 07:24:51 ncq 1304 # - gmPG -> gmPG2 1305 # - match provider _SQL deprecated 1306 # 1307 # Revision 1.76 2006/07/19 20:29:50 ncq 1308 # - import cleanup 1309 # 1310 # Revision 1.75 2006/07/04 14:15:17 ncq 1311 # - lots of cleanup 1312 # - make dropdown list scroll ! :-) 1313 # - add customized list control 1314 # - don't make dropdown go below screen height 1315 # 1316 # Revision 1.74 2006/07/01 15:14:26 ncq 1317 # - lots of cleanup 1318 # - simple border around list dropdown 1319 # - remove on_resize handling 1320 # - remove setdependant() 1321 # - handle down-arrow to drop down list 1322 # 1323 # Revision 1.73 2006/07/01 13:14:50 ncq 1324 # - cleanup as gleaned from TextCtrlAutoComplete 1325 # 1326 # Revision 1.72 2006/06/28 22:16:08 ncq 1327 # - add SetData() -- which only works if data can be found in the match space 1328 # 1329 # Revision 1.71 2006/06/18 13:47:29 ncq 1330 # - set self.input_was_selected=True if SetValue() does have data with it 1331 # 1332 # Revision 1.70 2006/06/05 21:36:40 ncq 1333 # - cleanup 1334 # 1335 # Revision 1.69 2006/06/02 09:59:03 ncq 1336 # - must invalidate associated data object *as soon as* 1337 # the text in the control changes 1338 # 1339 # Revision 1.68 2006/05/31 10:28:27 ncq 1340 # - cleanup 1341 # - deprecation warning for <id_callback> argument 1342 # 1343 # Revision 1.67 2006/05/25 22:24:20 ncq 1344 # - self.__input_was_selected -> self._input_was_selected 1345 # because subclasses need access to it 1346 # 1347 # Revision 1.66 2006/05/24 09:47:34 ncq 1348 # - remove superfluous self._is_modified, use MarkDirty() instead 1349 # - cleanup SetValue() 1350 # - client data in picklist better be object, not string 1351 # - add _calc_display_string() for better reuse in subclasses 1352 # - fix "pick list windows too small if one match" at the cost of extra 1353 # empty row when no horizontal scrollbar needed ... 1354 # 1355 # Revision 1.65 2006/05/20 18:54:15 ncq 1356 # - cleanup 1357 # 1358 # Revision 1.64 2006/05/01 18:49:49 ncq 1359 # - add_callback_on_set_focus() 1360 # 1361 # Revision 1.63 2005/10/09 08:15:21 ihaywood 1362 # SetValue () has optional second parameter to set data. 1363 # 1364 # Revision 1.62 2005/10/09 02:19:40 ihaywood 1365 # the address widget now has the appropriate widget order and behaviour for australia 1366 # when os.environ["LANG"] == 'en_AU' (is their a more graceful way of doing this?) 1367 # 1368 # Remember our postcodes work very differently. 1369 # 1370 # Revision 1.61 2005/10/04 00:04:45 sjtan 1371 # convert to wx.; catch some transitional errors temporarily 1372 # 1373 # Revision 1.60 2005/09/28 21:27:30 ncq 1374 # - a lot of wx2.6-ification 1375 # 1376 # Revision 1.59 2005/09/28 15:57:48 ncq 1377 # - a whole bunch of wx.Foo -> wx.Foo 1378 # 1379 # Revision 1.58 2005/09/26 18:01:51 ncq 1380 # - use proper way to import wx26 vs wx2.4 1381 # - note: THIS WILL BREAK RUNNING THE CLIENT IN SOME PLACES 1382 # - time for fixup 1383 # 1384 # Revision 1.57 2005/08/14 15:37:36 ncq 1385 # - cleanup 1386 # 1387 # Revision 1.56 2005/07/24 11:35:59 ncq 1388 # - use robustified gmTimer.Start() interface 1389 # 1390 # Revision 1.55 2005/07/23 21:55:40 shilbert 1391 # *** empty log message *** 1392 # 1393 # Revision 1.54 2005/07/23 21:10:58 ncq 1394 # - explicitely use milliseconds=-1 in timer.Start() 1395 # 1396 # Revision 1.53 2005/07/23 19:24:58 ncq 1397 # - debug timer start() on windows 1398 # 1399 # Revision 1.52 2005/07/04 11:20:59 ncq 1400 # - cleanup cruft 1401 # - on_set_focus() set value to first match if previously empty 1402 # - on_lose_focus() set value if selection_only and only one match and not yet selected 1403 # 1404 # Revision 1.51 2005/06/14 19:55:37 cfmoro 1405 # Set selection flag when setting value 1406 # 1407 # Revision 1.50 2005/06/07 10:18:23 ncq 1408 # - cleanup 1409 # - setContext -> set_context 1410 # 1411 # Revision 1.49 2005/06/01 23:09:02 ncq 1412 # - set default phrasewheel delay to 150ms 1413 # 1414 # Revision 1.48 2005/05/23 16:42:50 ncq 1415 # - when we SetValue(val) we need to only check those matches 1416 # that actually *can* match, eg the output of getMatches(val) 1417 # 1418 # Revision 1.47 2005/05/22 23:09:13 cfmoro 1419 # Adjust the underlying data when setting the phrasewheel value 1420 # 1421 # Revision 1.46 2005/05/17 08:06:38 ncq 1422 # - support for callbacks on lost focus 1423 # 1424 # Revision 1.45 2005/05/14 15:06:48 ncq 1425 # - GetData() 1426 # 1427 # Revision 1.44 2005/05/05 06:31:06 ncq 1428 # - remove dead cWheelTimer code in favour of gmTimer.py 1429 # - add self._on_enter_callbacks and add_callback_on_enter() 1430 # - addCallback() -> add_callback_on_selection() 1431 # 1432 # Revision 1.43 2005/03/14 14:37:56 ncq 1433 # - only disable timer if slave mode is really active 1434 # 1435 # Revision 1.42 2004/12/27 16:23:39 ncq 1436 # - gmTimer callbacks take a cookie 1437 # 1438 # Revision 1.41 2004/12/23 16:21:21 ncq 1439 # - some cleanup 1440 # 1441 # Revision 1.40 2004/10/16 22:42:12 sjtan 1442 # 1443 # script for unitesting; guard for unit tests where unit uses gmPhraseWheel; fixup where version of wxPython doesn't allow 1444 # a child widget to be multiply inserted (gmDemographics) ; try block for later versions of wxWidgets that might fail 1445 # the Add (.. w,h, ... ) because expecting Add(.. (w,h) ...) 1446 # 1447 # Revision 1.39 2004/09/13 09:24:30 ncq 1448 # - don't start timers in slave_mode since cannot start from 1449 # other than main thread, this is a dirty fix but will do for now 1450 # 1451 # Revision 1.38 2004/06/25 12:30:52 ncq 1452 # - use True/False 1453 # 1454 # Revision 1.37 2004/06/17 11:43:15 ihaywood 1455 # Some minor bugfixes. 1456 # My first experiments with wxGlade 1457 # changed gmPhraseWheel so the match provider can be added after instantiation 1458 # (as wxGlade can't do this itself) 1459 # 1460 # Revision 1.36 2004/05/02 22:53:53 ncq 1461 # - cleanup 1462 # 1463 # Revision 1.35 2004/05/01 10:27:47 shilbert 1464 # - self._picklist.Append() needs string or unicode object 1465 # 1466 # Revision 1.34 2004/03/05 11:22:35 ncq 1467 # - import from Gnumed.<pkg> 1468 # 1469 # Revision 1.33 2004/03/02 10:21:10 ihaywood 1470 # gmDemographics now supports comm channels, occupation, 1471 # country of birth and martial status 1472 # 1473 # Revision 1.32 2004/02/25 09:46:22 ncq 1474 # - import from pycommon now, not python-common 1475 # 1476 # Revision 1.31 2004/01/12 13:14:39 ncq 1477 # - remove dead code 1478 # - correctly calculate new pick list position: don't go to TOPLEVEL 1479 # window but rather to immediate parent ... 1480 # 1481 # Revision 1.30 2004/01/06 10:06:02 ncq 1482 # - make SQL based phrase wheel test work again 1483 # 1484 # Revision 1.29 2003/11/19 23:42:00 ncq 1485 # - cleanup, comment out snap() 1486 # 1487 # Revision 1.28 2003/11/18 23:17:47 ncq 1488 # - cleanup, fixed variable names 1489 # 1490 # Revision 1.27 2003/11/17 10:56:38 sjtan 1491 # 1492 # synced and commiting. 1493 # 1494 # Revision 1.26 2003/11/09 14:28:30 ncq 1495 # - cleanup 1496 # 1497 # Revision 1.25 2003/11/09 02:24:42 ncq 1498 # - added Syans "input was selected from list" state flag to avoid unnecessary list 1499 # drop downs 1500 # - variable name cleanup 1501 # 1502 # Revision 1.24 2003/11/07 20:48:04 ncq 1503 # - place comments where they belong 1504 # 1505 # Revision 1.23 2003/11/05 22:21:06 sjtan 1506 # 1507 # let's gmDateInput specify id_callback in constructor list. 1508 # 1509 # Revision 1.22 2003/11/04 10:35:23 ihaywood 1510 # match providers in gmDemographicRecord 1511 # 1512 # Revision 1.21 2003/11/04 01:40:27 ihaywood 1513 # match providers moved to python-common 1514 # 1515 # Revision 1.20 2003/10/26 11:27:10 ihaywood 1516 # gmPatient is now the "patient stub", all demographics stuff in gmDemographics. 1517 # 1518 # Ergregious breakages are fixed, but needs more work 1519 # 1520 # Revision 1.19 2003/10/09 15:45:16 ncq 1521 # - validate cookie column in score tables, too 1522 # 1523 # Revision 1.18 2003/10/07 22:20:50 ncq 1524 # - ported Syan's extra_sql_condition extension 1525 # - make SQL match provider aware of separate scoring tables 1526 # 1527 # Revision 1.17 2003/10/03 00:20:25 ncq 1528 # - handle case where matches = 1 and match = input -> don't show picklist 1529 # 1530 # Revision 1.16 2003/10/02 20:51:12 ncq 1531 # - add alt-XX shortcuts, move __* to _* 1532 # 1533 # Revision 1.15 2003/09/30 18:52:40 ncq 1534 # - factored out date input wheel 1535 # 1536 # Revision 1.14 2003/09/29 23:11:58 ncq 1537 # - add __explicit_offset() date expander 1538 # 1539 # Revision 1.13 2003/09/29 00:16:55 ncq 1540 # - added date match provider 1541 # 1542 # Revision 1.12 2003/09/21 10:55:04 ncq 1543 # - coalesce merge conflicts due to optional SQL phrase wheel testing 1544 # 1545 # Revision 1.11 2003/09/21 07:52:57 ihaywood 1546 # those bloody umlauts killed by python interpreter! 1547 # 1548 # Revision 1.10 2003/09/17 05:54:32 ihaywood 1549 # phrasewheel box size now approximate to length of search results 1550 # 1551 # Revision 1.8 2003/09/16 22:25:45 ncq 1552 # - cleanup 1553 # - added first draft of single-column-per-table SQL match provider 1554 # - added module test for SQL matcher 1555 # 1556 # Revision 1.7 2003/09/15 16:05:30 ncq 1557 # - allow several phrases to be typed in and only try to match 1558 # the one the cursor is in at the moment 1559 # 1560 # Revision 1.6 2003/09/13 17:46:29 ncq 1561 # - pattern match word separators 1562 # - pattern match ignore characters as per Richard's suggestion 1563 # - start work on phrase separator pattern matching with extraction of 1564 # relevant input part (where the cursor is at currently) 1565 # 1566 # Revision 1.5 2003/09/10 01:50:25 ncq 1567 # - cleanup 1568 # 1569 # 1570