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