1 import math
2 import string
3 import time
4 import tkMessageBox
5 from Tkinter import *
6 import Pmw
7 from teamwork.widgets.MultiWin import InnerWindow
8 from teamwork.widgets.bigwidgets import *
9 from teamwork.widgets.fsa import GraphEdgeWidget,GraphWidget
10 from teamwork.agent.support import Supporter
11 from teamwork.agent.Generic import GenericModel
12 from teamwork.agent.Entities import PsychEntity
13 from teamwork.agent.DefaultBased import GenericEntity
14 from teamwork.widgets.images import loadImages
15 try:
16 from pygraphviz import *
17 __GRAPHVIZ__ = True
18 except ImportError:
19 __GRAPHVIZ__ = False
20
22 """Dual-purpose widget that displays the entities graphically.
23
24 Class Hierarchy View
25 ====================
26 - When viewing a L{generic society<teamwork.multiagent.GenericSociety>}, the window displays the entities as nodes in a hierarchy. The links among nodes represent the sub/superclass relationships among them.
27 - Left-clicking on a node will bring the corresponding agent's window
28 to the top of the stack.
29 - Right-clicking on a node will pop up a context menu with the
30 following possible operations:
31 - Rename the class
32 - Add a new subclass
33 - Delete this node (and all of its subclasses)
34 - Raise the corresponding agent's window
35
36 Social Network View
37 ===================
38 - When viewing a L{scenario<teamwork.multiagent.Simulation>}, the window displays the entities as nodes in a social network. The links among nodes represent the current social relationships among them.
39 - Left-clicking on a node will bring the corresponding agent's window
40 to the top of the stack.
41 - Right-clicking on a node will pop up a context menu with the
42 following possible operations:
43 - Center the network on this node, using a given relationship
44 - Raise the corresponding agent's window
45
46 @ivar selected: the node currently selected (C{None} if no node is selected)
47 @type selected: L{CanvasWidget}
48 """
49
51
52 self.images = loadImages({'del': 'icons/user--minus.png',
53 'add': 'icons/user--plus.png',
54 'edit': 'icons/user--pencil.png',
55 'prev': 'left_arrow.png',
56 'next': 'right_arrow.png'})
57 optiondefs = (
58 ('entities', None, self.setview),
59 ('height', 600, None),
60 ('width', 600, None),
61 ('padx', 150, None),
62 ('pady', 180, None),
63 ('balloon', None, None),
64
65 ('nodeWidth', 100, None),
66 ('nodeHeight', 50, None),
67
68 ('delete', None, None),
69
70 ('add', None, None),
71
72 ('rename', None, None),
73
74 ('prefix', "Network", self.setTitle),
75
76 ('likingLabel','Liking', None),
77 ('trustLabel', 'Trust', None),
78
79 ('showWindow', None, Pmw.INITOPT),
80 ('windows', {}, None),
81 ('clipboard', None, Pmw.INITOPT),
82 ('expert', True, self.redrawSupport),
83 ('layout',None,None),
84 )
85 self.defineoptions(kw, optiondefs)
86 self.edgedict = {}
87 self.nodes = {}
88 InnerWindow.__init__(self,frame)
89 toolbar = Frame(self.component('frame'),bd=2,relief='raised')
90 self.createcomponent('Selector',(),None,Pmw.OptionMenu,(toolbar,),
91 command=self.setview).pack(side='right')
92 self.createcomponent('Add',(),None,Button,(toolbar,),
93 command=self.add,state='disabled')
94 try:
95 self.component('Add').configure(image=self.images['add'])
96 except:
97 self.component('Add').configure(text='Add')
98 if self['balloon']:
99 self['balloon'].bind(self.component('Add'),
100 'Create new subclass of entity')
101 self.createcomponent('Remove',(),None,Button,(toolbar,),
102 command=self.remove,state='disabled')
103 try:
104 self.component('Remove').configure(image=self.images['del'])
105 except:
106 self.component('Remove').configure(text='Delete')
107 if self['balloon']:
108 self['balloon'].bind(self.component('Remove'),
109 'Delete selected entity and subclasses')
110 self.createcomponent('Rename',(),None,Button,(toolbar,),
111 command=self.rename,state='disabled')
112 try:
113 self.component('Rename').configure(image=self.images['edit'])
114 except:
115 self.component('Rename').configure(text='Rename')
116 if self['balloon']:
117 self['balloon'].bind(self.component('Rename'),'Rename selected entity')
118 self.createcomponent('Prev',(),None,Button,(toolbar,),
119 command=self.prev)
120 try:
121 self.component('Prev').configure(image=self.images['prev'])
122 except:
123 self.component('Prev').configure(text='Prev')
124 self.createcomponent('Next',(),None,Button,(toolbar,),
125 command=self.next)
126 try:
127 self.component('Next').configure(image=self.images['next'])
128 except:
129 self.component('Next').configure(text='Next')
130 if __GRAPHVIZ__:
131 Button(toolbar,text='Layout',
132 command=self.layout).pack(side='right')
133 toolbar.pack(fill='x',side='top')
134 self.cf = CanvasFrame(parent = self.component('frame'))
135 self.c = self.cf.canvas()
136 self.c.configure(background='white')
137 self.c.bind("<Key>", self.handle_key)
138 self.cf.pack(expand = 'yes', fill = 'both', side='bottom')
139
140 entity = self['entities'].members()[0]
141 self.rel = None
142 self.vname = None
143 self.selected = None
144 self.initialiseoptions()
145
147 """Removes any existing network"""
148 if self.selected:
149 self.selectNode(None,self.selected)
150 self.edgedict.clear()
151 self.nodes.clear()
152 self.edges = []
153 self.c.delete('all')
154
155 - def setview(self,rel=None,name=None):
156 """Redraws network to center on the named entity
157 @param name: the entity to center on
158 @type name: str
159 @param rel: the relationship to use for link weights (if '_parent', then draws the nodes in a hierarchy)
160 @type rel: str
161 """
162 assert len(self['entities']) > 0
163 self.clear()
164 coordinates = {}
165 if rel == 'class':
166 rel = '_parent'
167
168 linkTypes = {}
169 for entity in self['entities'].members():
170 for relation in entity.getLinkTypes():
171 linkTypes[relation] = True
172 linkTypes = linkTypes.keys()
173 if isinstance(self['entities'].members()[0],GenericModel):
174 linkTypes.insert(0,'class')
175 self.component('Selector').setitems(linkTypes)
176
177 if rel is None:
178 if self.rel in ['_parent',None] and \
179 not isinstance(self['entities'].members()[0],GenericModel):
180 self.rel = Supporter._supportFeature
181 elif self.rel is None and \
182 isinstance(self['entities'].members()[0],GenericModel):
183 self.rel = '_parent'
184 else:
185 self.rel = rel
186 if name is None:
187 if self.vname is None or not self['entities'].has_key(self.vname):
188 self.vname = self['entities'].keys()[0]
189 else:
190 name = self.vname
191 else:
192 self.vname = name
193 coords = {}
194 if self.rel == '_parent':
195 self.component('Add').pack(side='left')
196 self.component('Remove').pack(side='left')
197 self.component('Rename').pack(side='left')
198 self.component('Next').pack_forget()
199 self.component('Prev').pack_forget()
200 self['title'] = 'Class %s' % (self['prefix'])
201 self.setTitle()
202 for entity in self['entities'].members():
203 try:
204 coords[entity.name] = entity.attributes['coords']
205 except KeyError:
206 coords[entity.name] = None
207 else:
208 self.component('Add').pack_forget()
209 self.component('Remove').pack_forget()
210 self.component('Rename').pack_forget()
211 self.component('Prev').pack(side='left')
212 self.component('Next').pack(side='left')
213 self['title'] = '%s %s' % (self.rel.capitalize(),self['prefix'])
214
215 entity = self['entities'][self.vname]
216 x = float(self['width']-self['padx'])/2.
217 y = float(self['height']-self['pady'])/2.
218 coords[entity.name] = (x,y)
219
220 index = 0
221 offset = None
222 linkees = entity.getLinkees(self.rel)
223 for other in linkees:
224 if entity.name != other:
225 if offset is None:
226 offset = 2.0*math.pi/float(len(linkees))
227 xloc, yloc = self.PickXY(other,float(index)*offset)
228 coords[other] = (xloc,yloc)
229 index += 1
230 assert len(coords) > 0
231
232 for name in coords.keys():
233 entity = self['entities'][name]
234 self.nodes[name] = self.PickWidget(self.c, entity)
235
236 if self.rel == '_parent':
237
238 links = []
239 edges = {}
240 for name in self['entities'].keys():
241 edges[name] = {}
242 for other in filter(lambda n:n!=name,self['entities'].keys()):
243 edges[name][other] = 1.
244 for entity in self['entities'].members():
245 for parent in entity.getParents():
246 assert self['entities'].has_key(parent)
247 label = TextWidget(self.c,'')
248 edge = GraphEdgeWidget(self.c,0,0,0,0,label)
249 self.edgedict['%s_%s' % (entity.name,parent)] = edge
250 links.append((self.nodes[entity.name],
251 self.nodes[parent],edge))
252 edges[entity.name][parent] = 10.
253 else:
254 links = []
255 entity = self['entities'][self.vname]
256 for other in entity.getLinkees(self.rel):
257 label = TextWidget(self.c,'')
258 edge = GraphEdgeWidget(self.c,0,0,0,0,label)
259 self.edgedict['%s_%s' % (self.vname,other)] = edge
260 links.append((self.nodes[self.vname],
261 self.nodes[other],edge))
262
263 self.redrawSupport()
264 assert len(self.nodes) > 0
265 graph = GraphWidget(self.c,self.nodes.values(),links)
266 self.cf.add_widget(graph)
267
268
269 for name,node in self.nodes.items():
270 entity = self['entities'][name]
271 flag = True
272 node.bind_click(self.selectNode,1)
273 node.bind_click(self.context,3)
274 node.bind_click(self.raiseWindow,'double')
275 try:
276 point = coords[name]
277 except KeyError:
278 point = None
279 if point:
280
281 current = self.c.coords(node.tags()[0])
282 deltaX = point[0]-current[0]
283 deltaY = point[1]-current[1]
284 node.move(deltaX,deltaY)
285 else:
286
287 current = self.c.coords(node.tags()[0])
288 node.move(-current[0],-current[1])
289 entity.attributes['coords'] = self.c.coords(node.tags()[0])
290
291 - def PickXY(self,name,angle=0.):
292 x = 0.5+math.cos(angle)/2.
293 y = 0.5+math.sin(angle)/2.
294 return float(self['width']-self['padx'])*x, \
295 float(self['height']-self['pady'])*y
296
297
336
338 g = AGraph(strict=False,directed=True)
339 for entity in self['entities'].members():
340 g.add_node(entity.name)
341 for entity in self['entities'].members():
342 for child in self['entities'].network[entity.name]:
343 g.add_edge(entity.name,child)
344 if self['layout']:
345 g.layout(self['layout'])
346 else:
347 g.layout()
348 for entity in self['entities'].members():
349 x,y = map(int,g.get_node(entity.name).attr['pos'].split(','))
350 x *= 2
351 y *= 2
352 node = self.nodes[entity.name]
353 current = self.c.coords(node.tags()[0])
354 deltaX = x-current[0]
355 deltaY = y-current[1]
356 node.move(deltaX,deltaY)
357
359 entities = map(lambda e: e.name,self['entities'].members())
360 entities.sort(lambda x,y: cmp(x.lower(),y.lower()))
361 index = entities.index(self.vname) - 1
362 if index < 0:
363 index += len(entities)
364 self.setview(name=entities[index])
365
367 entities = map(lambda e: e.name,self['entities'].members())
368 entities.sort(lambda x,y: cmp(x.lower(),y.lower()))
369 index = entities.index(self.vname) + 1
370 if index >= len(entities):
371 index = 0
372 self.setview(name=entities[index])
373
375 """Updates thickness and color of any links in network"""
376 for s1 in self['entities'].members():
377 for s2 in self['entities'].members():
378 key = s1.name + '_' + s2.name
379 if self.edgedict.has_key(key):
380 edge = self.edgedict[key]
381 if self.rel == '_parent':
382
383 pass
384 else:
385 value = float(s1.getLink(self.rel,s2.name))
386 if self['expert']:
387 edge._label.set_text(str(value))
388 else:
389 edge._label.set_text('')
390 if value < 0.:
391 width = int(-10.*value)
392 color = 'red'
393 elif value > 0.:
394 width = int(10.*value)
395 color = 'green'
396 else:
397 width = value
398 color = 'black'
399 edge['color'] = color
400 edge['width'] = max(1,width)
401
403 """Callback when selecting a node (left-click)
404 """
405 if isinstance(widget,StackWidget):
406 child = widget._children[1]
407 else:
408 child = widget.child()
409 entity = self['entities'][child.text()]
410 palette = Pmw.Color.getdefaultpalette(self.parent.parent)
411 if self.selected:
412 self.selected.child()['color'] = palette['foreground']
413 self.selected['fill'] = palette['background']
414 if self.selected == widget:
415
416 self.selected = None
417 self.component('Add').configure(state='disabled')
418 self.component('Remove').configure(state='disabled')
419 self.component('Rename').configure(state='disabled')
420 else:
421
422 widget.child()['color'] = palette['selectForeground']
423 widget['fill'] = palette['selectBackground']
424 self.selected = widget
425 self.component('Add').configure(state='normal')
426 self.component('Remove').configure(state='normal')
427 self.component('Rename').configure(state='normal')
428 if self['clipboard'] and isinstance(entity,GenericModel):
429 self['clipboard'](self.selected,self)
430
431 - def copy(self,node):
432 for name in self.nodes.keys():
433 if self.nodes[name] is node:
434 break
435 else:
436 raise UserWarning,'Unable to find node to copy'
437 nodes = [self['entities'][name]]
438 next = 0
439 while next < len(nodes):
440 for entity in self['entities'].network[nodes[next].name]:
441 if not entity in nodes:
442 nodes.append(self['entities'][entity])
443 next += 1
444 return nodes
445
446 - def cut(self,node):
447 result = self.copy(node)
448
449 parents = {}
450 for node in result[1:]:
451 parents[node.name] = node.getParents()[:]
452
453 self.selectNode(None,self.selected)
454 self.remove(result[0],True)
455
456 while len(result[0].getParents()):
457 result[0].parentModels.pop()
458
459 for node in result[1:]:
460 node.parentModels = parents[node.name]
461 return result
462
464 if self.selected is None:
465
466 return False
467 for parent in self.nodes.keys():
468 if self.nodes[parent] is self.selected:
469 break
470 else:
471 raise UserWarning,'Unable to find node to paste to'
472 self.add(self['entities'][parent],nodes[0])
473 for node in nodes[1:]:
474 self.add(self['entities'][node.getParents()[0]],node)
475 return True
476
478 if isinstance(widget,StackWidget):
479 child = widget._children[1]
480 else:
481 child = widget.child()
482 entity = self['entities'][child.text()]
483 self['showWindow'](entity.name)
484
485 - def context(self,event,widget):
486 """Pops up a context-sensitive menu in the network"""
487 if isinstance(widget,StackWidget):
488 child = widget._children[1]
489 else:
490 child = widget.child()
491 entity = self['entities'][child.text()]
492 menu = Menu(self.component('frame'),tearoff=0)
493 if isinstance(entity,GenericModel):
494
495 if len(entity.getParents()) > 0:
496
497 menu.add_command(label='Rename',
498 command=lambda s=self,e=entity:s.rename(e))
499 menu.add_command(label='Add subclass',
500 command=lambda s=self,e=entity:s.add(e))
501 menu.add_command(label='Delete class',
502 command=lambda s=self,e=entity:s.remove(e))
503 menu.add_separator()
504 if self.rel != '_parent':
505 menu.add_command(label='Show class hierarchy',
506 command=lambda s=self:
507 s.setview(name=None,rel='_parent'))
508 if isinstance(entity,Supporter):
509
510 if entity.name != self.vname or \
511 self.rel != Supporter._supportFeature:
512 menu.add_command(label='Center and show liking',
513 command=lambda s=self,n=entity.name:
514 s.setview(name=n,
515 rel=Supporter._supportFeature))
516 if entity.name != self.vname or \
517 self.rel != Supporter._trustFeature:
518 menu.add_command(label='Center and show trust',
519 command=lambda s=self,n=entity.name:
520 s.setview(name=n,
521 rel=Supporter._trustFeature))
522 if entity.name != self.vname and self.rel != '_parent' and \
523 isinstance(entity,GenericModel):
524 menu.add_command(label='Delete link',
525 command=lambda s=self,n=entity.name:
526 s.delLink(n))
527 menu.add_separator()
528 menu.add_command(label='Show window',
529 command=lambda c=self['showWindow'],
530 n=entity.name:c(n))
531 menu.bind("<Leave>",self.unpost)
532 menu.post(event.x_root,event.y_root)
533
535 """Stores the coordinates of the nodes in their corresponding L{Agent<teamwork.agent.Agent.Agent>} objects
536 """
537 if self.rel == '_parent':
538 if isinstance(widget,StackWidget):
539 name = widget._children[1].text()
540 else:
541 name = widget.child().text()
542 entity = self['entities'][name]
543 node = self.nodes[entity.name]
544 entity.attributes['coords'] = self.c.coords(node.tags()[0])
545
546 - def unpost(self,event):
547 event.widget.unpost()
548
549 - def add(self,entity=None,agent=None):
550 """Adds a child to the selected entity"""
551 if entity is None:
552 entity = self['entities'][self.selected.child().text()]
553 if self['add']:
554 self['add'](entity,agent)
555 self.setview(name=self.vname)
556
557 - def remove(self,entity=None,confirm=None):
558 """Removes the selected entity from the group"""
559 if entity is None:
560 entity = self['entities'][self.selected.child().text()]
561 if confirm is None:
562 msg = 'Are you sure you want to permanently delete the entity %s and all of its descendent classes?' % \
563 (entity.name)
564 confirm = tkMessageBox.askyesno('Confirm Delete',msg)
565 if confirm:
566 if self['delete']:
567 self['delete'](entity)
568 if not self['entities'].has_key(entity.name):
569
570 if self.vname == entity.name:
571 self.setview(name=self['entities'].members()[0].name)
572 else:
573 self.setview(name=self.vname)
574
576 """Adds the link from the center entity to the named one"""
577 self['entities'][self.vname].setLink(self.rel,entity,0.)
578 self.setview(name=self.vname)
579 if self['windows'].has_key(self.vname):
580 widget = self['windows'][self.vname].component('Relationships')
581 widget.addDynamic(self.rel)
582
584 """Removes the link from the center entity to the named one"""
585 msg = 'Do you really want to delete the link, %s %s %s?' % \
586 (self.vname,self.rel,entity)
587 if tkMessageBox.askyesno('Confirm Delete',msg):
588 self['entities'][self.vname].removeLink(self.rel,entity)
589 self.setview(name=self.vname)
590 if self['windows'].has_key(self.vname):
591 widget = self['windows'][self.vname].component('Relationships')
592 widget.addDynamic(self.rel)
593
594 - def rename(self,entity=None):
595 if entity is None:
596 node = self.selected
597 else:
598 node = self.nodes[entity.name]
599 tag = node.child().tags()[0]
600 widget = self.c.find_withtag(tag)
601 self.c.focus_set()
602 self.c.focus(widget)
603 self.c.select_from(widget, 0)
604 self.c.select_to(widget,'end')
605
607
608 return self.c.tk.call(self.c._w, 'select', 'item')
609
611
612
613
614 bbox = self.c.bbox(item)
615 self.c.delete("highlight")
616 if bbox:
617 i = self.c.create_rectangle(bbox, fill="white",tag="highlight")
618 self.c.lower(i, item)
619
621
622 item = self.c.focus()
623 if not item:
624 return
625 try:
626 insert = self.c.index(item, INSERT)
627 except:
628
629 return
630 if event.char >= " ":
631
632 if self.has_selection():
633 self.c.dchars(item, SEL_FIRST, SEL_LAST)
634 self.c.select_clear()
635 self.c.insert(item, "insert", event.char)
636 self.highlight(item)
637
638 elif event.keysym == "BackSpace":
639 if self.has_selection():
640 self.c.dchars(item, SEL_FIRST, SEL_LAST)
641 self.c.select_clear()
642 else:
643 if insert > 0:
644 self.c.dchars(item, insert-1, insert-1)
645 self.highlight(item)
646
647
648 elif event.keysym == "Home":
649 self.c.icursor(item, 0)
650 self.c.select_clear()
651 elif event.keysym == "End":
652 self.c.icursor(item, END)
653 self.c.select_clear()
654 elif event.keysym == "Right":
655 self.c.icursor(item, insert+1)
656 self.c.select_clear()
657 elif event.keysym == "Left":
658 self.c.icursor(item, insert-1)
659 self.c.select_clear()
660 elif event.keysym == 'Return':
661 item = int(item)
662 for node in self.nodes.values():
663 if item == node.child()._tag:
664 break
665 else:
666 raise UserWarning,'Attempted to rename non-agent object!'
667
668 new = node.child().text()
669 old = node.child()._text
670 if len(new) == 0:
671 tkMessageBox.showerror('Illegal Agent Name','An agent cannot have an empty name.')
672 elif new != old and self.nodes.has_key(new):
673 tkMessageBox.showerror('Illegal Agent Name','Agent names cannot be duplicated.')
674 else:
675 self.c.focus('')
676 if old != new:
677 node.child()._text = new
678 del self.nodes[old]
679 self.nodes[new] = node
680 node.update(node.child())
681 if self['rename']:
682
683 self['rename'](old,new)
684 self.c.delete("highlight")
685 else:
686 pass
687