chiark / gitweb /
Rewrite graphical tools in Python.
[xtoys] / xtoys.py
diff --git a/xtoys.py b/xtoys.py
new file mode 100644 (file)
index 0000000..6109c85
--- /dev/null
+++ b/xtoys.py
@@ -0,0 +1,264 @@
+### -*-python-*-
+###
+### Utility module for xtoys Python programs
+###
+### (c) 2007 Straylight/Edgeware
+###
+
+###----- Licensing notice ---------------------------------------------------
+###
+### This file is part of the Edgeware X tools collection.
+###
+### X tools is free software; you can redistribute it and/or modify
+### it under the terms of the GNU General Public License as published by
+### the Free Software Foundation; either version 2 of the License, or
+### (at your option) any later version.
+###
+### X tools is distributed in the hope that it will be useful,
+### but WITHOUT ANY WARRANTY; without even the implied warranty of
+### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+### GNU General Public License for more details.
+###
+### You should have received a copy of the GNU General Public License
+### along with X tools; if not, write to the Free Software Foundation,
+### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+###--------------------------------------------------------------------------
+### External dependencies.
+
+import os as OS
+import optparse as O
+from sys import stdin, stdout, exit, argv
+
+import pygtk
+pygtk.require('2.0')
+import gtk as GTK
+GDK = GTK.gdk
+import gobject as GO
+del pygtk
+
+###--------------------------------------------------------------------------
+### Reasons for living.
+
+_reasons = 0
+
+def addreason():
+  """Add a reason."""
+  global _reasons
+  _reasons += 1
+
+def delreason():
+  """Drop a reason.  When reasons reach zero, the main loop stops."""
+  global _reasons
+  _reasons -= 1
+  if _reasons == 0:
+    GTK.main_quit()
+
+###--------------------------------------------------------------------------
+### General utilities.
+
+def make_optparse(options, **kw):
+  """
+  Construct an option parser object.
+
+  The KW are keyword arguments to be passed to OptionParser.  The OPTIONS are
+  a list of (SHORT, LONG, KW) triples; the SHORT and LONG strings do /not/
+  have leading dashes.
+  """
+  op = O.OptionParser(**kw)
+  for short, long, kw in options:
+    names = ['--%s' % long]
+    if short is not None:
+      names.append('-%s' % short)
+    op.add_option(*names, **kw)
+  return op
+
+###--------------------------------------------------------------------------
+### Message boxes.
+
+class MessageButton (object):
+  """
+  An object storing information about a button in a Message.
+  """
+
+  def __init__(me, label, value = None, *options):
+    """
+    Initialize a button definition.
+
+    If LABEL is a tuple, then it should have the form
+    (LABEL, VALUE, OPTIONS...); the initialization parser is applied to its
+    contents.  This is not done recursively.
+
+    If LABEL is a string, and VALUE and OPTIONS are omitted, then it is
+    parsed as OPT:OPT:...:LABEL, where OPT is either an option (see below) or
+    `=VALUE'.  Only one VALUE may be given.
+
+    The LABEL is the label to put on the button, or the GTK stock id.  The
+    VALUE is the value to return from Message.ask if the button is chosen.
+    The OPTIONS are:
+
+      * 'default': this is the default button
+      * 'cancel': this is the cancel button
+    """
+    if value is not None or len(options) > 0:
+      me._doinit(label, value, *options)
+    elif isinstance(label, tuple):
+      me._doinit(*label)
+    else:
+      i = 0
+      options = []
+      while 0 <= i < len(label):
+        if label[i] == '!':
+          i += 1
+          break
+        j = label.find(':', i)
+        if j < 0:
+          break
+        if label[i] == '=':
+          if value is not None:
+            raise ValueError, 'Duplicate value in button spec %s' % label
+          value = label[i + 1:j]
+        else:
+          options.append(label[i:j])
+        i = j + 1
+      label = label[i:]
+      me._doinit(label, value, *options)
+
+  def _doinit(me, label, value = None, *options):
+    """
+    Does the work of processing the initialization parameters.
+    """
+    me.label = label
+    if value is None:
+      me.value = label
+    else:
+      me.value = value
+    me.defaultp = me.cancelp = False
+    for opt in options:
+      if opt == 'default':
+        me.defaultp = True
+      elif opt == 'cancel':
+        me.cancelp = True
+      else:
+        raise ValueError, 'unknown button option %s' % opt
+
+class Message (GTK.MessageDialog):
+  """
+  A simple message-box window: contains text and some buttons.
+
+  See __init__ for the usage instructions.
+  """
+
+  ## Mapping from Pythonic strings to GTK constants.
+  dboxtype = {'info': GTK.MESSAGE_INFO,
+              'warning': GTK.MESSAGE_WARNING,
+              'question': GTK.MESSAGE_QUESTION,
+              'error': GTK.MESSAGE_ERROR}
+
+  def __init__(me,
+               title = None,
+               type = 'info',
+               message = '',
+               headline = None,
+               buttons = [],
+               markupp = False):
+    """
+    Report a message to the user and get a response back.
+
+    The TITLE is placed in the window's title bar.
+
+    The TYPE controls what kind of icon is placed in the window; it should be
+    one of 'info', 'warning', 'question' or 'error'.
+
+    The MESSAGE is the string which should be displayed.  It may have
+    multiple lines.  There may also be a HEADLINE message.  The messages are
+    parsed for Pango markup if MARKUPP is set.
+
+    The BUTTONS are a list of buttons to show, right to left.  Each one
+    should be a string which may either be a GTK stock tag or a plain label
+    string.  This may be prefixed, optionally, by `!' to ignore other prefix
+    characters, `+' to make this button the default, or `:' to make this the
+    `cancel' button, chosen by pressing escape.  If no button is marked as
+    cancel, we just use the first.
+    """
+
+    ## Initialize superclasses.
+    GTK.MessageDialog.__init__(me, type = me.dboxtype.get(type))
+
+    ## Set the title.
+    if title is None:
+      title = OS.path.basename(argv[0])
+    me.set_title(title)
+
+    ## Add the message strings.
+    me.set_property('use-markup', markupp)
+    if headline is None:
+      me.set_property('text', message)
+    else:
+      me.set_property('text', headline)
+      me.set_property('secondary-text', message)
+      me.set_property('secondary-use-markup', markupp)
+
+    ## Include the buttons.
+    if len(buttons) == 0:
+      buttons = [MessageButton('gtk-ok', True, 'default', 'cancel')]
+    else:
+      buttons = buttons[:]
+      buttons.reverse()
+    me.buttons = [isinstance(b, MessageButton) and b or MessageButton(b)
+                  for b in buttons]
+    cancel = -1
+    default = -1
+    for i in xrange(len(buttons)):
+      button = me.buttons[i]
+      label = button.label
+      if GTK.stock_lookup(label) is None:
+        b = GTK.Button(label = label)
+      else:
+        b = GTK.Button(stock = label)
+      if button.defaultp:
+        default = i
+      if button.cancelp:
+        cancel = i
+      b.set_flags(b.flags() | GTK.CAN_DEFAULT)
+      button.widget = b
+      me.add_action_widget(b, i)
+
+    ## Choose default buttons.
+    if cancel == -1:
+      cancel = 0
+    if default == -1:
+      default = len(me.buttons) - 1
+    me.cancel = cancel
+    me.default = default
+
+    ## Connect up the handlers and go.
+    me.connect('key-press-event', me.keypress, me.buttons[cancel])
+    me.buttons[default].widget.grab_default()
+
+  def keypress(me, win, event, cancel):
+    """
+    Handle key-press events.
+
+    If the EVENT was an escape-press, then activate the CANCEL button.
+    """
+    key = GDK.keyval_name(event.keyval)
+    if key != 'Escape':
+      return False
+    cancel.widget.activate()
+    return True
+
+  def ask(me):
+    """
+    Display the message, and wait for a response.
+
+    The return value is the label of the button which was chosen.
+    """
+    me.show_all()
+    r = me.run()
+    if r < 0:
+      r = me.cancel
+    me.hide()
+    return me.buttons[r].value
+
+###----- That's all, folks --------------------------------------------------