spelunk_gnome.py   [plain text]


# -*- Python -*-
# Twisted, the Framework of Your Internet
# $Id: spelunk_gnome.py,v 1.2 2004/09/23 14:25:25 murata Exp $
# Copyright (C) 2001 Matthew W. Lefkowitz
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of version 2.1 of the GNU Lesser General Public
# License as published by the Free Software Foundation.
#
# This library 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

"""Object browser GUI, GnomeCanvas implementation.
"""

from twisted.python import log

# TODO:
#  gzigzag-style navigation

class SillyModule:
    def __init__(self, module, prefix):
        self.__module = module
        self.__prefix = prefix

    def __getattr__(self, attr):
        try:
            return getattr(self.__module, self.__prefix + attr)
        except AttributeError:
            return getattr(self.__module, attr)


# We use gnome.ui because that's what happens to have Python bindings
# for the Canvas.  I think this canvas widget is available seperately
# in "libart", but nobody's given me Python bindings for just that.

# The Gnome canvas is said to be modeled after the Tk canvas, so we
# could probably write this in Tk too.  But my experience is with GTK,
# not with Tk, so this is what I use.

import gnome.ui
gnome = SillyModule(gnome.ui, 'Gnome')

import gtk
(True, False) = (gtk.TRUE, gtk.FALSE)
gtk = SillyModule(gtk, 'Gtk')

import GDK

from twisted.python import reflect, text
from twisted.spread import pb
from twisted.manhole import explorer

import string, sys, types
import UserList
_PIXELS_PER_UNIT=10

#### Support class.

class PairList(UserList.UserList):
    """An ordered list of key, value pairs.

    Kinda like an ordered dictionary.  Made with small data sets
    in mind, as get() does a linear search, not hashing.
    """
    def get(self, key):
        i = 0
        for k, v in self.data:
            if key == k:
                return (i, v)
            i = i + 1
        else:
            return (None, None)

    def keys(self):
        return map(lambda x: x[0], self.data)


#### Public

class SpelunkDisplay(gnome.Canvas):
    """Spelunk widget.

    The top-level widget for this module.  This gtk.Widget is where the
    explorer display will be, and this object is also your interface to
    creating new visages.
    """
    def __init__(self, aa=False):
        gnome.Canvas.__init__(self, aa)
        self.set_pixels_per_unit(_PIXELS_PER_UNIT)
        self.visages = {}

    def makeDefaultCanvas(self):
        """Make myself the default canvas which new visages are created on.
        """
        # XXX: For some reason, the 'canvas' and 'parent' properties
        # of CanvasItems aren't accessible thorugh pygnome.
        Explorer.canvas = self

    def receiveExplorer(self, xplorer):
        if self.visages.has_key(xplorer.id):
            log.msg("Using cached visage for %d" % (xplorer.id, ))
            # Ikk.  Just because we just received this explorer, that
            # doesn't necessarily mean its attributes are fresh.  Fix
            # that, either by having this side pull or the server
            # side push.
            visage = self.visages[xplorer.id]
            #xplorer.give_properties(visage)
            #xplorer.give_attributes(visage)
        else:
            log.msg("Making new visage for %d" % (xplorer.id, ))
            self.visages[xplorer.id] = xplorer.newVisage(self.root(),
                                                         self)

#### Base classes

class Explorer(pb.RemoteCache):
    """Base class for all RemoteCaches of explorer.Explorer cachables.

    Meaning that when an Explorer comes back over the wire, one of
    these is created.  From this, you can make a Visage for the
    SpelunkDisplay, or a widget to display as an Attribute.
    """
    canvas = None
    # From our cache:
    id = None
    identifier = None
    explorerClass = None
    attributeGroups = None

    def newVisage(self, group, canvas=None):
        """Make a new visage for the object I explore.

        Returns a Visage.
        """
        canvas = canvas or self.canvas
        klass = spelunkerClassTable.get(self.explorerClass, None)
        if (not klass) or (klass[0] is None):
            log.msg("%s not in table, using generic" % self.explorerClass)
            klass = GenericVisage
        else:
            klass = klass[0]
        spelunker = klass(self, group, canvas)
        if hasattr(canvas, "visages") \
           and not canvas.visages.has_key(self.id):
            canvas.visages[self.id] = spelunker

        self.give_properties(spelunker)

        self.give_attributes(spelunker)

        return spelunker

    def newAttributeWidget(self, group):
        """Make a new attribute item for my object.

        Returns a gtk.Widget.
        """
        klass = spelunkerClassTable.get(self.explorerClass, None)
        if (not klass) or (klass[1] is None):
            log.msg("%s not in table, using generic" % self.explorerClass)
            klass = GenericAttributeWidget
        else:
            klass = klass[1]

        return klass(self, group)

    def give_properties(self, spelunker):
        """Give a spelunker my properties in an ordered list.
        """
        valuelist = PairList()
        for p in spelunker.propertyLabels.keys():
            value = getattr(self, p, None)
            valuelist.append((p,value))
        spelunker.fill_properties(valuelist)

    def give_attributes(self, spelunker):
        for a in spelunker.groupLabels.keys():
            things = getattr(self, a)
            spelunker.fill_attributeGroup(a, things)

class _LooseBoxBorder:
    box = None
    color = 'black'
    width = 1
    def __init__(self, box):
        self.box = box

class LooseBox(gnome.CanvasGroup):
    def __init__(self):
        self.border = _LooseBoxBorder(self)

class Visage(gnome.CanvasGroup):
    """A \"face\" of an object under exploration.

    A Visage is a representation of an object presented to the user.
    The \"face\" in \"interface\".

    'propertyLabels' and 'groupLabels' are lists of (key, name)
    2-ples, with 'key' being the string the property or group is
    denoted by in the code, and 'name' being the pretty human-readable
    string you want me to show on the Visage.  These attributes are
    accumulated from base classes as well.

    I am a gnome.CanvasItem (more specifically, CanvasGroup).
    """
    color = {'border': '#006644'}
    border_width = 8
    detail_level = 0
    # These are mappings from the strings the code calls these by
    # and the pretty names you want to see on the screen.
    # (e.g. Capitalized or localized)
    propertyLabels = []
    groupLabels = []

    drag_x0 = 0
    drag_y0 = 0

    def __init__(self, explorer, rootGroup, canvas):
        """Place a new Visage of an explorer in a canvas group.

        I also need a 'canvas' reference is for certain coordinate
        conversions, and pygnome doesn't give access to my GtkObject's
        .canvas attribute.  :(
        """
        # Ugh.  PyGtk/GtkObject/GnomeCanvas interfacing grits.
        gnome.CanvasGroup.__init__(self,
                                   _obj = rootGroup.add('group')._o)

        self.propertyLabels = PairList()
        reflect.accumulateClassList(self.__class__, 'propertyLabels',
                                    self.propertyLabels)
        self.groupLabels = PairList()
        reflect.accumulateClassList(self.__class__, 'groupLabels',
                                    self.groupLabels)

        self.explorer = explorer
        self.identifier = explorer.identifier
        self.objectId = explorer.id

        self.canvas = canvas
        self.rootGroup = rootGroup

        self.ebox = gtk.EventBox()
        self.ebox.set_name("Visage")
        self.frame = gtk.Frame(self.identifier)
        self.container = gtk.VBox()
        self.ebox.add(self.frame)
        self.frame.add(self.container)

        self.canvasWidget = self.add('widget', widget=self.ebox,
                                     x=0, y=0, anchor=gtk.ANCHOR_NW,
                                     size_pixels=0)

        self.border = self.add('rect', x1=0, y1=0,
                               x2=1, y2=1,
                               fill_color=None,
                               outline_color=self.color['border'],
                               width_pixels=self.border_width)

        self.subtable = {}

        self._setup_table()

        # TODO:
        #  Collapse me
        #  Movable/resizeable me
        #  Destroy me
        #  Set my detail level

        self.frame.connect("size_allocate", self.signal_size_allocate,
                           None)
        self.connect("destroy", self.signal_destroy, None)
        self.connect("event", self.signal_event)

        self.ebox.show_all()

        # Our creator will call our fill_ methods when she has the goods.

    def _setup_table(self):
        """Called by __init__ to set up my main table.

        You can easily override me instead of clobbering __init__.
        """

        table = gtk.Table(len(self.propertyLabels), 2)
        self.container.add(table)
        table.set_name("PropertyTable")
        self.subtable['properties'] = table
        row = 0

        for p, name in self.propertyLabels:
            label = gtk.Label(name)
            label.set_name("PropertyName")
            label.set_data("property", p)
            table.attach(label, 0, 1, row, row + 1)
            label.set_alignment(0, 0)
            row = row + 1

        # XXX: make these guys collapsable
        for g, name in self.groupLabels:
            table = gtk.Table(1, 2)
            self.container.add(table)
            table.set_name("AttributeGroupTable")
            self.subtable[g] = table
            label = gtk.Label(name)
            label.set_name("AttributeGroupTitle")
            table.attach(label, 0, 2, 0, 1)

    def fill_properties(self, propValues):
        """Fill in values for my properites.

        Takes a list of (name, value) pairs.  'name' should be one of
        the keys in my propertyLabels, and 'value' either an Explorer
        or a string.
        """
        table = self.subtable['properties']

        table.resize(len(propValues), 2)

        # XXX: Do I need to destroy previously attached children?

        for name, value in propValues:
            self.fill_property(name, value)

        table.show_all()

    def fill_property(self, property, value):
        """Set a value for a particular property.

        'property' should be one of the keys in my propertyLabels.
        """
        row, name = self.propertyLabels.get(property)
        if type(value) is not types.InstanceType:
            widget = gtk.Label(str(value))
            widget.set_alignment(0, 0)
        else:
            widget = value.newAttributeWidget(self)
        widget.set_name("PropertyValue")

        self.subtable['properties'].attach(widget, 1, 2, row, row+1)

    def fill_attributeGroup(self, group, attributes):
        """Provide members of an attribute group.

        'group' should be one of the keys in my groupLabels, and
        'attributes' a list of (name, value) pairs, with each value as
        either an Explorer or string.
        """

        # XXX: How to indicate detail level of members?

        table = self.subtable[group]
        if not attributes:
            table.hide()
            return

        table.resize(len(attributes)+1, 2)

        # XXX: Do I need to destroy previously attached children?

        row = 1 # 0 is title

        for name, value in attributes.items():
            label = gtk.Label(name)
            label.set_name("AttributeName")
            label.set_alignment(0, 0)

            if type(value) is types.StringType:
                widget = gtk.Label(value)
                widget.set_alignment(0, 0)
            else:
                widget = value.newAttributeWidget(self)

            table.attach(label, 0, 1, row, row + 1)
            table.attach(widget, 1, 2, row, row + 1)
            row = row + 1

        table.show_all()

    def signal_event(self, widget, event=None):
        if not event:
            log.msg("Huh? got event signal with no event.")
            return
        if event.type == GDK.BUTTON_PRESS:
            if event.button == 1:
                self.drag_x0, self.drag_y0 = event.x, event.y
                return True
        elif event.type == GDK.MOTION_NOTIFY:
            if event.state & GDK.BUTTON1_MASK:
                self.move(event.x - self.drag_x0, event.y - self.drag_y0)
                self.drag_x0, self.drag_y0 = event.x, event.y
                return True
        return False

    def signal_size_allocate(self, frame_widget,
                             unusable_allocation, unused_data):
        (x, y, w, h) = frame_widget.get_allocation()

        # XXX: allocation PyCObject is apparently unusable!
        # (w, h) = allocation.width, allocation.height

        w, h = (float(w)/_PIXELS_PER_UNIT, float(h)/_PIXELS_PER_UNIT)

        x1, y1 = (self.canvasWidget['x'], self.canvasWidget['y'])

        b = self.border
        (b['x1'], b['y1'], b['x2'], b['y2']) = (x1, y1, x1+w, y1+h)

    def signal_destroy(self, unused_object, unused_data):
        del self.explorer

        del self.canvasWidget
        del self.border

        del self.ebox
        del self.frame
        del self.container

        self.subtable.clear()


class AttributeWidget(gtk.Widget):
    """A widget briefly describing an object.

    This is similar to a Visage, but has far less detail.  This should
    display only essential identifiying information, a gtk.Widget
    suitable for including in a single table cell.

    (gtk.Widgets are used here instead of the more graphically
    pleasing gnome.CanvasItems because I was too lazy to re-write
    gtk.table for the canvas.  A new table widget/item would be great
    though, not only for canvas prettiness, but also because we could
    use one with a mone pythonic API.)

    """
    def __init__(self, explorer, parent):
        """A new AttributeWidget describing an explorer.
        """
        self.parent = parent

        self.explorer = explorer
        self.identifier = explorer.identifier
        self.id = explorer.id

        widgetObj = self._makeWidgetObject()
        gtk.Widget.__init__(self, _obj=widgetObj)
        self.set_name("AttributeValue")
        self.connect("destroy", self.signal_destroy, None)
        self.connect("button-press-event", self.signal_buttonPressEvent,
                     None)

    def getTextForLabel(self):
        """Returns text for my label.

        The default implementation of AttributeWidget is a gtk.Label
        widget.  You may override this method to change the text which
        appears in the label.  However, if you don't want to be a
        label, override _makeWidgetObject instead.
        """
        return self.identifier

    def _makeWidgetObject(self):
        """Make the GTK widget object that is me.

        Called by __init__ to construct the GtkObject I wrap-- the ._o
        member of a pygtk GtkObject.  Isn't subclassing GtkObjects in
        Python fun?
        """
        ebox = gtk.EventBox()
        label = gtk.Label(self.getTextForLabel())
        label.set_alignment(0,0)
        ebox.add(label)
        return ebox._o

    def signal_destroy(self, unused_object, unused_data):
        del self.explorer

    def signal_buttonPressEvent(self, widget, eventButton, unused_data):
        if eventButton.type == GDK._2BUTTON_PRESS:
            if self.parent.canvas.visages.has_key(self.explorer.id):
                visage = self.parent.canvas.visages[self.explorer.id]
            else:
                visage = self.explorer.newVisage(self.parent.rootGroup,
                                                 self.parent.canvas)
            (x, y, w, h) = self.get_allocation()
            wx, wy = self.parent.canvas.c2w(x, y)

            x1, y1, x2, y2 = self.parent.get_bounds()

            v_x1, v_y1, v_x2, v_y2 = visage.get_bounds()

            visage.move(x2 - v_x1, wy + y1 - v_y1)


#### Widget-specific subclasses of Explorer, Visage, and Attribute

# Instance

class ExplorerInstance(Explorer):
    pass

class InstanceVisage(Visage):
    # Detail levels:
    # Just me
    # me and my class
    # me and my whole class heirarchy

    propertyLabels = [('klass', "Class")]
    groupLabels = [('data', "Data"),
                   ('methods', "Methods")]

    detail = 0

    def __init__(self, explorer, group, canvas):
        Visage.__init__(self, explorer, group, canvas)

        class_identifier = self.explorer.klass.name
        # XXX: include partial module name in class?
        self.frame.set_label("%s (%s)" % (self.identifier,
                                          class_identifier))

class InstanceAttributeWidget(AttributeWidget):
    def getTextForLabel(self):
        return "%s instance" % (self.explorer.klass.name,)


# Class

class ExplorerClass(Explorer):
    pass

class ClassVisage(Visage):
    propertyLabels = [("name", "Name"),
                      ("module", "Module"),
                      ("bases", "Bases")]
    groupLabels = [('data', "Data"),
                   ('methods', "Methods")]

    def fill_properties(self, propValues):
        Visage.fill_properties(self, propValues)
        basesExplorer = propValues.get('bases')[1]
        basesExplorer.view.callRemote("get_elements").addCallback(self.fill_bases)

    def fill_bases(self, baseExplorers):
        box = gtk.HBox()
        for b in baseExplorers:
            box.add(b.newAttributeWidget(self))
        row = self.propertyLabels.get('bases')[0]
        self.subtable["properties"].attach(box, 1, 2, row, row+1)
        box.show_all()

class ClassAttributeWidget(AttributeWidget):
    def getTextForLabel(self):
        return self.explorer.name


# Function

class ExplorerFunction(Explorer):
    pass

class FunctionAttributeWidget(AttributeWidget):
    def getTextForLabel(self):
        signature = self.explorer.signature
        arglist = []
        for arg in xrange(len(signature)):
            name = signature.name[arg]
            hasDefault, default = signature.get_default(arg)
            if hasDefault:
                if default.explorerClass == "ExplorerImmutable":
                    default = default.value
                else:
                    # XXX
                    pass
                a = "%s=%s" % (name, default)
            elif signature.is_varlist(arg):
                a = "*%s" % (name,)
            elif signature.is_keyword(arg):
                a = "**%s" % (name,)
            else:
                a = name
            arglist.append(a)

        return string.join(arglist, ", ")


# Method

class ExplorerMethod(ExplorerFunction):
    pass

class MethodAttributeWidget(FunctionAttributeWidget):
    pass

class ExplorerBulitin(Explorer):
    pass

class ExplorerModule(Explorer):
    pass

class ExplorerSequence(Explorer):
    pass


# Sequence

class SequenceVisage(Visage):
    propertyLabels = [('len', 'length')]
    # XXX: add elements group

class SequenceAttributeWidget(AttributeWidget):
    def getTextForLabel(self):
        # XXX: Differentiate between lists and tuples.
        if self.explorer.len:
            txt = "list of length %d" % (self.explorer.len,)
        else:
            txt = "[]"
        return txt


# Mapping

class ExplorerMapping(Explorer):
    pass

class MappingVisage(Visage):
    propertyLabels = [('len', 'length')]
    # XXX: add items group

class MappingAttributeWidget(AttributeWidget):
    def getTextForLabel(self):
        if self.explorer.len:
            txt = "dict with %d elements" % (self.explorer.len,)
        else:
            txt = "{}"
        return txt

class ExplorerImmutable(Explorer):
    pass


# Immutable

class ImmutableVisage(Visage):
    def __init__(self, explorer, rootGroup, canvas):
        Visage.__init__(self, explorer, rootGroup, canvas)
        widget = explorer.newAttributeWidget(self)
        self.container.add(widget)
        self.container.show_all()

class ImmutableAttributeWidget(AttributeWidget):
    def getTextForLabel(self):
        return repr(self.explorer.value)


#### misc. module definitions

spelunkerClassTable = {
    "ExplorerInstance": (InstanceVisage, InstanceAttributeWidget),
    "ExplorerFunction": (None, FunctionAttributeWidget),
    "ExplorerMethod": (None, MethodAttributeWidget),
    "ExplorerImmutable": (ImmutableVisage, ImmutableAttributeWidget),
    "ExplorerClass": (ClassVisage, ClassAttributeWidget),
    "ExplorerSequence": (SequenceVisage, SequenceAttributeWidget),
    "ExplorerMapping": (MappingVisage, MappingAttributeWidget),
    }
GenericVisage = Visage
GenericAttributeWidget = AttributeWidget

pb.setCopierForClassTree(sys.modules[__name__],
                         Explorer, 'twisted.manhole.explorer')