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