OLD | NEW |
| (Empty) |
1 # -*- Python -*- | |
2 # Copyright (c) 2001-2004 Twisted Matrix Laboratories. | |
3 # See LICENSE for details. | |
4 | |
5 | |
6 """Object browser GUI, GnomeCanvas implementation. | |
7 """ | |
8 | |
9 from twisted.python import log | |
10 | |
11 # TODO: | |
12 # gzigzag-style navigation | |
13 | |
14 class SillyModule: | |
15 def __init__(self, module, prefix): | |
16 self.__module = module | |
17 self.__prefix = prefix | |
18 | |
19 def __getattr__(self, attr): | |
20 try: | |
21 return getattr(self.__module, self.__prefix + attr) | |
22 except AttributeError: | |
23 return getattr(self.__module, attr) | |
24 | |
25 | |
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. | |
29 | |
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. | |
33 | |
34 import gnome.ui | |
35 gnome = SillyModule(gnome.ui, 'Gnome') | |
36 | |
37 import gtk | |
38 (True, False) = (gtk.TRUE, gtk.FALSE) | |
39 gtk = SillyModule(gtk, 'Gtk') | |
40 | |
41 import GDK | |
42 | |
43 from twisted.python import reflect, text | |
44 from twisted.spread import pb | |
45 from twisted.manhole import explorer | |
46 | |
47 import string, sys, types | |
48 import UserList | |
49 _PIXELS_PER_UNIT=10 | |
50 | |
51 #### Support class. | |
52 | |
53 class PairList(UserList.UserList): | |
54 """An ordered list of key, value pairs. | |
55 | |
56 Kinda like an ordered dictionary. Made with small data sets | |
57 in mind, as get() does a linear search, not hashing. | |
58 """ | |
59 def get(self, key): | |
60 i = 0 | |
61 for k, v in self.data: | |
62 if key == k: | |
63 return (i, v) | |
64 i = i + 1 | |
65 else: | |
66 return (None, None) | |
67 | |
68 def keys(self): | |
69 return map(lambda x: x[0], self.data) | |
70 | |
71 | |
72 #### Public | |
73 | |
74 class SpelunkDisplay(gnome.Canvas): | |
75 """Spelunk widget. | |
76 | |
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 | |
79 creating new visages. | |
80 """ | |
81 def __init__(self, aa=False): | |
82 gnome.Canvas.__init__(self, aa) | |
83 self.set_pixels_per_unit(_PIXELS_PER_UNIT) | |
84 self.visages = {} | |
85 | |
86 def makeDefaultCanvas(self): | |
87 """Make myself the default canvas which new visages are created on. | |
88 """ | |
89 # XXX: For some reason, the 'canvas' and 'parent' properties | |
90 # of CanvasItems aren't accessible thorugh pygnome. | |
91 Explorer.canvas = self | |
92 | |
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 | |
99 # side push. | |
100 visage = self.visages[xplorer.id] | |
101 #xplorer.give_properties(visage) | |
102 #xplorer.give_attributes(visage) | |
103 else: | |
104 log.msg("Making new visage for %d" % (xplorer.id, )) | |
105 self.visages[xplorer.id] = xplorer.newVisage(self.root(), | |
106 self) | |
107 | |
108 #### Base classes | |
109 | |
110 class Explorer(pb.RemoteCache): | |
111 """Base class for all RemoteCaches of explorer.Explorer cachables. | |
112 | |
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. | |
116 """ | |
117 canvas = None | |
118 # From our cache: | |
119 id = None | |
120 identifier = None | |
121 explorerClass = None | |
122 attributeGroups = None | |
123 | |
124 def newVisage(self, group, canvas=None): | |
125 """Make a new visage for the object I explore. | |
126 | |
127 Returns a Visage. | |
128 """ | |
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 | |
134 else: | |
135 klass = klass[0] | |
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 | |
140 | |
141 self.give_properties(spelunker) | |
142 | |
143 self.give_attributes(spelunker) | |
144 | |
145 return spelunker | |
146 | |
147 def newAttributeWidget(self, group): | |
148 """Make a new attribute item for my object. | |
149 | |
150 Returns a gtk.Widget. | |
151 """ | |
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 | |
156 else: | |
157 klass = klass[1] | |
158 | |
159 return klass(self, group) | |
160 | |
161 def give_properties(self, spelunker): | |
162 """Give a spelunker my properties in an ordered list. | |
163 """ | |
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) | |
169 | |
170 def give_attributes(self, spelunker): | |
171 for a in spelunker.groupLabels.keys(): | |
172 things = getattr(self, a) | |
173 spelunker.fill_attributeGroup(a, things) | |
174 | |
175 class _LooseBoxBorder: | |
176 box = None | |
177 color = 'black' | |
178 width = 1 | |
179 def __init__(self, box): | |
180 self.box = box | |
181 | |
182 class LooseBox(gnome.CanvasGroup): | |
183 def __init__(self): | |
184 self.border = _LooseBoxBorder(self) | |
185 | |
186 class Visage(gnome.CanvasGroup): | |
187 """A \"face\" of an object under exploration. | |
188 | |
189 A Visage is a representation of an object presented to the user. | |
190 The \"face\" in \"interface\". | |
191 | |
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. | |
197 | |
198 I am a gnome.CanvasItem (more specifically, CanvasGroup). | |
199 """ | |
200 color = {'border': '#006644'} | |
201 border_width = 8 | |
202 detail_level = 0 | |
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) | |
206 propertyLabels = [] | |
207 groupLabels = [] | |
208 | |
209 drag_x0 = 0 | |
210 drag_y0 = 0 | |
211 | |
212 def __init__(self, explorer, rootGroup, canvas): | |
213 """Place a new Visage of an explorer in a canvas group. | |
214 | |
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. :( | |
218 """ | |
219 # Ugh. PyGtk/GtkObject/GnomeCanvas interfacing grits. | |
220 gnome.CanvasGroup.__init__(self, | |
221 _obj = rootGroup.add('group')._o) | |
222 | |
223 self.propertyLabels = PairList() | |
224 reflect.accumulateClassList(self.__class__, 'propertyLabels', | |
225 self.propertyLabels) | |
226 self.groupLabels = PairList() | |
227 reflect.accumulateClassList(self.__class__, 'groupLabels', | |
228 self.groupLabels) | |
229 | |
230 self.explorer = explorer | |
231 self.identifier = explorer.identifier | |
232 self.objectId = explorer.id | |
233 | |
234 self.canvas = canvas | |
235 self.rootGroup = rootGroup | |
236 | |
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) | |
243 | |
244 self.canvasWidget = self.add('widget', widget=self.ebox, | |
245 x=0, y=0, anchor=gtk.ANCHOR_NW, | |
246 size_pixels=0) | |
247 | |
248 self.border = self.add('rect', x1=0, y1=0, | |
249 x2=1, y2=1, | |
250 fill_color=None, | |
251 outline_color=self.color['border'], | |
252 width_pixels=self.border_width) | |
253 | |
254 self.subtable = {} | |
255 | |
256 self._setup_table() | |
257 | |
258 # TODO: | |
259 # Collapse me | |
260 # Movable/resizeable me | |
261 # Destroy me | |
262 # Set my detail level | |
263 | |
264 self.frame.connect("size_allocate", self.signal_size_allocate, | |
265 None) | |
266 self.connect("destroy", self.signal_destroy, None) | |
267 self.connect("event", self.signal_event) | |
268 | |
269 self.ebox.show_all() | |
270 | |
271 # Our creator will call our fill_ methods when she has the goods. | |
272 | |
273 def _setup_table(self): | |
274 """Called by __init__ to set up my main table. | |
275 | |
276 You can easily override me instead of clobbering __init__. | |
277 """ | |
278 | |
279 table = gtk.Table(len(self.propertyLabels), 2) | |
280 self.container.add(table) | |
281 table.set_name("PropertyTable") | |
282 self.subtable['properties'] = table | |
283 row = 0 | |
284 | |
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) | |
291 row = row + 1 | |
292 | |
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) | |
302 | |
303 def fill_properties(self, propValues): | |
304 """Fill in values for my properites. | |
305 | |
306 Takes a list of (name, value) pairs. 'name' should be one of | |
307 the keys in my propertyLabels, and 'value' either an Explorer | |
308 or a string. | |
309 """ | |
310 table = self.subtable['properties'] | |
311 | |
312 table.resize(len(propValues), 2) | |
313 | |
314 # XXX: Do I need to destroy previously attached children? | |
315 | |
316 for name, value in propValues: | |
317 self.fill_property(name, value) | |
318 | |
319 table.show_all() | |
320 | |
321 def fill_property(self, property, value): | |
322 """Set a value for a particular property. | |
323 | |
324 'property' should be one of the keys in my propertyLabels. | |
325 """ | |
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) | |
330 else: | |
331 widget = value.newAttributeWidget(self) | |
332 widget.set_name("PropertyValue") | |
333 | |
334 self.subtable['properties'].attach(widget, 1, 2, row, row+1) | |
335 | |
336 def fill_attributeGroup(self, group, attributes): | |
337 """Provide members of an attribute group. | |
338 | |
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. | |
342 """ | |
343 | |
344 # XXX: How to indicate detail level of members? | |
345 | |
346 table = self.subtable[group] | |
347 if not attributes: | |
348 table.hide() | |
349 return | |
350 | |
351 table.resize(len(attributes)+1, 2) | |
352 | |
353 # XXX: Do I need to destroy previously attached children? | |
354 | |
355 row = 1 # 0 is title | |
356 | |
357 for name, value in attributes.items(): | |
358 label = gtk.Label(name) | |
359 label.set_name("AttributeName") | |
360 label.set_alignment(0, 0) | |
361 | |
362 if type(value) is types.StringType: | |
363 widget = gtk.Label(value) | |
364 widget.set_alignment(0, 0) | |
365 else: | |
366 widget = value.newAttributeWidget(self) | |
367 | |
368 table.attach(label, 0, 1, row, row + 1) | |
369 table.attach(widget, 1, 2, row, row + 1) | |
370 row = row + 1 | |
371 | |
372 table.show_all() | |
373 | |
374 def signal_event(self, widget, event=None): | |
375 if not event: | |
376 log.msg("Huh? got event signal with no event.") | |
377 return | |
378 if event.type == GDK.BUTTON_PRESS: | |
379 if event.button == 1: | |
380 self.drag_x0, self.drag_y0 = event.x, event.y | |
381 return True | |
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 | |
386 return True | |
387 return False | |
388 | |
389 def signal_size_allocate(self, frame_widget, | |
390 unusable_allocation, unused_data): | |
391 (x, y, w, h) = frame_widget.get_allocation() | |
392 | |
393 # XXX: allocation PyCObject is apparently unusable! | |
394 # (w, h) = allocation.width, allocation.height | |
395 | |
396 w, h = (float(w)/_PIXELS_PER_UNIT, float(h)/_PIXELS_PER_UNIT) | |
397 | |
398 x1, y1 = (self.canvasWidget['x'], self.canvasWidget['y']) | |
399 | |
400 b = self.border | |
401 (b['x1'], b['y1'], b['x2'], b['y2']) = (x1, y1, x1+w, y1+h) | |
402 | |
403 def signal_destroy(self, unused_object, unused_data): | |
404 del self.explorer | |
405 | |
406 del self.canvasWidget | |
407 del self.border | |
408 | |
409 del self.ebox | |
410 del self.frame | |
411 del self.container | |
412 | |
413 self.subtable.clear() | |
414 | |
415 | |
416 class AttributeWidget(gtk.Widget): | |
417 """A widget briefly describing an object. | |
418 | |
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. | |
422 | |
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.) | |
428 | |
429 """ | |
430 def __init__(self, explorer, parent): | |
431 """A new AttributeWidget describing an explorer. | |
432 """ | |
433 self.parent = parent | |
434 | |
435 self.explorer = explorer | |
436 self.identifier = explorer.identifier | |
437 self.id = explorer.id | |
438 | |
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, | |
444 None) | |
445 | |
446 def getTextForLabel(self): | |
447 """Returns text for my label. | |
448 | |
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. | |
453 """ | |
454 return self.identifier | |
455 | |
456 def _makeWidgetObject(self): | |
457 """Make the GTK widget object that is me. | |
458 | |
459 Called by __init__ to construct the GtkObject I wrap-- the ._o | |
460 member of a pygtk GtkObject. Isn't subclassing GtkObjects in | |
461 Python fun? | |
462 """ | |
463 ebox = gtk.EventBox() | |
464 label = gtk.Label(self.getTextForLabel()) | |
465 label.set_alignment(0,0) | |
466 ebox.add(label) | |
467 return ebox._o | |
468 | |
469 def signal_destroy(self, unused_object, unused_data): | |
470 del self.explorer | |
471 | |
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] | |
476 else: | |
477 visage = self.explorer.newVisage(self.parent.rootGroup, | |
478 self.parent.canvas) | |
479 (x, y, w, h) = self.get_allocation() | |
480 wx, wy = self.parent.canvas.c2w(x, y) | |
481 | |
482 x1, y1, x2, y2 = self.parent.get_bounds() | |
483 | |
484 v_x1, v_y1, v_x2, v_y2 = visage.get_bounds() | |
485 | |
486 visage.move(x2 - v_x1, wy + y1 - v_y1) | |
487 | |
488 | |
489 #### Widget-specific subclasses of Explorer, Visage, and Attribute | |
490 | |
491 # Instance | |
492 | |
493 class ExplorerInstance(Explorer): | |
494 pass | |
495 | |
496 class InstanceVisage(Visage): | |
497 # Detail levels: | |
498 # Just me | |
499 # me and my class | |
500 # me and my whole class heirarchy | |
501 | |
502 propertyLabels = [('klass', "Class")] | |
503 groupLabels = [('data', "Data"), | |
504 ('methods', "Methods")] | |
505 | |
506 detail = 0 | |
507 | |
508 def __init__(self, explorer, group, canvas): | |
509 Visage.__init__(self, explorer, group, canvas) | |
510 | |
511 class_identifier = self.explorer.klass.name | |
512 # XXX: include partial module name in class? | |
513 self.frame.set_label("%s (%s)" % (self.identifier, | |
514 class_identifier)) | |
515 | |
516 class InstanceAttributeWidget(AttributeWidget): | |
517 def getTextForLabel(self): | |
518 return "%s instance" % (self.explorer.klass.name,) | |
519 | |
520 | |
521 # Class | |
522 | |
523 class ExplorerClass(Explorer): | |
524 pass | |
525 | |
526 class ClassVisage(Visage): | |
527 propertyLabels = [("name", "Name"), | |
528 ("module", "Module"), | |
529 ("bases", "Bases")] | |
530 groupLabels = [('data', "Data"), | |
531 ('methods', "Methods")] | |
532 | |
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_base
s) | |
537 | |
538 def fill_bases(self, baseExplorers): | |
539 box = gtk.HBox() | |
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) | |
544 box.show_all() | |
545 | |
546 class ClassAttributeWidget(AttributeWidget): | |
547 def getTextForLabel(self): | |
548 return self.explorer.name | |
549 | |
550 | |
551 # Function | |
552 | |
553 class ExplorerFunction(Explorer): | |
554 pass | |
555 | |
556 class FunctionAttributeWidget(AttributeWidget): | |
557 def getTextForLabel(self): | |
558 signature = self.explorer.signature | |
559 arglist = [] | |
560 for arg in xrange(len(signature)): | |
561 name = signature.name[arg] | |
562 hasDefault, default = signature.get_default(arg) | |
563 if hasDefault: | |
564 if default.explorerClass == "ExplorerImmutable": | |
565 default = default.value | |
566 else: | |
567 # XXX | |
568 pass | |
569 a = "%s=%s" % (name, default) | |
570 elif signature.is_varlist(arg): | |
571 a = "*%s" % (name,) | |
572 elif signature.is_keyword(arg): | |
573 a = "**%s" % (name,) | |
574 else: | |
575 a = name | |
576 arglist.append(a) | |
577 | |
578 return string.join(arglist, ", ") | |
579 | |
580 | |
581 # Method | |
582 | |
583 class ExplorerMethod(ExplorerFunction): | |
584 pass | |
585 | |
586 class MethodAttributeWidget(FunctionAttributeWidget): | |
587 pass | |
588 | |
589 class ExplorerBulitin(Explorer): | |
590 pass | |
591 | |
592 class ExplorerModule(Explorer): | |
593 pass | |
594 | |
595 class ExplorerSequence(Explorer): | |
596 pass | |
597 | |
598 | |
599 # Sequence | |
600 | |
601 class SequenceVisage(Visage): | |
602 propertyLabels = [('len', 'length')] | |
603 # XXX: add elements group | |
604 | |
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,) | |
610 else: | |
611 txt = "[]" | |
612 return txt | |
613 | |
614 | |
615 # Mapping | |
616 | |
617 class ExplorerMapping(Explorer): | |
618 pass | |
619 | |
620 class MappingVisage(Visage): | |
621 propertyLabels = [('len', 'length')] | |
622 # XXX: add items group | |
623 | |
624 class MappingAttributeWidget(AttributeWidget): | |
625 def getTextForLabel(self): | |
626 if self.explorer.len: | |
627 txt = "dict with %d elements" % (self.explorer.len,) | |
628 else: | |
629 txt = "{}" | |
630 return txt | |
631 | |
632 class ExplorerImmutable(Explorer): | |
633 pass | |
634 | |
635 | |
636 # Immutable | |
637 | |
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() | |
644 | |
645 class ImmutableAttributeWidget(AttributeWidget): | |
646 def getTextForLabel(self): | |
647 return repr(self.explorer.value) | |
648 | |
649 | |
650 #### misc. module definitions | |
651 | |
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), | |
660 } | |
661 GenericVisage = Visage | |
662 GenericAttributeWidget = AttributeWidget | |
663 | |
664 pb.setCopierForClassTree(sys.modules[__name__], | |
665 Explorer, 'twisted.manhole.explorer') | |
OLD | NEW |