Gnumed.wxpython.gmPhraseWheel

Package Gnumed :: Package wxpython :: Module gmPhraseWheel |
[frames] | no frames]

# Source Code for Module Gnumed.wxpython.gmPhraseWheel

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