Package teamwork :: Package widgets :: Package PsychGUI :: Module NetworkView
[hide private]
[frames] | no frames]

Source Code for Module teamwork.widgets.PsychGUI.NetworkView

  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       
21 -class PsymWindow(InnerWindow):
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
50 - def __init__(self,frame,**kw):
51 # Set up images if available 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 # Dimensions of the rectangles for each node icon 65 ('nodeWidth', 100, None), 66 ('nodeHeight', 50, None), 67 # Command invoked when deleting a node 68 ('delete', None, None), 69 # Command invoked when adding a node 70 ('add', None, None), 71 # Command invoked when renaming node 72 ('rename', None, None), 73 # Prefix string for the title of this window 74 ('prefix', "Network", self.setTitle), 75 # Relationship label strings 76 ('likingLabel','Liking', None), 77 ('trustLabel', 'Trust', None), 78 # Agent windows 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
146 - def clear(self):
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 # Determine the options for network selector 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 # Save new settings 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 # Set up entity at center 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 # Set up others in a circle around 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 # Draw entity widgets 232 for name in coords.keys(): 233 entity = self['entities'][name] 234 self.nodes[name] = self.PickWidget(self.c, entity) 235 # Event bindings for nodes 236 if self.rel == '_parent': 237 # Draw lines as subclass relations 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 # Update line thickness 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 ## from teamwork.math.forcebased import forcebased 268 ## coords = forcebased(coords,edges,maxIterations=100) 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 # Position node according to stored coordinates 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 # Move the node to the origin 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
298 - def PickWidget(self,parent,entity):
299 """Returns a shaped widget appropriate for the given entity""" 300 palette = Pmw.Color.getdefaultpalette(self.parent.parent) 301 # Find the appropriate shape for this node 302 if isinstance(entity,GenericModel): 303 widget = 'oval' 304 elif isinstance(entity,GenericEntity): 305 try: 306 widget = entity.getDefault('widget') 307 except KeyError: 308 widget = 'oval' 309 if widget is None: 310 widget = 'oval' 311 else: 312 widget = 'oval' 313 # Build the widget of the chosen shape 314 if entity.attributes.has_key('imageName'): 315 image = ImageWidget(parent,entity.attributes['image']) 316 widget = StackWidget(parent,image, 317 TextWidget(self.c,entity.name)) 318 elif widget == 'oval': 319 widget = OvalWidget(self.c,TextWidget(self.c,entity.name,), 320 fill=palette['background'], 321 outline=palette['foreground']) 322 elif widget == 'polygon': 323 widget = PolygonWidget(parent,TextWidget(parent,entity.name), 324 fill=palette['background'],outline='red',width=2, 325 margin=10) 326 elif widget == 'box': 327 widget = BoxWidget(parent,TextWidget(parent,entity.name), 328 fill=palette['background'],width=2, 329 outline=palette['foreground'],margin=10) 330 else: 331 raise NameError,'Unknown widget type %s' % widget 332 if self.rel == '_parent': 333 widget['draggable'] = True 334 widget.bind_drag(self.readCoords) 335 return widget
336
337 - def layout(self):
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
358 - def prev(self):
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
366 - def next(self):
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
374 - def redrawSupport(self):
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 # What should the lines look like in the hierarchy? 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
402 - def selectNode(self,event,widget):
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 # Deselect node 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 # Newly selected node 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 # Save parents for subtree, which will get clobbered upon deletion 449 parents = {} 450 for node in result[1:]: 451 parents[node.name] = node.getParents()[:] 452 # Delete copied nodes as well 453 self.selectNode(None,self.selected) 454 self.remove(result[0],True) 455 # Cut agent no longer has parent links 456 while len(result[0].getParents()): 457 result[0].parentModels.pop() 458 # Descendent agents maintain previous parent links 459 for node in result[1:]: 460 node.parentModels = parents[node.name] 461 return result
462
463 - def paste(self,nodes):
464 if self.selected is None: 465 # Haven't selected anywhere to paste to 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
477 - def raiseWindow(self,event,widget):
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 # Menu allows modification of hierarchy 495 if len(entity.getParents()) > 0: 496 # Can't rename root node 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 # Menu allows re-centering of support graph 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
534 - def readCoords(self,widget):
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 # We've actually deleted the class, so update view 570 if self.vname == entity.name: 571 self.setview(name=self['entities'].members()[0].name) 572 else: 573 self.setview(name=self.vname)
574 582 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
606 - def has_selection(self):
607 # hack to work around bug in Tkinter 1.101 (Python 1.5.1) 608 return self.c.tk.call(self.c._w, 'select', 'item')
609
610 - def highlight(self, item):
611 # mark focused item. note that this code recreates the 612 # rectangle for each update, but that's fast enough for 613 # this case. 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
620 - def handle_key(self, event):
621 # widget-wide key dispatcher 622 item = self.c.focus() 623 if not item: 624 return 625 try: 626 insert = self.c.index(item, INSERT) 627 except: 628 # Focus call doesn't work on Mac? 629 return 630 if event.char >= " ": 631 # printable character 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 # navigation 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 # Grab and test new name 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 # Update agent references 683 self['rename'](old,new) 684 self.c.delete("highlight") 685 else: 686 pass
687