# -*- Python -*-
# Twisted, the Framework of Your Internet
# $Id: spelunk_gnome.py,v 1.10 2002/09/21 08:16:49 acapnotic 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')
syntax highlighted by Code2HTML, v. 0.9.1