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