2
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
3
# See LICENSE for details.
6
"""Object browser GUI, GnomeCanvas implementation.
9
from twisted.python import log
12
# gzigzag-style navigation
15
def __init__(self, module, prefix):
16
self.__module = module
17
self.__prefix = prefix
19
def __getattr__(self, attr):
21
return getattr(self.__module, self.__prefix + attr)
22
except AttributeError:
23
return getattr(self.__module, attr)
26
# We use gnome.ui because that's what happens to have Python bindings
27
# for the Canvas. I think this canvas widget is available seperately
28
# in "libart", but nobody's given me Python bindings for just that.
30
# The Gnome canvas is said to be modeled after the Tk canvas, so we
31
# could probably write this in Tk too. But my experience is with GTK,
32
# not with Tk, so this is what I use.
35
gnome = SillyModule(gnome.ui, 'Gnome')
38
(True, False) = (gtk.TRUE, gtk.FALSE)
39
gtk = SillyModule(gtk, 'Gtk')
43
from twisted.python import reflect, text
44
from twisted.spread import pb
45
from twisted.manhole import explorer
47
import string, sys, types
53
class PairList(UserList.UserList):
54
"""An ordered list of key, value pairs.
56
Kinda like an ordered dictionary. Made with small data sets
57
in mind, as get() does a linear search, not hashing.
61
for k, v in self.data:
69
return map(lambda x: x[0], self.data)
74
class SpelunkDisplay(gnome.Canvas):
77
The top-level widget for this module. This gtk.Widget is where the
78
explorer display will be, and this object is also your interface to
81
def __init__(self, aa=False):
82
gnome.Canvas.__init__(self, aa)
83
self.set_pixels_per_unit(_PIXELS_PER_UNIT)
86
def makeDefaultCanvas(self):
87
"""Make myself the default canvas which new visages are created on.
89
# XXX: For some reason, the 'canvas' and 'parent' properties
90
# of CanvasItems aren't accessible thorugh pygnome.
91
Explorer.canvas = self
93
def receiveExplorer(self, xplorer):
94
if self.visages.has_key(xplorer.id):
95
log.msg("Using cached visage for %d" % (xplorer.id, ))
96
# Ikk. Just because we just received this explorer, that
97
# doesn't necessarily mean its attributes are fresh. Fix
98
# that, either by having this side pull or the server
100
visage = self.visages[xplorer.id]
101
#xplorer.give_properties(visage)
102
#xplorer.give_attributes(visage)
104
log.msg("Making new visage for %d" % (xplorer.id, ))
105
self.visages[xplorer.id] = xplorer.newVisage(self.root(),
110
class Explorer(pb.RemoteCache):
111
"""Base class for all RemoteCaches of explorer.Explorer cachables.
113
Meaning that when an Explorer comes back over the wire, one of
114
these is created. From this, you can make a Visage for the
115
SpelunkDisplay, or a widget to display as an Attribute.
122
attributeGroups = None
124
def newVisage(self, group, canvas=None):
125
"""Make a new visage for the object I explore.
129
canvas = canvas or self.canvas
130
klass = spelunkerClassTable.get(self.explorerClass, None)
131
if (not klass) or (klass[0] is None):
132
log.msg("%s not in table, using generic" % self.explorerClass)
133
klass = GenericVisage
136
spelunker = klass(self, group, canvas)
137
if hasattr(canvas, "visages") \
138
and not canvas.visages.has_key(self.id):
139
canvas.visages[self.id] = spelunker
141
self.give_properties(spelunker)
143
self.give_attributes(spelunker)
147
def newAttributeWidget(self, group):
148
"""Make a new attribute item for my object.
150
Returns a gtk.Widget.
152
klass = spelunkerClassTable.get(self.explorerClass, None)
153
if (not klass) or (klass[1] is None):
154
log.msg("%s not in table, using generic" % self.explorerClass)
155
klass = GenericAttributeWidget
159
return klass(self, group)
161
def give_properties(self, spelunker):
162
"""Give a spelunker my properties in an ordered list.
164
valuelist = PairList()
165
for p in spelunker.propertyLabels.keys():
166
value = getattr(self, p, None)
167
valuelist.append((p,value))
168
spelunker.fill_properties(valuelist)
170
def give_attributes(self, spelunker):
171
for a in spelunker.groupLabels.keys():
172
things = getattr(self, a)
173
spelunker.fill_attributeGroup(a, things)
175
class _LooseBoxBorder:
179
def __init__(self, box):
182
class LooseBox(gnome.CanvasGroup):
184
self.border = _LooseBoxBorder(self)
186
class Visage(gnome.CanvasGroup):
187
"""A \"face\" of an object under exploration.
189
A Visage is a representation of an object presented to the user.
190
The \"face\" in \"interface\".
192
'propertyLabels' and 'groupLabels' are lists of (key, name)
193
2-ples, with 'key' being the string the property or group is
194
denoted by in the code, and 'name' being the pretty human-readable
195
string you want me to show on the Visage. These attributes are
196
accumulated from base classes as well.
198
I am a gnome.CanvasItem (more specifically, CanvasGroup).
200
color = {'border': '#006644'}
203
# These are mappings from the strings the code calls these by
204
# and the pretty names you want to see on the screen.
205
# (e.g. Capitalized or localized)
212
def __init__(self, explorer, rootGroup, canvas):
213
"""Place a new Visage of an explorer in a canvas group.
215
I also need a 'canvas' reference is for certain coordinate
216
conversions, and pygnome doesn't give access to my GtkObject's
217
.canvas attribute. :(
219
# Ugh. PyGtk/GtkObject/GnomeCanvas interfacing grits.
220
gnome.CanvasGroup.__init__(self,
221
_obj = rootGroup.add('group')._o)
223
self.propertyLabels = PairList()
224
reflect.accumulateClassList(self.__class__, 'propertyLabels',
226
self.groupLabels = PairList()
227
reflect.accumulateClassList(self.__class__, 'groupLabels',
230
self.explorer = explorer
231
self.identifier = explorer.identifier
232
self.objectId = explorer.id
235
self.rootGroup = rootGroup
237
self.ebox = gtk.EventBox()
238
self.ebox.set_name("Visage")
239
self.frame = gtk.Frame(self.identifier)
240
self.container = gtk.VBox()
241
self.ebox.add(self.frame)
242
self.frame.add(self.container)
244
self.canvasWidget = self.add('widget', widget=self.ebox,
245
x=0, y=0, anchor=gtk.ANCHOR_NW,
248
self.border = self.add('rect', x1=0, y1=0,
251
outline_color=self.color['border'],
252
width_pixels=self.border_width)
260
# Movable/resizeable me
262
# Set my detail level
264
self.frame.connect("size_allocate", self.signal_size_allocate,
266
self.connect("destroy", self.signal_destroy, None)
267
self.connect("event", self.signal_event)
271
# Our creator will call our fill_ methods when she has the goods.
273
def _setup_table(self):
274
"""Called by __init__ to set up my main table.
276
You can easily override me instead of clobbering __init__.
279
table = gtk.Table(len(self.propertyLabels), 2)
280
self.container.add(table)
281
table.set_name("PropertyTable")
282
self.subtable['properties'] = table
285
for p, name in self.propertyLabels:
286
label = gtk.Label(name)
287
label.set_name("PropertyName")
288
label.set_data("property", p)
289
table.attach(label, 0, 1, row, row + 1)
290
label.set_alignment(0, 0)
293
# XXX: make these guys collapsable
294
for g, name in self.groupLabels:
295
table = gtk.Table(1, 2)
296
self.container.add(table)
297
table.set_name("AttributeGroupTable")
298
self.subtable[g] = table
299
label = gtk.Label(name)
300
label.set_name("AttributeGroupTitle")
301
table.attach(label, 0, 2, 0, 1)
303
def fill_properties(self, propValues):
304
"""Fill in values for my properites.
306
Takes a list of (name, value) pairs. 'name' should be one of
307
the keys in my propertyLabels, and 'value' either an Explorer
310
table = self.subtable['properties']
312
table.resize(len(propValues), 2)
314
# XXX: Do I need to destroy previously attached children?
316
for name, value in propValues:
317
self.fill_property(name, value)
321
def fill_property(self, property, value):
322
"""Set a value for a particular property.
324
'property' should be one of the keys in my propertyLabels.
326
row, name = self.propertyLabels.get(property)
327
if type(value) is not types.InstanceType:
328
widget = gtk.Label(str(value))
329
widget.set_alignment(0, 0)
331
widget = value.newAttributeWidget(self)
332
widget.set_name("PropertyValue")
334
self.subtable['properties'].attach(widget, 1, 2, row, row+1)
336
def fill_attributeGroup(self, group, attributes):
337
"""Provide members of an attribute group.
339
'group' should be one of the keys in my groupLabels, and
340
'attributes' a list of (name, value) pairs, with each value as
341
either an Explorer or string.
344
# XXX: How to indicate detail level of members?
346
table = self.subtable[group]
351
table.resize(len(attributes)+1, 2)
353
# XXX: Do I need to destroy previously attached children?
357
for name, value in attributes.items():
358
label = gtk.Label(name)
359
label.set_name("AttributeName")
360
label.set_alignment(0, 0)
362
if type(value) is types.StringType:
363
widget = gtk.Label(value)
364
widget.set_alignment(0, 0)
366
widget = value.newAttributeWidget(self)
368
table.attach(label, 0, 1, row, row + 1)
369
table.attach(widget, 1, 2, row, row + 1)
374
def signal_event(self, widget, event=None):
376
log.msg("Huh? got event signal with no event.")
378
if event.type == GDK.BUTTON_PRESS:
379
if event.button == 1:
380
self.drag_x0, self.drag_y0 = event.x, event.y
382
elif event.type == GDK.MOTION_NOTIFY:
383
if event.state & GDK.BUTTON1_MASK:
384
self.move(event.x - self.drag_x0, event.y - self.drag_y0)
385
self.drag_x0, self.drag_y0 = event.x, event.y
389
def signal_size_allocate(self, frame_widget,
390
unusable_allocation, unused_data):
391
(x, y, w, h) = frame_widget.get_allocation()
393
# XXX: allocation PyCObject is apparently unusable!
394
# (w, h) = allocation.width, allocation.height
396
w, h = (float(w)/_PIXELS_PER_UNIT, float(h)/_PIXELS_PER_UNIT)
398
x1, y1 = (self.canvasWidget['x'], self.canvasWidget['y'])
401
(b['x1'], b['y1'], b['x2'], b['y2']) = (x1, y1, x1+w, y1+h)
403
def signal_destroy(self, unused_object, unused_data):
406
del self.canvasWidget
413
self.subtable.clear()
416
class AttributeWidget(gtk.Widget):
417
"""A widget briefly describing an object.
419
This is similar to a Visage, but has far less detail. This should
420
display only essential identifiying information, a gtk.Widget
421
suitable for including in a single table cell.
423
(gtk.Widgets are used here instead of the more graphically
424
pleasing gnome.CanvasItems because I was too lazy to re-write
425
gtk.table for the canvas. A new table widget/item would be great
426
though, not only for canvas prettiness, but also because we could
427
use one with a mone pythonic API.)
430
def __init__(self, explorer, parent):
431
"""A new AttributeWidget describing an explorer.
435
self.explorer = explorer
436
self.identifier = explorer.identifier
437
self.id = explorer.id
439
widgetObj = self._makeWidgetObject()
440
gtk.Widget.__init__(self, _obj=widgetObj)
441
self.set_name("AttributeValue")
442
self.connect("destroy", self.signal_destroy, None)
443
self.connect("button-press-event", self.signal_buttonPressEvent,
446
def getTextForLabel(self):
447
"""Returns text for my label.
449
The default implementation of AttributeWidget is a gtk.Label
450
widget. You may override this method to change the text which
451
appears in the label. However, if you don't want to be a
452
label, override _makeWidgetObject instead.
454
return self.identifier
456
def _makeWidgetObject(self):
457
"""Make the GTK widget object that is me.
459
Called by __init__ to construct the GtkObject I wrap-- the ._o
460
member of a pygtk GtkObject. Isn't subclassing GtkObjects in
463
ebox = gtk.EventBox()
464
label = gtk.Label(self.getTextForLabel())
465
label.set_alignment(0,0)
469
def signal_destroy(self, unused_object, unused_data):
472
def signal_buttonPressEvent(self, widget, eventButton, unused_data):
473
if eventButton.type == GDK._2BUTTON_PRESS:
474
if self.parent.canvas.visages.has_key(self.explorer.id):
475
visage = self.parent.canvas.visages[self.explorer.id]
477
visage = self.explorer.newVisage(self.parent.rootGroup,
479
(x, y, w, h) = self.get_allocation()
480
wx, wy = self.parent.canvas.c2w(x, y)
482
x1, y1, x2, y2 = self.parent.get_bounds()
484
v_x1, v_y1, v_x2, v_y2 = visage.get_bounds()
486
visage.move(x2 - v_x1, wy + y1 - v_y1)
489
#### Widget-specific subclasses of Explorer, Visage, and Attribute
493
class ExplorerInstance(Explorer):
496
class InstanceVisage(Visage):
500
# me and my whole class heirarchy
502
propertyLabels = [('klass', "Class")]
503
groupLabels = [('data', "Data"),
504
('methods', "Methods")]
508
def __init__(self, explorer, group, canvas):
509
Visage.__init__(self, explorer, group, canvas)
511
class_identifier = self.explorer.klass.name
512
# XXX: include partial module name in class?
513
self.frame.set_label("%s (%s)" % (self.identifier,
516
class InstanceAttributeWidget(AttributeWidget):
517
def getTextForLabel(self):
518
return "%s instance" % (self.explorer.klass.name,)
523
class ExplorerClass(Explorer):
526
class ClassVisage(Visage):
527
propertyLabels = [("name", "Name"),
528
("module", "Module"),
530
groupLabels = [('data', "Data"),
531
('methods', "Methods")]
533
def fill_properties(self, propValues):
534
Visage.fill_properties(self, propValues)
535
basesExplorer = propValues.get('bases')[1]
536
basesExplorer.view.callRemote("get_elements").addCallback(self.fill_bases)
538
def fill_bases(self, baseExplorers):
540
for b in baseExplorers:
541
box.add(b.newAttributeWidget(self))
542
row = self.propertyLabels.get('bases')[0]
543
self.subtable["properties"].attach(box, 1, 2, row, row+1)
546
class ClassAttributeWidget(AttributeWidget):
547
def getTextForLabel(self):
548
return self.explorer.name
553
class ExplorerFunction(Explorer):
556
class FunctionAttributeWidget(AttributeWidget):
557
def getTextForLabel(self):
558
signature = self.explorer.signature
560
for arg in xrange(len(signature)):
561
name = signature.name[arg]
562
hasDefault, default = signature.get_default(arg)
564
if default.explorerClass == "ExplorerImmutable":
565
default = default.value
569
a = "%s=%s" % (name, default)
570
elif signature.is_varlist(arg):
572
elif signature.is_keyword(arg):
578
return string.join(arglist, ", ")
583
class ExplorerMethod(ExplorerFunction):
586
class MethodAttributeWidget(FunctionAttributeWidget):
589
class ExplorerBulitin(Explorer):
592
class ExplorerModule(Explorer):
595
class ExplorerSequence(Explorer):
601
class SequenceVisage(Visage):
602
propertyLabels = [('len', 'length')]
603
# XXX: add elements group
605
class SequenceAttributeWidget(AttributeWidget):
606
def getTextForLabel(self):
607
# XXX: Differentiate between lists and tuples.
608
if self.explorer.len:
609
txt = "list of length %d" % (self.explorer.len,)
617
class ExplorerMapping(Explorer):
620
class MappingVisage(Visage):
621
propertyLabels = [('len', 'length')]
622
# XXX: add items group
624
class MappingAttributeWidget(AttributeWidget):
625
def getTextForLabel(self):
626
if self.explorer.len:
627
txt = "dict with %d elements" % (self.explorer.len,)
632
class ExplorerImmutable(Explorer):
638
class ImmutableVisage(Visage):
639
def __init__(self, explorer, rootGroup, canvas):
640
Visage.__init__(self, explorer, rootGroup, canvas)
641
widget = explorer.newAttributeWidget(self)
642
self.container.add(widget)
643
self.container.show_all()
645
class ImmutableAttributeWidget(AttributeWidget):
646
def getTextForLabel(self):
647
return repr(self.explorer.value)
650
#### misc. module definitions
652
spelunkerClassTable = {
653
"ExplorerInstance": (InstanceVisage, InstanceAttributeWidget),
654
"ExplorerFunction": (None, FunctionAttributeWidget),
655
"ExplorerMethod": (None, MethodAttributeWidget),
656
"ExplorerImmutable": (ImmutableVisage, ImmutableAttributeWidget),
657
"ExplorerClass": (ClassVisage, ClassAttributeWidget),
658
"ExplorerSequence": (SequenceVisage, SequenceAttributeWidget),
659
"ExplorerMapping": (MappingVisage, MappingAttributeWidget),
661
GenericVisage = Visage
662
GenericAttributeWidget = AttributeWidget
664
pb.setCopierForClassTree(sys.modules[__name__],
665
Explorer, 'twisted.manhole.explorer')