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