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