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

# Source Code for Module Gnumed.wxpython.gmResizingWidgets

       1  """gmResizingWidgets - Resizing widgets for use in GNUmed.
       2
       3  Design by Richard Terry and Ian Haywood.
       4  """
       5  #====================================================================
       6  # $Source: /home/ncq/Projekte/cvs2git/vcs-mirror/gnumed/gnumed/client/wxpython/gmResizingWidgets.py,v $
       7  # $Id: gmResizingWidgets.py,v 1.55 2009-03-10 14:24:15 ncq Exp $
       8  __version__ = "$Revision: 1.55 $"
       9  __author__ = "Ian Haywood, Karsten Hilbert, Richard Terry"
      10  __license__ = 'GPL v2 or later (details at http://www.gnu.org)'
      11
      12  import sys, logging, re as regex
      13
      14
      15  import wx
      16  import wx.stc
      17
      18
      19  from Gnumed.pycommon import gmI18N, gmDispatcher, gmPG2
      20  from Gnumed.wxpython import gmGuiHelpers, gmTimer
      21
      22  _log = logging.getLogger('gm.ui')
      23  _log.info(__version__)
      24
      25  STYLE_ERROR=1
      26  STYLE_TEXT=2
      27  STYLE_EMBED=4
      28
      29  #====================================================================
      30 -class cPickList(wx.ListBox):
      31 -        def __init__ (self, parent, pos, size, callback):
      32                  wx.ListBox.__init__(self, parent, -1, pos, size, style=wx.LB_SINGLE | wx.LB_NEEDED_SB)
      33                  self.callback = callback
      34                  self.alive = 1 # 0=dead, 1=alive, 2=must die
      35                  wx.EVT_LISTBOX (self, self.GetId(), self.OnList)
      36          #------------------------------------------------
      37 -        def SetItems (self, items):
      38                  """
      39                  Sets the items, Items is a dict with label, data, weight items
      40                  """
      41                  items.sort (lambda a,b: cmp(b['weight'], a['weight']))
      42                  self.Clear()
      43                  self.Set([item['label'] for item in items])
      44                  n = 0
      45                  for item in items:
      46                          self.SetClientData(n, item['data'])
      47                          # n += 1  ??
      48                  self.SetSelection(0)
      49          #------------------------------------------------
      50 -        def Up(self):
      51                  line = self.GetSelection()
      52                  if line > 0:
      53                          self.SetSelection(line-1)
      54          #------------------------------------------------
      55 -        def Down(self):
      56                  line = self.GetSelection()
      57                  if line < self.GetCount()-1:
      58                          self.SetSelection(line+1)
      59          #------------------------------------------------
      60 -        def Enter (self):
      61                  line = self.GetSelection()
      62                  if line >= 0:
      63                          text = self.GetString(line)
      64                          data = self.GetClientData(line)
      65                          self.callback(text, data)
      66                  self.alive = 2
      67                  self.Destroy() # this is only safe when in the event handler of another widget
      68          #------------------------------------------------
      69 -        def OnList(self, event):
      70                  event.Skip()
      71                  if self.alive != 2:
      72                          line = self.GetSelection()
      73                          if line >= 0:
      74                                  text = self.GetString(line)
      75                                  data = self.GetClientData(line)
      76                                  self.callback (text, data)
      77                          self.alive = 2
      78                  else:
      79                          wx.CallAfter (self.Destroy) # in theory we shouldn't have to do this,
      80                                                                             # but when we don't, wx segfaults.
      81          #------------------------------------------------
      82 -        def Destroy (self):
      83                  self.alive = 0
      84                  wx.ListBox.Destroy (self)
      85  #====================================================================
      86  # according to Ian there isn't really a particular reason
      87  # why we do not use wxMiniFrame instead of wx.Frame or even a wxWindow
      88 -class cPopupFrame(wx.Frame):
      89  #       def __init__ (self, embed_header, widget_class, originator=None, pos=wx.DefaultPosition):
      90  #               wx.Frame.__init__(self, None, wxNewId(), widget_class.__name__, pos=pos, style=wx.SIMPLE_BORDER)
      91  #               self.win = widget_class(self, -1, pos = pos, size = wx.Size(300, 150), complete = self.OnOK)
      92 -        def __init__ (self, embed_header, widget, originator=None, pos=wx.DefaultPosition):
      93                  wx.Frame.__init__(self, None, wx.NewId(), widget.__class__.__name__, pos=pos, style=wx.SIMPLE_BORDER)
      94                  widget.set_completion_callback(self.OnOK)
      95                  self.win = widget
      96                  self.embed_header = embed_header
      97                  self.originator = originator
      98
      99                  self.__do_layout()
     100
     101                  wx.EVT_BUTTON(self.__BTN_OK, self.__BTN_OK.GetId(), self.OnOK)
     102                  wx.EVT_BUTTON(self.__BTN_Cancel, self.__BTN_Cancel.GetId(), self._on_close)
     103                  self.win.SetFocus ()
     104          #------------------------------------------------
     105 -        def __do_layout(self):
     106                  self.__BTN_OK = wx.Button (self, -1, _("OK"), style=wx.BU_EXACTFIT)
     107                  self.__BTN_Cancel = wx.Button (self, -1, _("Cancel"), style=wx.BU_EXACTFIT)
     108                  szr_btns = wx.BoxSizer (wx.HORIZONTAL)
     109                  szr_btns.Add(self.__BTN_OK, 0, 0)
     110                  szr_btns.Add(self.__BTN_Cancel, 0, 0)
     111
     112                  szr_main = wx.BoxSizer(wx.VERTICAL)
     113                  szr_main.Add(self.win, 1, wx.EXPAND, 0)
     114                  szr_main.Add(szr_btns, 0, wx.EXPAND)
     115
     116                  self.SetAutoLayout(1)
     117                  self.SetSizer(szr_main)
     118                  szr_main.Fit(self)
     119                  szr_main.SetSizeHints(self)
     120                  self.Layout()
     121          #------------------------------------------------
     122 -        def _on_close (self, event):
     123                  self.Close()
     124          #------------------------------------------------
     125 -        def OnOK (self, event=None):
     126                  if self.originator:
     127                          self.originator.Embed ("%s: %s" % (self.embed_header, self.win.GetSummary()))
     128                  self.Close ()
     129  #====================================================================
     130 -class cSTCval:
     131 -        def __init__(self):
     132                  self.text = None
     133                  self.data = None
     134  #====================================================================
     135 -class cResizingWindow(wx.ScrolledWindow):
     136          """A vertically-scrolled window which allows subwindows
     137             to change their size, and adjusts accordingly.
     138          """
     139 -        def __init__ (self, parent, id, pos = wx.DefaultPosition, size = wx.DefaultSize):
     140
     141                  wx.ScrolledWindow.__init__(self, parent, id, pos = pos, size = size, style=wx.VSCROLL)
     142                  self.SetScrollRate(0, 20) # suppresses X scrolling by setting X rate to zero
     143
     144  #               self.__list = None
     145  #               self.complete = complete        # ??
     146
     147                  self.__input_lines = [[]]
     148                  self.__szr_main = None
     149                  self.DoLayout()
     150                  self.__szr_main = wx.FlexGridSizer(len(self.__input_lines), 2)
     151                  for line in self.__input_lines:
     152                          if len(line) != 0:
     153                                  # first label goes into column 1
     154                                  if line[0]['label'] is not None:
     155                                          self.__szr_main.Add(line[0]['label'], 1)
     156                                  else:
     157                                          self.__szr_main.Add((1, 1))
     158                                  # the rest gets crammed into column 2
     159                                  h_szr = wx.BoxSizer (wx.HORIZONTAL)
     160                                  h_szr.Add(line[0]['instance'], 1, wx.EXPAND)
     161                                  for widget in line[1:]:
     162                                          if widget['label'] is not None:
     163                                                  h_szr.Add(widget['label'], 0)
     164                                          h_szr.Add(widget['instance'], 1, wx.EXPAND)
     165                                  self.__szr_main.Add(h_szr, 1, wx.EXPAND)
     166                  self.__szr_main.AddGrowableCol(1)
     167                  self.__szr_main.Add((1, 1))
     168
     169                  self.SetSizer(self.__szr_main)
     170                  self.__szr_main.Fit(self)
     171                  self.FitInside()
     172          #------------------------------------------------
     173 -        def AddWidget(self, widget, label=None):
     174                  """
     175                  Adds a widget, optionally with label
     176
     177                  @type label: string
     178                  @param label: text of the label
     179                  @type widgets: wx.Window descendant
     180                  """
     181                  if label is None:
     182                          textbox = None
     183                  else:
     184                          textbox = wx.StaticText(self, -1, label, style=wx.ALIGN_RIGHT)
     185                  # append to last line
     186                  self.__input_lines[-1].append({'ID': label, 'label': textbox, 'instance': widget})
     187          #------------------------------------------------
     188 -        def Newline (self):
     189                  """
     190                  Starts a newline on the widget
     191                  """
     192                  self.__input_lines.append([])
     193          #------------------------------------------------
     194 -        def DoLayout (self):
     195                  """
     196                  Overridden by descendants, this function uses AddWidget and Newline to form
     197                  the outline of the widget
     198                  """
     199                  _log.error('[%s] forgot to override DoLayout()' % self.__class__.__name__)
     200          #------------------------------------------------
     201 -        def ReSize (self, widget, new_height):
     202                  """Called when a child widget has a new height, redoes the layout.
     203                  """
     204                  if self.__szr_main is not None:
     205                          self.__szr_main.SetItemMinSize(widget, -1, new_height)
     206                          self.__szr_main.FitInside(self)
     207          #------------------------------------------------
     208 -        def EnsureVisible (self, widget, cur_x = 0, cur_y = 0):
     209                  """
     210                  Ensures widget is visible
     211
     212                  @param widget: a child widget
     213                  @type cur_x: integer
     214                  @param cur_x: the X co-ordinate of the cursor inside widget, if applicable
     215                  @type cur_y: integer
     216                  @param cur_y: the Y co-ordinate of the cursor inside widget
     217                  """
     218                  # get widget position
     219                  x, y = widget.GetPositionTuple()
     220                  # adjust for cursor offset
     221                  x += cur_x
     222                  y += cur_y
     223                  # convert to virtual coordinates
     224                  x, y = self.CalcUnscrolledPosition(x, y)
     225                  x_dimension, y_dimension = self.GetScrollPixelsPerUnit()
     226                  y = y / y_dimension
     227                  # currently, don't bother with X direction
     228                  self.Scroll (-1, y)
     229          #------------------------------------------------
     230 -        def SetValue(self, values):
     231                  """
     232                  Runs SetValue() on all the fields
     233
     234                  @type values: dictionary
     235                  @param values: keys are the labels, values are passed to SetValue()
     236                  """
     237                  # FIXME: adapt to cSTCval
     238                  for line in self.__input_lines:
     239                          for widget in line:
     240                                  if values.has_key(widget['ID']):
     241                                          if isinstance(widget['instance'], wx.stc.StyledTextCtrl):
     242                                                  widget['instance'].SetText(values[widget['ID']])
     243                                          elif isinstance(widget['instance'], (wx.Choice, wx.RadioBox)):
     244                                                  widget['instance'].SetSelection(values[widget['ID']])
     245                                          else:
     246                                                  widget['instance'].SetValue(values[widget['ID']])
     247          #------------------------------------------------
     248 -        def GetValue(self):
     249                  """Return dict of values of inner widgets.
     250
     251                  Returns a dictionary of the results of GetValue()
     252                  called on all widgets, keyed by label
     253                  Unlabelled widgets don't get called
     254                  """
     255                  # FIXME: this does not detect ID collisions between lines
     256                  vals = {}
     257                  for line in self.__input_lines:
     258                          for widget in line:
     259                                  if widget['ID'] is None:
     260                                          continue
     261                                  result = cSTCval()
     262                                  if isinstance(widget['instance'], cResizingSTC):
     263                                          result.text = widget['instance'].GetText()
     264                                          result.data = widget['instance'].GetData()
     265                                  elif isinstance(widget['instance'], wx.stc.StyledTextCtrl):
     266                                          result.text = widget['instance'].GetText()
     267                                  elif isinstance(widget['instance'], (wx.Choice, wx.RadioBox)):
     268                                          result.selection = widget['instance'].GetSelection()
     269                                  else:
     270                                          result.value = widget['instance'].GetValue()
     271                                  vals[widget['ID']] = result
     272                  return vals
     273          #------------------------------------------------
     274 -        def Clear (self):
     275                  """
     276                  Clears all widgets where this makes sense
     277                  """
     278                  for line in self.__input_lines:
     279                          for widget in line:
     280                                  if isinstance (widget['instance'], wx.stc.StyledTextCtrl):
     281                                          widget['instance'].ClearAll()
     282                                  elif isinstance (widget['instance'], wx.TextCtrl):
     283                                          widget['instance'].Clear()
     284                                  elif isinstance (widget['instance'], (wx.ToggleButton, wx.CheckBox, wx.RadioButton, wx.Gauge)):
     285                                          widget['instance'].SetValue(0)
     286                                  elif isinstance (widget['instance'], (wx.Choice, wx.ComboBox, wx.RadioBox)):
     287                                          widget['instance'].SetSelection(0)
     288                                  elif isinstance (widget['instance'], wx.SpinCtrl):
     289                                          widget['instance'].SetValue(widget['instance'].GetMin())
     290          #------------------------------------------------
     291 -        def SetFocus (self):
     292                  # try to focus on the first line if we can.
     293                  try:
     294                          self.lines[0][0]['instance'].SetFocus()
     295                  except IndexError:
     296                          pass
     297                  except AttributeError:
     298                          pass
     299          #------------------------------------------------
     300 -        def GetPickList (self, callback, x_intended, y_intended):
     301                  """
     302                  Returns a pick list, destroying a pre-existing pick list for this widget
     303
     304                  the alive member is true until the object is Destroy ()'ed
     305
     306                  @param callback: called when a item is selected,
     307                  @type callback: callable
     308                  @param x_intended: the X-position where the list should appear
     309                  @type x_intended: int
     310                  @param x: the Y-position where the list should appear
     311                  @type y_intended: int
     312
     313                  @return: PickList
     314                  """
     315  #               # retire previous pick list
     316  #               if self.__list and self.__list.alive:
     317  #                       self.__list.Destroy()
     318                  our_width, our_height = self.GetSizeTuple()
     319                  char_height = self.GetCharHeight()
     320                  # make list 9 lines of height char_height high
     321                  list_height = char_height * 9
     322                  # and find best placement
     323                  # - height
     324                  if (list_height + char_height) > our_height:
     325                          list_height = our_height
     326                          y_final = 0
     327                  elif (y_intended + list_height + char_height) > our_height:
     328                          y_final = our_height - list_height
     329                  else:
     330                          y_final = y_intended + char_height
     331                  # - width
     332                  list_width = int(list_height / 1.4)
     333                  if list_width > our_width:
     334                          list_width = our_width
     335                          x_final = 0
     336                  elif (x_intended + list_width) > our_width:
     337                          x_final = our_width - list_width
     338                  else:
     339                          x_final = x_intended
     340  #               self.__list = cPickList(self, wx.Point(x_final, y_final), wx.Size(list_width, list_height), callback=callback)
     341  #               return self.__list
     342                  list = cPickList(self, wx.Point(x_final, y_final), wx.Size(list_width, list_height), callback=callback)
     343                  return list
     344          #------------------------------------------------
     345  #       def set_completion_callback(self, callback):
     346  #               self.complete = callback
     347          #------------------------------------------------
     348 -        def GetSummary (self):
     349                  """Gets a terse summary string for the data in the widget"""
     350                  return ""
     351  #====================================================================
     352 -class cResizingSTC(wx.stc.StyledTextCtrl):
     353          """
     354          A StyledTextCrl that monitors the size of its internal text and
     355          resizes the parent accordingly.
     356
     357          MUST ONLY be used inside ResizingWindow !
     358
     359          FIXME: override standard STC popup menu
     360          """
     361 -        def __init__ (self, parent, id, pos=wx.DefaultPosition, size=wx.DefaultSize, style=0, data=None):
     362                  if not isinstance(parent, cResizingWindow):
     363                           raise ValueError, 'parent of %s MUST be a ResizingWindow' % self.__class__.__name__
     364
     365                  wx.stc.StyledTextCtrl.__init__ (self, parent, id, pos, size, style)
     366
     367                  self.SetWrapMode (wx.stc.STC_WRAP_WORD)
     368                  # FIXME: configure
     369                  self.StyleSetSpec (STYLE_ERROR, "fore:#7F11010,bold")
     370                  self.StyleSetSpec (STYLE_EMBED, "fore:#4040B0")
     371                  self.StyleSetChangeable (STYLE_EMBED, 0)
     372  #               self.StyleSetHotSpot (STYLE_EMBED, 1)
     373                  self.SetEOLMode (wx.stc.STC_EOL_LF)
     374
     375                  self.__register_interests()
     376
     377                  self.next_in_tab_order = None
     378                  self.prev_in_tab_order = None
     379
     380                  self.__parent = parent
     381
     382                  self.__popup_keywords = {}
     383
     384                  # FIXME: delay configurable
     385  #               self.__timer = gmTimer.cTimer (
     386  #                       callback = self._on_timer_fired,
     387  #                       delay = 300
     388  #               )
     389                  self.__matcher = None
     390
     391                  self.__show_list = 1
     392                  self.__embed = {}
     393                  self.list = None
     394                  self.no_list = 0                        # ??
     395
     396                  self.__data = data                      # this is just a placeholder for data to be attached to this STC, will be returned from GetData()
     397
     398                  self.__keyword_separators = regex.compile("[!?'\".,:;)}\]\r\n\s\t]+")
     399          #------------------------------------------------
     400          # public API
     401          #------------------------------------------------
     402 -        def set_keywords(self, popup_keywords=None):
     403                  if popup_keywords is None:
     404                          return
     405                  self.__popup_keywords = popup_keywords
     406          #------------------------------------------------
     407 -        def SetText(self, text):
     408                  self.__show_list = 0
     409                  wx.stc.StyledTextCtrl.SetText(self, text)
     410                  self.__show_list = 1
     411          #------------------------------------------------
     412 -        def ReplaceText (self, start, end, text, style=None):
     413                  self.replace_text(start, end, text, style)
     414          #------------------------------------------------
     415 -        def Embed (self, text, data=None):
     416                  self.no_list = 1
     417                  self.ReplaceText(self.fragment_start, self.fragment_end, text+';', STYLE_EMBED)
     418                  self.GotoPos(self.fragment_start+len (text)+1)
     419                  self.SetFocus()
     420  #               if data:
     421  #                       self.__embed[text] = data
     422                  self.no_list = 0
     423          #------------------------------------------------
     424 -        def DelPhrase (self, pos):
     425                  # FIXME: optimize
     426                  end = pos+1
     427                  while (end < self.GetLength()) and (self.GetCharAt(end) != ord(';')):
     428                          end += 1
     429                  start = pos
     430                  while (start > 0) and (self.GetCharAt(start and start-1) != ord(';')):
     431                          start -= 1
     432                  self.SetTargetStart(start)
     433                  self.SetTargetEnd(end)
     434                  self.ReplaceTarget('')
     435          #------------------------------------------------
     436 -        def SetFocus(self, x=None, line=None):
     437                  """Set focus to current position in STC.
     438
     439                  - make sure that's visible, too
     440                  """
     441                  wx.stc.StyledTextCtrl.SetFocus(self)
     442                  # goto first line ?
     443                  if line == 1:
     444                          if x is None:
     445                                  x = 0
     446                          self.GotoPos(self.PositionFromPoint(wx.Point(x,0)))
     447                          return
     448                  # goto last line ?
     449                  if line == -1:
     450                          _log.debug('going to last line in STC')
     451                          last_char_pos = self.GetLength()
     452                          if x is None:
     453                                  self.GotoPos(last_char_pos)
     454                                  _log.debug('no X given, use X=%s' % last_char_pos.x)
     455                                  return
     456                          y = self.PointFromPosition(last_char_pos).y
     457                          _log.debug('going to given X=%s' % x)
     458                          self.GotoPos(self.PositionFromPoint(wx.Point(x,y)))
     459                          return
     460                  # goto last current position
     461                  cur = self.PointFromPosition(self.GetCurrentPos())
     462                  self.__parent.EnsureVisible (self, cur.x, cur.y)
     463          #------------------------------------------------
     464 -        def AttachMatcher (self, matcher):
     465                  """
     466                  Attaches a gmMatchProvider to the STC,this will be used to drive auto-completion
     467                  """
     468                  self.__matcher = matcher
     469          #------------------------------------------------
     470 -        def SetData(self, data):
     471                  """
     472                  Configures the data associated with this STC
     473                  @param data The associated data
     474                  @type data Any object
     475                  """
     476                  self.__data = data
     477          #------------------------------------------------
     478 -        def GetData(self):
     479                  """
     480                  Retrieves the data associated with this STC
     481                  """
     482                  return self.__data
     483          #------------------------------------------------
     484 -        def replace_text(self, start=None, end=None, text=None, style=None):
     485                  """
     486                  Oddly, the otherwise very rich wx.STC API does not provide an
     487                  easy way to replace text, so we provide it here.
     488
     489                  @param start: the position in the text to start from
     490                  @param length: the length of the string to replace
     491                  @param text: the new string
     492                  @param style: the style for the replaced string
     493                  """
     494                  self.SetTargetStart(start)
     495                  self.SetTargetEnd(end)
     496                  self.ReplaceTarget(text)
     497                  if style is not None:
     498                          self.StartStyling(start, 0xFF)
     499                          self.SetStyling(len(text), style)
     500          #------------------------------------------------
     501 -        def replace_keyword_with_expansion(self, keyword=None, position=None):
     502
     503                  if keyword == u'$$steffi':                      # Easter Egg ;-)
     504                          expansion = u'Hai, play! Versucht das!  (Keks dazu?)  :-)'
     505                  else:
     506                          expansion = gmPG2.expand_keyword(keyword = keyword)
     507
     508                  if expansion is None:
     509                          return
     510
     511                  if expansion == u'':
     512                          return
     513
     514                  self.replace_text (
     515                          start = position,
     516                          end = position + len(keyword),
     517                          text = expansion
     518                  )
     519
     520                  self.GotoPos(position + len(expansion) + 1)
     521                  #wx.stc.StyledTextCtrl.SetFocus(self)
     522                  cur = self.PointFromPosition(position + len(expansion) + 1)
     523                  self.__parent.EnsureVisible(self, cur.x, cur.y)
     524          #------------------------------------------------
     525          # event handling
     526          #------------------------------------------------
     527 -        def __register_interests(self):
     528                  self.SetModEventMask (wx.stc.STC_MOD_INSERTTEXT | wx.stc.STC_MOD_DELETETEXT | wx.stc.STC_PERFORMED_USER)
     529
     530                  wx.stc.EVT_STC_MODIFIED (self, self.GetId(), self.__on_STC_modified)
     531
     532                  wx.EVT_KEY_DOWN (self, self.__on_key_down)
     533                  wx.EVT_KEY_UP (self, self.__OnKeyUp)
     534                  wx.EVT_CHAR(self, self.__on_char)
     535          #------------------------------------------------
     536 -        def __on_STC_modified(self, event):
     537
     538                  # did the user do anything of note to us ?
     539                  if not (event.GetModificationType() & (wx.stc.STC_MOD_INSERTTEXT | wx.stc.STC_MOD_DELETETEXT)):
     540                          event.Skip()
     541                          return
     542
     543                  last_char_pos = self.GetLength()
     544
     545                  # stop timer if empty
     546                  if last_char_pos == 0:
     547  #                       self.__timer.Stop()
     548                          return
     549
     550                  # do we need to resize ?
     551                  line_height = self.TextHeight(0)
     552                  true_txt_height = (self.PointFromPosition(last_char_pos).y - self.PointFromPosition(0).y) + line_height
     553                  x, visible_height = self.GetSizeTuple()
     554                  if visible_height < true_txt_height:
     555  #                       print "line:", line_height
     556  #                       print "before resize: too small"
     557  #                       print "visible height", visible_height
     558  #                       print "true text hgt", true_txt_height
     559                          n, remainder = divmod((true_txt_height - visible_height), line_height)
     560                          if remainder > 0: n = n + 1
     561                          target_height = visible_height + (n * line_height)
     562                          self.__parent.ReSize(self, target_height)
     563  #                       print "after resize"
     564                          x, y = self.GetSizeTuple()
     565  #                       print "visible height", y
     566
     567                  if ((visible_height - line_height) > true_txt_height):
     568  #                       print "line:", line_height
     569  #                       print "before resize: too big"
     570  #                       print "visible height", visible_height
     571  #                       print "true text hgt", true_txt_height
     572  #                       n, delta = divmod((visible_height - true_txt_height), line_height)
     573  #                       target_height = visible_height - (n * line_height)
     574                          target_height = visible_height - line_height
     575                          self.__parent.ReSize(self, target_height)
     576  #                       print "after resize"
     577                          x, y = self.GetSizeTuple()
     578  #                       print "visible height", y
     579
     580                  # is currently relevant term a keyword for popping up an edit area or something ?
     581                  fragment = self.__get_focussed_fragment()
     582                  if fragment in self.__popup_keywords.keys():
     583  #                       self.__timer.Stop()
     584                          self.__handle_keyword(fragment)
     585                          return
     586                  # else restart timer for match list
     587  #               self.__timer.Start(oneShot = True)
     588  #               event.Skip()
     589                  return
     590          #------------------------------------------------
     591 -        def __on_key_down(self, event):
     592                  """Act on some key presses we want to process ourselves."""
     593
     594  #               if (self.list is not None) and not self.list.alive:
     595  #                       self.list = None # someone else has destroyed our list!
     596
     597  #               curs_pos = self.GetCurrentPos()
     598
     599                  # <DOWN>
     600                  # - if in list: scroll list
     601                  # - if in last line: goto first line, same character, in next_in_tab_order
     602                  # - else standard behaviour
     603                  #if event.GetKeyCode() == wx.WXK_DOWN:
     604  #                       if (self.list is not None) and self.list.alive:
     605  #                               self.list.Down()
     606  #                               return
     607  #                       print "arrow down @ %s (line %s of %s)" % (curs_pos, self.LineFromPosition(curs_pos), self.GetLineCount())
     608                  #       if self.LineFromPosition(curs_pos)+1 == self.GetLineCount():
     609                  #               if self.next_in_tab_order is not None:
     610                  #                       curs_coords = self.PointFromPosition(curs_pos)
     611                  #                       self.next_in_tab_order.SetFocus(x=curs_coords.x, line=1)
     612                  #                       return
     613
     614                  # <UP>
     615                  # - if in list: scroll list
     616                  # - if in first line: goto last line, same character, in prev_in_tab_order
     617                  # - else standard behaviour
     618                  #if event.GetKeyCode() == wx.WXK_UP:
     619                  #       _log.debug('<UP-ARROW> key press detected')
     620  #                       if (self.list is not None) and self.list.alive:
     621  #                               self.list.Up()
     622  #                               return
     623                  #       _log.debug('pos %s = line %s' % (curs_pos, self.LineFromPosition(curs_pos)))
     624                  #       if self.LineFromPosition(curs_pos) == 0:
     625                  #               _log.debug('first line of STC - special handling')
     626                  #               if self.prev_in_tab_order is not None:
     627                  #                       _log.debug('prev_in_tab_order = %s' % str(self.prev_in_tab_order))
     628                  #                       curs_coords = self.PointFromPosition(curs_pos)
     629                  #                       _log.debug('cursor coordinates in current STC: %s:%s' % (curs_coords.x, curs_coords.y))
     630                  #                       self.prev_in_tab_order.SetFocus(x=curs_coords.x, line=-1)
     631                  #                       return
     632                  #       else:
     633                  #               _log.debug('not first line of STC - standard handling')
     634
     635                  # <TAB> key
     636                  # - move to next/prev_in_tab_order
     637                  # FIXME: what about inside a list ?
     638                  if event.GetKeyCode() == wx.WXK_TAB:
     639                          if event.m_shiftDown:
     640                                  if self.prev_in_tab_order is not None:
     641                                          self.prev_in_tab_order.SetFocus()
     642                          else:
     643                                  if self.next_in_tab_order is not None:
     644                                          self.next_in_tab_order.SetFocus()
     645                          return
     646
     647                  # <DEL>
     648                  # - if inside embedded string
     649                  #       - delete entire string and data dict
     650                  # - else standard behaviour
     651  #               if event.GetKeyCode() == wx.WXK_DELETE:
     652  #                       # FIXME: perhaps add check for regex, too ?
     653  #                       if self.GetStyleAt(curs_pos) == STYLE_EMBED:
     654  #                               self.DelPhrase(curs_pos)
     655  #                               # FIXME: also delete corresponding "additional data" dict ...
     656  #                               return
     657
     658                  # <BACKSPACE>
     659                  # - if inside embedded string
     660                  #       - delete entire string and data dict
     661                  # - else standard behaviour
     662  #               if event.GetKeyCode() == wx.WXK_BACK:
     663  #                       # FIXME: perhaps add check for regex, too ?
     664  #                       if self.GetStyleAt(curs_pos-1) == STYLE_EMBED:
     665  #                               self.DelPhrase (curs_pos-1)
     666  #                               # FIXME: also delete corresponding "additional data" dict ...
     667  #                               return
     668
     669                  event.Skip()    # skip to next event handler to keep processing
     670          #------------------------------------------------
     671 -        def __OnKeyUp (self, event):
     672                  if not self.list:
     673                          curs_pos = self.PointFromPosition(self.GetCurrentPos())
     674                          self.__parent.EnsureVisible (self, curs_pos.x, curs_pos.y)
     675          #------------------------------------------------
     676 -        def __on_char(self, evt):
     677
     678                  char = unichr(evt.GetUnicodeKey())
     679
     680                  if self.__keyword_separators.match(char) is not None:
     681                          if self.GetLength() == 1:
     682                                  evt.Skip()
     683                                  return
     684
     685                          line, caret_pos = self.GetCurLine()
     686                          word = self.__keyword_separators.split(line[:caret_pos])[-1]
     687                          if (word not in [ r[0] for r in gmPG2.get_text_expansion_keywords() ]) and (word != u'$$steffi'):               # Easter Egg ;-)
     688                                  evt.Skip()
     689                                  return
     690
     691                          start = self.GetCurrentPos() - len(word)
     692                          wx.CallAfter(self.replace_keyword_with_expansion, word, start)
     693                          evt.Skip()
     694                          return
     695
     696                  evt.Skip()
     697          #------------------------------------------------
     698  #       def _cb_on_popup_completion(self, was_cancelled=False):
     699  #               """Callback for popup completion.
     700  #
     701  #               - this is called when the user has signalled
     702  #                 being done interacting with the popup
     703  #               - if was_cancelled is True the popup content should
     704  #                 be ignored and no further action taken on it
     705  #               """
     706  #               print "popup interaction completed"
     707  #               if was_cancelled:
     708  #                       print "popup cancelled, ignoring data"
     709  ##                      self.__popup.Destroy()
     710  #                       self.__popup = None
     711  #                       return
     712  #               print "getting data from popup and acting on it"
     713  #               print self.__popup.GetData()
     714  #               # FIXME: wxCallAfter(embed) and store
     715  #               # maybe be a little smarter here
     716  #               self.__popup.Destroy()
     717  #               self.__popup = None
     718          #------------------------------------------------
     719 -        def _on_timer_fired(self, cookie):
     720  #               print 'timer <%s> fired' % cookie
     721                  fragment = self.__get_focussed_fragment()
     722                  if fragment.strip() == '':
     723                          return 1
     724  #               print 'should popup context pick list on <%s> now' % fragment
     725
     726                  return 1
     727
     728                  # - get matches and popup select list
     729                  if self.no_list:
     730                          return
     731                  if self.__matcher is None:
     732                          return
     733                  if not self.__show_list:
     734                          return
     735
     736                  # do indeed show list
     737                  if len(fragment) == 0:
     738                          if (self.list is not None) and self.list.alive:
     739                                  self.list.Destroy()
     740                          return
     741                  matches_found, matches = self.__matcher.getMatches(fragment)
     742                  if not matches_found:
     743                          if (self.list is not None) and self.list.alive:
     744                                  self.list.Destroy()
     745                          return
     746                  if not ((self.list is not None) and self.list.alive):
     747                          x, y = self.GetPositionTuple()
     748                          p = self.PointFromPosition(curs_pos)
     749                          self.list = self.__parent.GetPickList(self.__userlist, x+p.x, y+p.y)
     750                  self.list.SetItems(matches)
     751          #------------------------------------------------
     752          # internal API
     753          #------------------------------------------------
     754 -        def __get_focussed_fragment(self):
     755                  curs_pos = self.GetCurrentPos()
     756                  text = self.GetText()
     757                  self.fragment_start = text.rfind(';', 0, curs_pos)                              # FIXME: ';' hardcoded as separator
     758                  if self.fragment_start == -1:
     759                          self.fragment_start = 0
     760                  else:
     761                          self.fragment_start += 1
     762                  last_char_pos = self.GetLength()
     763                  self.fragment_end = text.find(';', curs_pos, last_char_pos)             # FIXME: ';' hardcoded as separator
     764                  if self.fragment_end == -1:
     765                          self.fragment_end = last_char_pos
     766                  return text[self.fragment_start:self.fragment_end].strip()
     767          #------------------------------------------------
     768 -        def __get_best_popup_geom(self):
     769  #               print "calculating optimal popup geometry"
     770                  parent_width, parent_height = self.__parent.GetSizeTuple()
     771  #               print "parent size is %sx%s pixel" % (parent_width, parent_height)
     772                  # FIXME: this should be gotten from ourselves, not the parent, but how ?
     773                  parent_char_height = self.__parent.GetCharHeight()
     774  #               print "char height in parent is", parent_char_height, "pixel"
     775                  # make popup 9 lines of height parent_char_height high
     776                  # FIXME: better detect this, but how ?
     777                  popup_height = parent_char_height * 9
     778  #               print "hence intended popup height is", popup_height, "pixel"
     779                  # get STC displacement inside parent
     780                  stc_origin_x, stc_origin_y = self.GetPositionTuple()
     781  #               print "inside parent STC is @ %s:%s" % (stc_origin_x, stc_origin_y)
     782                  # get current cursor position inside STC in pixels
     783                  curs_pos = self.PointFromPosition(self.GetCurrentPos())
     784  #               print "inside STC cursor is @ %s:%s" % (curs_pos.x, curs_pos.y)
     785                  # find best placement
     786                  # - height
     787                  if (popup_height + parent_char_height) > parent_height:
     788                          # don't let popup get bigger than parent window
     789                          popup_height = parent_height
     790                          popup_y_pos = 0
     791                  elif ((popup_height + parent_char_height) + (curs_pos.y + stc_origin_y)) > parent_height:
     792                          # if would fit inside but forced (partially) outside
     793                          # by cursor position then move inside
     794                          popup_y_pos = parent_height - popup_height
     795                  else:
     796                          popup_y_pos = (curs_pos.y + stc_origin_y) + parent_char_height
     797                  # - width
     798                  popup_width = int(popup_height / 1.4)           # Golden Cut
     799                  if popup_width > parent_width:
     800                          # don't let popup get bigger than parent window
     801                          popup_width = parent_width
     802                          popup_x_pos = 0
     803                  elif (popup_width + (curs_pos.x + stc_origin_x)) > parent_width:
     804                          # if would fit inside but forced (partially) outside
     805                          # by cursor position then move inside
     806                          popup_x_pos = parent_width - popup_width
     807                  else:
     808                          popup_x_pos = curs_pos.x + stc_origin_x
     809  #               print "optimal geometry = %sx%s @ %s:%s" % (popup_width, popup_height, popup_x_pos, popup_y_pos)
     810                  return (wx.Point(popup_x_pos, popup_y_pos), wx.Size(popup_width, popup_height))
     811          #------------------------------------------------
     812 -        def __handle_keyword(self, kwd=None):
     813                  try:
     814                          create_widget = self.__popup_keywords[kwd]['widget_factory']
     815                  except KeyError:
     816                          gmDispatcher.send(signal='statustext', msg=_('No action configured for keyword [%s].') % kwd)
     817                          return False
     818
     819  #               best_pos, best_size = self.__get_best_popup_geom()
     820                  screen_pos = self.ClientToScreen(self.PointFromPosition(self.GetCurrentPos()))
     821                  top_parent = wx.GetTopLevelParent(self)
     822                  best_pos = top_parent.ScreenToClient(screen_pos)
     823                  try:
     824                          popup = create_widget (
     825                                  parent = top_parent,
     826                                  pos = best_pos,
     827                                  size = wx.Size(400, 300),
     828                                  style = wx.SUNKEN_BORDER,
     829                                  data_sink = self.__popup_keywords[kwd]['widget_data_sink']
     830                          )
     831                  except StandardError:
     832                          _log.exception('cannot call [%s] on keyword [%s] to create widget' % (create_widget, kwd))
     833                          gmGuiHelpers.gm_show_error (
     834                                  aMessage = _('Cannot invoke [%s] for keyword [%s].') % (create_widget, kwd),
     835                                  aTitle = _('showing keyword popup')
     836                          )
     837                          return False
     838
     839                  if not isinstance(popup, wx.Dialog):
     840                          gmDispatcher.send(signal='statustext', msg=_('Action [%s] on keyword [%s] is invalid.') % (create_widget, kwd))
     841                          _log.error('keyword [%s] triggered action [%s]' % (kwd, create_widget))
     842                          _log.error('the result (%s) is not a wx.Dialog subclass instance, however' % str(popup))
     843                          return False
     844
     845                  # display widget
     846                  result = popup.ShowModal()
     847                  if result == wx.ID_OK:
     848                          summary = popup.get_summary()
     849                          wx.CallAfter(self.Embed, summary)
     850                  popup.Destroy()
     851          #------------------------------------------------
     852 -        def __userlist (self, text, data=None):
     853                  # this is a callback
     854  #               --- old --------------
     855  #               # FIXME: need explanation on instance/callable business, it seems complicated
     856  #               if issubclass(data, cResizingWindow):
     857  #                       win = data (
     858  #                               self,
     859  #                               -1,
     860  #                               pos = self.ClientToScreen(self.PointFromPosition(self.GetCurrentPos())),
     861  #                               size = wx.Size(300, 150)
     862  #                       )
     863  #                       cPopupFrame (
     864  #                               embed_header = text,
     865  #                               widget = win,
     866  #                               originator = self,
     867  #                               pos = self.ClientToScreen(self.PointFromPosition(self.GetCurrentPos()))
     868  #                       ).Show()
     869  #               elif callable(data):
     870  #                       data (text, self.__parent, self, self.ClientToScreen (self.PointFromPosition (self.GetCurrentPos ())))
     871  #               --- old --------------
     872                  if self.MakePopup (text, data, self, self.ClientToScreen (self.PointFromPosition (self.GetCurrentPos ()))):
     873                          pass
     874                  else:
     875                          self.Embed (text, data)
     876          #--------------------------------------------------
     877 -        def MakePopup (self, text, data, parent, cursor_position):
     878                  """
     879                  An overrideable method, called whenever a match is made in this STC
     880                  Designed for producing popups, but the overrider can in fact, do
     881                  whatever they please.
     882
     883                  @return True if a poup-up or similar actually happened (which suppresses inserting the match string in the text
     884                  @rtype boolean
     885                  """
     886                  #cPopupFrame(text, win, self, cursor_position)).Show()
     887                  return False
     888  #====================================================================
     889  #====================================================================
     890  if __name__ == '__main__':
     891
     892  #       from Gnumed.pycommon.gmMatchProvider import cMatchProvider_FixedList
     893  #       from Gnumed.pycommon import gmI18N
     894
     895 -        def create_widget_on_test_kwd1(*args, **kwargs):
     896                  print "test keyword must have been typed..."
     897                  print "actually this would have to return a suitable wx.Window subclass instance"
     898                  print "args:", args
     899                  print "kwd args:"
     900                  for key in kwargs.keys():
     901                          print key, "->", kwargs[key]
     902          #================================================================
     903 -        def create_widget_on_test_kwd2(*args, **kwargs):
     904                  msg = (
     905                          "test keyword must have been typed...\n"
     906                          "actually this would have to return a suitable wx.Window subclass instance\n"
     907                  )
     908                  for arg in args:
     909                          msg = msg + "\narg ==> %s" % arg
     910                  for key in kwargs.keys():
     911                          msg = msg + "\n%s ==> %s" % (key, kwargs[key])
     912                  gmGuiHelpers.gm_show_info (
     913                          aMessage = msg,
     914                          aTitle = 'msg box on create_widget from test_keyword'
     915                  )
     916          #================================================================
     917 -        class cTestKwdPopupPanel(wx.Panel):
     918 -                def __init__(self, parent, pos, size, style, completion_callback):
     919                          wx.Panel.__init__ (
     920                                  self,
     921                                  parent,
     922                                  -1,
     923                                  pos,
     924                                  size,
     925                                  style
     926                          )
     927                          self.__completion_callback = completion_callback
     928                          self._wx.ID_BTN_OK = wx.NewId()
     929                          self._wx.ID_BTN_Cancel = wx.NewId()
     930                          self.__do_layout()
     931                          self.__register_interests()
     932                          self.Show()
     933
     934 -                def __do_layout(self):
     935                          # message
     936                          msg = "test keyword popup"
     937                          text = wx.StaticText (self, -1, msg)
     938                          # buttons
     939                          self.btn_OK = wx.Button(self, self._wx.ID_BTN_OK, _("OK"))
     940                          self.btn_OK.SetToolTipString(_('dismiss popup and embed data'))
     941                          self.btn_Cancel = wx.Button(self, self._wx.ID_BTN_Cancel, _("Cancel"))
     942                          self.btn_Cancel.SetToolTipString(_('dismiss popup and throw away data'))
     943                          szr_buttons = wx.BoxSizer(wx.HORIZONTAL)
     944                          szr_buttons.Add(self.btn_OK, 1, wx.EXPAND | wx.ALL, 1)
     945                          szr_buttons.Add(5, 0, 0)
     946                          szr_buttons.Add(self.btn_Cancel, 1, wx.EXPAND | wx.ALL, 1)
     947                          # arrange
     948                          szr_main = wx.BoxSizer(wx.VERTICAL)
     949                          szr_main.Add(text, 1, wx.EXPAND | wx.ALL, 1)
     950                          szr_main.Add(szr_buttons, 0)
     951                          # layout
     952                          self.SetAutoLayout(True)
     953                          self.SetSizer(szr_main)
     954                          szr_main.Fit(self)
     955
     956 -                def __register_interests(self):
     957                          wx.EVT_BUTTON(self.btn_OK, self._wx.ID_BTN_OK, self._on_ok)
     958                          wx.EVT_BUTTON(self.btn_Cancel, self._wx.ID_BTN_Cancel, self._on_cancel)
     959
     960 -                def _on_ok(self, event):
     961                          self.__completion_callback(was_cancelled = False)
     962
     963 -                def _on_cancel(self, event):
     964                          self.__completion_callback(was_cancelled = True)
     965          #================================================================
     966 -        def create_widget_on_test_kwd3(parent, pos, size, style, completion_callback):
     967                  pnl = cTestKwdPopupPanel (
     968                          parent = parent,
     969                          pos = pos,
     970                          size = size,
     971                          style = style,
     972                          completion_callback = completion_callback
     973                  )
     974                  return pnl
     975          #================================================================
     976 -        class cSoapWin (cResizingWindow):
     977 -                def DoLayout(self):
     978                          self.input1 = cResizingSTC(self, -1)
     979                          self.input2 = cResizingSTC(self, -1)
     980                          self.input3 = cResizingSTC(self, -1)
     981
     982                          self.input1.prev_in_tab_order = None
     983                          self.input1.next_in_tab_order = self.input2
     984                          self.input2.prev_in_tab_order = self.input1
     985                          self.input2.next_in_tab_order = self.input3
     986                          self.input3.prev_in_tab_order = self.input2
     987                          self.input3.next_in_tab_order = None
     988
     989                          self.AddWidget (widget=self.input1, label="S")
     990                          self.Newline()
     991                          self.AddWidget (widget=self.input2, label="O")
     992                          self.Newline()
     993                          self.AddWidget (widget=self.input3, label="A+P")
     994
     995                          kwds = {}
     996                          kwds['$test_keyword'] = {'widget_factory': create_widget_on_test_kwd3}
     997                          self.input2.set_keywords(popup_keywords=kwds)
     998          #================================================================
     999 -        class cSoapPanel(wx.Panel):
    1000 -                def __init__ (self, parent, id):
    1001                          wx.Panel.__init__(self, parent, id)
    1002                          sizer = wx.BoxSizer(wx.VERTICAL)
    1003                          self.soap = cSoapWin(self, -1)
    1004                          self.save = wx.Button (self, -1, _(" Save "))
    1005                          self.delete = wx.Button (self, -1, _(" Delete "))
    1006                          self.new = wx.Button (self, -1, _(" New "))
    1007  #                       self.list = wx.ListBox (self, -1, style=wx.LB_SINGLE | wx.LB_NEEDED_SB)
    1008                          wx.EVT_BUTTON (self.save, self.save.GetId (), self.OnSave)
    1009                          wx.EVT_BUTTON (self.delete, self.delete.GetId (), self.OnDelete)
    1010                          wx.EVT_BUTTON (self.new, self.new.GetId (), self.OnNew)
    1011  #                       wx.EVT_LISTBOX (self.list, self.list.GetId (), self.OnList)
    1012                          self.__do_layout()
    1013
    1014 -                def __do_layout (self):
    1015                          sizer_1 = wx.BoxSizer(wx.VERTICAL)
    1016                          sizer_1.Add(self.soap, 3, wx.EXPAND, 0)
    1017                          sizer_2 = wx.BoxSizer (wx.HORIZONTAL)
    1018                          sizer_2.Add(self.save, 0, 0)
    1019                          sizer_2.Add(self.delete, 0, 0)
    1020                          sizer_2.Add(self.new, 0, 0)
    1021                          sizer_1.Add(sizer_2, 0, wx.EXPAND)
    1022  #                       sizer_1.Add(self.list, 3, wx.EXPAND, 0)
    1023                          self.SetAutoLayout(1)
    1024                          self.SetSizer(sizer_1)
    1025                          sizer_1.Fit(self)
    1026                          sizer_1.SetSizeHints(self)
    1027                          self.Layout()
    1028
    1029 -                def OnDelete (self, event):
    1030                          self.soap.Clear()
    1031  #                       sel = self.list.GetSelection ()
    1032  #                       if sel >= 0:
    1033  #                               self.list.Delete (sel)
    1034
    1035 -                def OnNew (self, event):
    1036  #                       sel = self.list.GetSelection ()
    1037  #                       if sel >= 0:
    1038  #                               self.OnSave (None)
    1039                          self.soap.Clear()
    1040  #                       self.list.SetSelection (sel, 0)
    1041
    1042 -                def OnSave (self, event):
    1043                          data = self.soap.GetValue()
    1044  #                       title = data['Assessment'] or data['Subjective'] or data['Plan'] or data['Objective']
    1045                          self.soap.Clear()
    1046  #                       sel = self.list.GetSelection ()
    1047  #                       if sel < 0:
    1048  #                               self.list.Append (title, data)
    1049  #                       else:
    1050  #                               self.list.SetClientData (sel, data)
    1051  #                               self.list.SetString (sel, title)
    1052
    1053  #               def OnList (self, event):
    1054  #                       self.soap.SetValues (event.GetClientData ())
    1055          #================================================================
    1056 -        class testFrame(wx.Frame):
    1057 -                def __init__ (self, title):
    1058                          wx.Frame.__init__ (self, None, wx.NewId(), "test SOAP", size = wx.Size (350, 500)) # this frame will have big fat borders
    1059                          wx.EVT_CLOSE (self, self.OnClose)
    1060                          panel = cSoapPanel(self, -1)
    1061                          sizer = wx.BoxSizer(wx.VERTICAL)
    1062                          sizer.Add (panel, 1, wx.GROW)
    1063                          self.SetSizer(sizer)
    1064                          self.SetAutoLayout(1)
    1065                          sizer.Fit (self)
    1066                          self.Layout ()
    1067
    1068 -                def OnClose (self, event):
    1069                          self.Destroy()
    1070          #================================================================
    1071 -        class testApp(wx.App):
    1072 -                def OnInit (self):
    1073                          self.frame = testFrame ("testFrame")
    1074                          self.frame.Show()
    1075                          return 1
    1076          #================================================================
    1077          app = testApp(0)
    1078          app.MainLoop()
    1079  #====================================================================
    1080