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

# Source Code for Module Gnumed.wxpython.gmTextCtrl

      1  __doc__ = """GNUmed TextCtrl sbuclass."""
      2  #===================================================
      3  __author__  = "K. Hilbert <Karsten.Hilbert@gmx.net>"
      4  __license__ = "GPL v2 or later (details at http://www.gnu.org)"
      5
      6  import logging
      7  import sys
      8
      9
     10  import wx
     11  import wx.lib.expando
     12
     13
     14  if __name__ == '__main__':
     15          sys.path.insert(0, '../../')
     16
     17  from Gnumed.pycommon import gmShellAPI
     18  from Gnumed.wxpython import gmKeywordExpansionWidgets
     19
     20
     21  _log = logging.getLogger('gm.txtctrl')
     22
     23  #============================================================
     24  color_tctrl_invalid = 'pink'
     25  color_tctrl_partially_invalid = 'yellow'
     26
     27 -class cColoredStatus_TextCtrlMixin():
     28          """Mixin for setting background color based on validity of content.
     29
     30          Note that due to Python MRO classes using this mixin must
     31          list it before their base class (because we override Enable/Disable).
     32          """
     33 -        def __init__(self, *args, **kwargs):
     34
     35                  if not isinstance(self, (wx.TextCtrl)):
     36                          raise TypeError('[%s]: can only be applied to wx.TextCtrl, not [%s]' % (cColoredStatus_TextCtrlMixin, self.__class__.__name__))
     37
     38                  self.__initial_background_color = self.GetBackgroundColour()
     39                  self.__previous_enabled_bg_color = self.__initial_background_color
     40
     41          #--------------------------------------------------------
     42 -        def display_as_valid(self, valid=None):
     43                  if valid is True:
     44                          color2show = self.__initial_background_color
     45                  elif valid is False:
     46                          color2show = color_tctrl_invalid
     47                  elif valid is None:
     48                          color2show = color_tctrl_partially_invalid
     49                  else:
     50                          raise ValueError('<valid> must be True or False or None')
     51
     52                  if self.IsEnabled():
     53                          self.SetBackgroundColour(color2show)
     54                          self.Refresh()
     55                          return
     56
     57                  # remember for when it gets enabled
     58                  self.__previous_enabled_bg_color = color2show
     59
     60          #--------------------------------------------------------
     61 -        def display_as_disabled(self, disabled=None):
     62                  current_enabled_state = self.IsEnabled()
     63                  desired_enabled_state = disabled is False
     64                  if current_enabled_state is desired_enabled_state:
     65                          return
     66
     67                  if disabled is True:
     68                          self.__previous_enabled_bg_color = self.GetBackgroundColour()
     69                          color2show = wx.SystemSettings.GetColour(wx.SYS_COLOUR_BACKGROUND)
     70                  elif disabled is False:
     71                          color2show = self.__previous_enabled_bg_color
     72                  else:
     73                          raise ValueError('<disabled> must be True or False')
     74
     75                  self.SetBackgroundColour(color2show)
     76                  self.Refresh()
     77
     78          #--------------------------------------------------------
     79 -        def Disable(self):
     80                  self.Enable(enable = False)
     81
     82          #--------------------------------------------------------
     83 -        def Enable(self, enable=True):
     84
     85                  if self.IsEnabled() is enable:
     86                          return
     87
     88                  wx.TextCtrl.Enable(self, enable)
     89
     90                  if enable is True:
     91                          self.SetBackgroundColour(self.__previous_enabled_bg_color)
     92                  elif enable is False:
     93                          self.__previous_enabled_bg_color = self.GetBackgroundColour()
     94                          self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BACKGROUND))
     95                  else:
     96                          raise ValueError('<enable> must be True or False')
     97
     98                  self.Refresh()
     99
    100  #============================================================
    101  _KNOWN_UNICODE_SELECTORS = [
    102          'kcharselect',                  # KDE
    103          'gucharmap',                    # GNOME
    104          'BabelMap.exe',                 # Windows, supposed to be better than charmap.exe
    105          'charmap.exe',                  # Microsoft utility
    106          'gm-unicode2clipboard'  # generic GNUmed workaround
    107          # Mac OSX supposedly features built-in support
    108  ]
    109
    110 -class cUnicodeInsertion_TextCtrlMixin():
    111          """Mixin for inserting unicode characters via selection tool."""
    112
    113          _unicode_selector = None
    114
    115 -        def __init__(self, *args, **kwargs):
    116                  if not isinstance(self, (wx.TextCtrl, wx.stc.StyledTextCtrl)):
    117                          raise TypeError('[%s]: can only be applied to wx.TextCtrl or wx.stc.StyledTextCtrl, not [%s]' % (cUnicodeInsertion_TextCtrlMixin, self.__class__.__name__))
    118
    119                  if cUnicodeInsertion_TextCtrlMixin._unicode_selector is None:
    120                          found, cUnicodeInsertion_TextCtrlMixin._unicode_selector = gmShellAPI.find_first_binary(binaries = _KNOWN_UNICODE_SELECTORS)
    121                          if found:
    122                                  _log.debug('found [%s] for unicode character selection', cUnicodeInsertion_TextCtrlMixin._unicode_selector)
    123                          else:
    124                                  _log.error('no unicode character selection tool found')
    125
    126          #--------------------------------------------------------
    127 -        def mixin_insert_unicode_character(self):
    128                  if cUnicodeInsertion_TextCtrlMixin._unicode_selector is None:
    129                          return False
    130
    131                  # read clipboard
    132                  if wx.TheClipboard.IsOpened():
    133                          _log.error('clipboard already open')
    134                          return False
    135                  if not wx.TheClipboard.Open():
    136                          _log.error('cannot open clipboard')
    137                          return False
    138                  data_obj = wx.TextDataObject()
    139                  prev_clip = None
    140                  got_it = wx.TheClipboard.GetData(data_obj)
    141                  if got_it:
    142                          prev_clip = data_obj.Text
    143
    144                  # run selector
    145                  if not gmShellAPI.run_command_in_shell(command = cUnicodeInsertion_TextCtrlMixin._unicode_selector, blocking = True):
    146                          wx.TheClipboard.Close()
    147                          return False
    148
    149                  # read clipboard again
    150                  got_it = wx.TheClipboard.GetData(data_obj)
    151                  wx.TheClipboard.Close()
    152                  if not got_it:
    153                          _log.debug('clipboard does not contain text')
    154                          return False
    155                  curr_clip = data_obj.Text
    156
    157                  # insert clip if so
    158                  if curr_clip == prev_clip:
    159                          # nothing put into clipboard (that is, clipboard still the same)
    160                          return False
    161
    162                  self.WriteText(curr_clip)
    163                  return True
    164
    165  #============================================================
    166 -class cTextSearch_TextCtrlMixin():
    167          """Code using classes with this mixin must call
    168             show_find_dialog() at appropriate times. Everything
    169             else will be handled.
    170          """
    171 -        def __init__(self, *args, **kwargs):
    172                  if not isinstance(self, (wx.TextCtrl, wx.stc.StyledTextCtrl)):
    173                          raise TypeError('[%s]: can only be applied to wx.TextCtrl or wx.stc.StyledTextCtrl, not [%s]' % (cTextSearch_TextCtrlMixin, self.__class__.__name__))
    174
    175                  self.__mixin_find_replace_data = None
    176                  self.__mixin_find_replace_dlg = None
    177                  self.__mixin_find_replace_last_match_start = 0
    178                  self.__mixin_find_replace_last_match_end = 0
    179                  self.__mixin_find_replace_last_match_attr = None
    180
    181          #--------------------------------------------------------
    182 -        def show_find_dialog(self, title=None):
    183
    184                  if self.__mixin_find_replace_dlg is not None:
    185                          return self.__mixin_find_replace_dlg
    186
    187                  self.__mixin_find_replace_last_match_end = 0
    188
    189                  if title is None:
    190                          title = _('Find text')
    191                  self.__mixin_find_replace_data = wx.FindReplaceData()
    192                  self.__mixin_find_replace_dlg = wx.FindReplaceDialog (
    193                          self,
    194                          self.__mixin_find_replace_data,
    195                          title,
    196                          wx.FR_NOUPDOWN | wx.FR_NOMATCHCASE | wx.FR_NOWHOLEWORD
    197                  )
    198                  self.__mixin_find_replace_dlg.Bind(wx.EVT_FIND, self._mixin_on_find)
    199                  self.__mixin_find_replace_dlg.Bind(wx.EVT_FIND_NEXT, self._mixin_on_find)
    200                  self.__mixin_find_replace_dlg.Bind(wx.EVT_FIND_CLOSE, self._mixin_on_find_close)
    201                  self.__mixin_find_replace_dlg.Show()
    202                  return self.__mixin_find_replace_dlg
    203
    204          #--------------------------------------------------------
    205          # events
    206          #--------------------------------------------------------
    207 -        def _mixin_on_find(self, evt):
    208
    209                  # reset style of previous match
    210                  if self.__mixin_find_replace_last_match_attr is not None:
    211                          self.SetStyle (
    212                                  self.__mixin_find_replace_last_match_start,
    213                                  self.__mixin_find_replace_last_match_end,
    214                                  self.__mixin_find_replace_last_match_attr
    215                          )
    216
    217                  # find current match
    218                  search_term = self.__mixin_find_replace_data.GetFindString().lower()
    219                  match_start = self.Value.lower().find(search_term, self.__mixin_find_replace_last_match_end)
    220                  if match_start == -1:
    221                          # wrap around
    222                          self.__mixin_find_replace_last_match_start = 0
    223                          self.__mixin_find_replace_last_match_end = 0
    224                          wx.Bell()
    225                          return
    226
    227                  # remember current match for next time around
    228                  attr = wx.TextAttr()
    229                  if self.GetStyle(match_start, attr):
    230                          self.__mixin_find_replace_last_match_attr = attr
    231                  else:
    232                          self.__mixin_find_replace_last_match_attr = None
    233                  self.__mixin_find_replace_last_match_start = match_start
    234                  self.__mixin_find_replace_last_match_end = match_start + len(search_term)
    235
    236                  # react to current match
    237                  self.Freeze()
    238                  self.SetStyle (
    239                          self.__mixin_find_replace_last_match_start,
    240                          self.__mixin_find_replace_last_match_end,
    241                          wx.TextAttr("red", "black")
    242                  )
    243                  self.ShowPosition(0)
    244                  self.ShowPosition(self.__mixin_find_replace_last_match_end)
    245                  self.Thaw()
    246
    247          #--------------------------------------------------------
    248 -        def _mixin_on_find_close(self, evt):
    249                  # cleanup after last match if any
    250                  if self.__mixin_find_replace_last_match_attr is not None:
    251                          self.SetStyle (
    252                                  self.__mixin_find_replace_last_match_start,
    253                                  self.__mixin_find_replace_last_match_end,
    254                                  self.__mixin_find_replace_last_match_attr
    255                          )
    256                  # deactivate the events
    257                  self.__mixin_find_replace_dlg.Unbind(wx.EVT_FIND)
    258                  self.__mixin_find_replace_dlg.Unbind(wx.EVT_FIND_NEXT)
    259                  self.__mixin_find_replace_dlg.Unbind(wx.EVT_FIND_CLOSE)
    260                  # unshow dialog
    261                  self.__mixin_find_replace_dlg.DestroyLater()
    262                  self.__mixin_find_replace_data = None
    263                  self.__mixin_find_replace_dlg = None
    264                  self.__mixin_find_replace_last_match_end = 0
    265
    266  #============================================================
    267 -class cTextCtrl(gmKeywordExpansionWidgets.cKeywordExpansion_TextCtrlMixin, cTextSearch_TextCtrlMixin, cColoredStatus_TextCtrlMixin, cUnicodeInsertion_TextCtrlMixin, wx.TextCtrl):
    268
    269 -        def __init__(self, *args, **kwargs):
    270
    271                  self._on_set_focus_callbacks = []
    272                  self._on_lose_focus_callbacks = []
    273                  self._on_modified_callbacks = []
    274
    275                  wx.TextCtrl.__init__(self, *args, **kwargs)
    276                  gmKeywordExpansionWidgets.cKeywordExpansion_TextCtrlMixin.__init__(self)
    277                  cTextSearch_TextCtrlMixin.__init__(self)
    278                  cColoredStatus_TextCtrlMixin.__init__(self)
    279                  cUnicodeInsertion_TextCtrlMixin.__init__(self)
    280
    281                  self.enable_keyword_expansions()
    282
    283          #--------------------------------------------------------
    284          # callback API
    285          #--------------------------------------------------------
    286 -        def add_callback_on_set_focus(self, callback=None):
    287                  """Add a callback for invocation when getting focus."""
    288                  if not callable(callback):
    289                          raise ValueError('[add_callback_on_set_focus]: ignoring callback [%s] - not callable' % callback)
    290
    291                  self._on_set_focus_callbacks.append(callback)
    292                  if len(self._on_set_focus_callbacks) == 1:
    293                          self.Bind(wx.EVT_SET_FOCUS, self._on_set_focus)
    294          #---------------------------------------------------------
    295 -        def add_callback_on_lose_focus(self, callback=None):
    296                  """Add a callback for invocation when losing focus."""
    297                  if not callable(callback):
    298                          raise ValueError('[add_callback_on_lose_focus]: ignoring callback [%s] - not callable' % callback)
    299
    300                  self._on_lose_focus_callbacks.append(callback)
    301                  if len(self._on_lose_focus_callbacks) == 1:
    302                          self.Bind(wx.EVT_KILL_FOCUS, self._on_lose_focus)
    303          #---------------------------------------------------------
    304 -        def add_callback_on_modified(self, callback=None):
    305                  """Add a callback for invocation when the content is modified.
    306
    307                  This callback will NOT be passed any values.
    308                  """
    309                  if not callable(callback):
    310                          raise ValueError('[add_callback_on_modified]: ignoring callback [%s] - not callable' % callback)
    311
    312                  self._on_modified_callbacks.append(callback)
    313                  if len(self._on_modified_callbacks) == 1:
    314                          self.Bind(wx.EVT_TEXT, self._on_text_update)
    315
    316          #--------------------------------------------------------
    317          # event handlers
    318          #--------------------------------------------------------
    319 -        def _on_set_focus(self, event):
    320                  event.Skip()
    321                  for callback in self._on_set_focus_callbacks:
    322                          callback()
    323                  return True
    324          #--------------------------------------------------------
    325 -        def _on_lose_focus(self, event):
    326                  """Do stuff when leaving the control.
    327
    328                  The user has had her say, so don't second guess
    329                  intentions but do report error conditions.
    330                  """
    331                  event.Skip()
    332                  wx.CallAfter(self.__on_lost_focus)
    333                  return True
    334          #--------------------------------------------------------
    335 -        def __on_lost_focus(self):
    336                  for callback in self._on_lose_focus_callbacks:
    337                          callback()
    338          #--------------------------------------------------------
    339 -        def _on_text_update (self, event):
    340                  """Internal handler for wx.EVT_TEXT.
    341
    342                  Called when text was changed by user or by SetValue().
    343                  """
    344                  for callback in self._on_modified_callbacks:
    345                          callback()
    346                  return
    347
    348  #============================================================
    349  # expando based text ctrl classes
    350  #============================================================
    351 -class cExpandoTextCtrlHandling_PanelMixin():
    352          """Mixin for panels wishing to handel expand text ctrls within themselves.
    353
    354          Panels using this mixin will need to call
    355
    356                  self.bind_expando_layout_event(<expando_field>)
    357
    358          on each <expando_field> they wish to auto-expand.
    359          """
    360          #--------------------------------------------------------
    361 -        def bind_expando_layout_event(self, expando):
    362                  self.Bind(wx.lib.expando.EVT_ETC_LAYOUT_NEEDED, self._on_expando_needs_layout)
    363                  #wx.lib.expando.EVT_ETC_LAYOUT_NEEDED(expando, expando.GetId(), self._on_expando_needs_layout)
    364
    365          #--------------------------------------------------------
    366 -        def _on_expando_needs_layout(self, evt):
    367                  # need to tell ourselves to re-Layout to refresh scroll bars
    368
    369                  # provoke adding scrollbar if needed
    370                  #self.Fit()                             # works on Linux but not on Windows
    371                  self.FitInside()                # needed on Windows rather than self.Fit()
    372
    373                  if self.HasScrollbar(wx.VERTICAL):
    374                          # scroll panel to show cursor
    375                          expando = self.FindWindowById(evt.GetId())
    376                          y_expando = expando.GetPositionTuple()[1]
    377                          h_expando = expando.GetSize()[1]
    378                          line_of_cursor = expando.PositionToXY(expando.GetInsertionPoint())[2] + 1
    379                          if expando.NumberOfLines == 0:
    380                                  no_of_lines = 1
    381                          else:
    382                                  no_of_lines = expando.NumberOfLines
    383                          y_cursor = int(round((float(line_of_cursor) / no_of_lines) * h_expando))
    384                          y_desired_visible = y_expando + y_cursor
    385
    386                          y_view = self.ViewStart[1]
    387                          h_view = self.GetClientSize()[1]
    388
    389  #                       print "expando:", y_expando, "->", h_expando, ", lines:", expando.NumberOfLines
    390  #                       print "cursor :", y_cursor, "at line", line_of_cursor, ", insertion point:", expando.GetInsertionPoint()
    391  #                       print "wanted :", y_desired_visible
    392  #                       print "view-y :", y_view
    393  #                       print "scroll2:", h_view
    394
    395                          # expando starts before view
    396                          if y_desired_visible < y_view:
    397  #                               print "need to scroll up"
    398                                  self.Scroll(0, y_desired_visible)
    399
    400                          if y_desired_visible > h_view:
    401  #                               print "need to scroll down"
    402                                  self.Scroll(0, y_desired_visible)
    403
    404  #============================================================
    405 -class cExpandoTextCtrl(gmKeywordExpansionWidgets.cKeywordExpansion_TextCtrlMixin, cTextSearch_TextCtrlMixin, cColoredStatus_TextCtrlMixin, wx.lib.expando.ExpandoTextCtrl):
    406          """Expando based text ctrl
    407
    408          - auto-sizing on input
    409          - keyword based text expansion
    410          - text search on show_find_dialog()
    411          - (on demand) status based background color
    412
    413          Parent panels should apply the cExpandoTextCtrlHandling_PanelMixin.
    414          """
    415 -        def __init__(self, *args, **kwargs):
    416
    417                  wx.lib.expando.ExpandoTextCtrl.__init__(self, *args, **kwargs)
    418                  gmKeywordExpansionWidgets.cKeywordExpansion_TextCtrlMixin.__init__(self)
    419                  cTextSearch_TextCtrlMixin.__init__(self)
    420                  cColoredStatus_TextCtrlMixin.__init__(self)
    421
    422                  self.__register_interests()
    423                  self.enable_keyword_expansions()
    424
    425          #------------------------------------------------
    426          # event handling
    427          #------------------------------------------------
    428 -        def __register_interests(self):
    429                  self.Bind(wx.EVT_SET_FOCUS, self.__cExpandoTextCtrl_on_focus)
    430
    431          #--------------------------------------------------------
    432 -        def __cExpandoTextCtrl_on_focus(self, evt):
    433                  evt.Skip()      # allow other processing to happen
    434                  wx.CallAfter(self._cExpandoTextCtrl_after_on_focus)
    435
    436          #--------------------------------------------------------
    437 -        def _cExpandoTextCtrl_after_on_focus(self):
    438                  # robustify against Py__DeadObjectError (RuntimeError) - since
    439                  # we are called from wx's CallAfter this SoapCtrl may be gone
    440                  # by the time we get around to handling this layout request,
    441                  # say, on patient change or some such
    442                  if not self:
    443                          return
    444
    445                  #wx. CallAfter(self._adjustCtrl)
    446                  evt = wx.PyCommandEvent(wx.lib.expando.wxEVT_ETC_LAYOUT_NEEDED, self.GetId())
    447                  evt.SetEventObject(self)
    448                  #evt.height = None
    449                  #evt.numLines = None
    450                  #evt.height = self.GetSize().height
    451                  #evt.numLines = self.GetNumberOfLines()
    452                  self.GetEventHandler().ProcessEvent(evt)
    453
    454          #------------------------------------------------
    455          # fix platform expando.py if needed
    456          #------------------------------------------------
    457 -        def _wrapLine(self, line, dc, max_width):
    458
    459                  if wx.MAJOR_VERSION > 2:
    460                          return wx.lib.expando.ExpandoTextCtrl._wrapLine(self, line, dc, max_width)
    461
    462                  if (wx.MAJOR_VERSION == 2) and (wx.MINOR_VERSION > 8):
    463                          return wx.lib.expando.ExpandoTextCtrl._wrapLine(self, line, dc, max_width)
    464
    465                  # THIS FIX LIFTED FROM TRUNK IN SVN:
    466                  # Estimate where the control will wrap the lines and
    467                  # return the count of extra lines needed.
    468                  partial_text_extents = dc.GetPartialTextExtents(line)
    469                  max_width -= wx.SystemSettings.GetMetric(wx.SYS_VSCROLL_X)
    470                  idx = 0
    471                  start = 0
    472                  count_of_extra_lines_needed = 0
    473                  idx_of_last_blank = -1
    474                  while idx < len(partial_text_extents):
    475                      if line[idx] == ' ':
    476                          idx_of_last_blank = idx
    477                      if (partial_text_extents[idx] - start) > max_width:
    478                          # we've reached the max width, add a new line
    479                          count_of_extra_lines_needed += 1
    480                          # did we see a space? if so restart the count at that pos
    481                          if idx_of_last_blank != -1:
    482                              idx = idx_of_last_blank + 1
    483                              idx_of_last_blank = -1
    484                          if idx < len(partial_text_extents):
    485                              start = partial_text_extents[idx]
    486                      else:
    487                          idx += 1
    488                  return count_of_extra_lines_needed
    489
    490  #===================================================
    491  # main
    492  #---------------------------------------------------
    493  if __name__ == '__main__':
    494
    495          if len(sys.argv) < 2:
    496                  sys.exit()
    497
    498          if sys.argv[1] != 'test':
    499                  sys.exit()
    500
    501          from Gnumed.pycommon import gmI18N
    502          gmI18N.activate_locale()
    503          gmI18N.install_domain(domain='gnumed')
    504
    505          #-----------------------------------------------
    506 -        def test_gm_textctrl():
    507                  app = wx.PyWidgetTester(size = (200, 50))
    508                  tc = cTextCtrl(app.frame, -1)
    509                  #tc.enable_keyword_expansions()
    510                  #tc.Enable(False)
    511                  app.frame.Show(True)
    512                  app.MainLoop()
    513                  return True
    514          #-----------------------------------------------
    515          test_gm_textctrl()
    516