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