def InitSizeAndObjs(self): # Only call this once enclosing frame has been set up, so that get correct world coord dimensions self.canvas_resizer = CanvasResizer(canvas=self) # Don't assert canvas size sanity anymore as wxpython3 (phoenix) doesn't set canvas size # as quickly as wxpython2.8 does, even though frame has been sized and shown # with frame.SetSize(WINDOW_SIZE) and frame.Show(True) # In wxpython3 (phoenix) canvas stays at (20,20) despite the frame increasing in size to (1024,768) # but good ole wxpython2.8 does indeed change canvas size immediately to (1024,768) # # assert not self.canvas_resizer.canvas_too_small(), "InitSizeAndObjs being called too early - please set up enclosing frame size first" self.umlworkspace = UmlWorkspace() self.layout = LayoutBasic(leftmargin=5, topmargin=5, verticalwhitespace=50, horizontalwhitespace=50, maxclassesperline=7) self.snapshot_mgr = GraphSnapshotMgr(graph=self.umlworkspace.graph, umlcanvas=self) self.coordmapper = CoordinateMapper(self.umlworkspace.graph, self.GetSize()) self.layouter = GraphLayoutSpring(self.umlworkspace.graph, gui=self) self.overlap_remover = OverlapRemoval(self.umlworkspace.graph, margin=50, gui=self)
def __init__(self, graph, oglcanvas): self.graph = graph self.oglcanvas = oglcanvas self.oglcanvas.graphrendererogl = self self.coordmapper = CoordinateMapper(self.graph, self.oglcanvas.GetSize()) self.oglcanvas.Bind(wx.EVT_MOUSEWHEEL, self.OnWheelZoom) self.oglcanvas.Bind(wx.EVT_RIGHT_DOWN, self.OnRightButtonMenu) self.oglcanvas.Bind(wx.EVT_KEY_DOWN, self.onKeyPress) self.oglcanvas.Bind(wx.EVT_CHAR, self.onKeyChar) self.oglcanvas.Bind(wx.EVT_SIZE, self.OnResizeFrame) self.popupmenu = None self.need_abort = False self.new_edge_from = None self.working = False self.snapshot_mgr = GraphSnapshotMgr(graph=self.graph, umlcanvas=self) if UNIT_TESTING_MODE: self.overlap_remover = OverlapRemoval(self.graph, margin=5, gui=self) else: self.overlap_remover = OverlapRemoval(self.graph, margin=50, gui=self)
def testStress2_InitialBoot(self): """ This is the slowest stress test because it runs the spring layout several times. """ from layout.layout_spring import GraphLayoutSpring from layout.coordinate_mapper import CoordinateMapper self.g.LoadGraphFromStrings( GRAPH_INITIALBOOT) # load the scenario ourselves layouter = GraphLayoutSpring(self.g) coordmapper = CoordinateMapper(self.g, (800, 800)) def AllToLayoutCoords(): coordmapper.AllToLayoutCoords() def AllToWorldCoords(): coordmapper.AllToWorldCoords() for i in range(8): print(i, end=" ") AllToLayoutCoords() layouter.layout(keep_current_positions=False) AllToWorldCoords() were_all_overlaps_removed = self.overlap_remover.RemoveOverlaps() self.assertTrue(were_all_overlaps_removed)
def InitSizeAndObjs(self): # Only call this once enclosing frame has been set up, so that get correct world coord dimensions self.canvas_resizer = CanvasResizer(canvas=self) assert not self.canvas_resizer.canvas_too_small( ), "InitSizeAndObjs being called too early - please set up enclosing frame size first" self.umlworkspace = UmlWorkspace() self.layout = LayoutBasic(leftmargin=5, topmargin=5, verticalwhitespace=50, horizontalwhitespace=50, maxclassesperline=7) self.snapshot_mgr = GraphSnapshotMgr(graph=self.umlworkspace.graph, umlcanvas=self) self.coordmapper = CoordinateMapper(self.umlworkspace.graph, self.GetSize()) self.layouter = GraphLayoutSpring(self.umlworkspace.graph, gui=self) self.overlap_remover = OverlapRemoval(self.umlworkspace.graph, margin=50, gui=self)
class UmlCanvas(ogl.ShapeCanvas): def __init__(self, parent, log, frame): ogl.ShapeCanvas.__init__(self, parent) self.observers = multicast() self.app = None # assigned later by app boot self.log = log self.frame = frame self.SetBackgroundColour("LIGHT BLUE") self.SetDiagram(ogl.Diagram()) self.GetDiagram().SetCanvas(self) wx.EVT_WINDOW_DESTROY(self, self.OnDestroy) self.Bind(wx.EVT_MOUSEWHEEL, self.OnWheelZoom) self.Bind(wx.EVT_KEY_DOWN, self.onKeyPress) self.Bind(wx.EVT_CHAR, self.onKeyChar) self.font1 = wx.Font(14, wx.MODERN, wx.NORMAL, wx.NORMAL, False) self.font2 = wx.Font(10, wx.MODERN, wx.NORMAL, wx.NORMAL, False) self.working = False self._kill_layout = False # flag to communicate with layout engine. aborting keypress in gui should set this to true @property def kill_layout(self): return self._kill_layout @kill_layout.setter def kill_layout(self, value): self._kill_layout = value def InitSizeAndObjs(self): # Only call this once enclosing frame has been set up, so that get correct world coord dimensions self.canvas_resizer = CanvasResizer(canvas=self) # Don't assert canvas size sanity anymore as wxpython3 (phoenix) doesn't set canvas size # as quickly as wxpython2.8 does, even though frame has been sized and shown # with frame.SetSize(WINDOW_SIZE) and frame.Show(True) # In wxpython3 (phoenix) canvas stays at (20,20) despite the frame increasing in size to (1024,768) # but good ole wxpython2.8 does indeed change canvas size immediately to (1024,768) # # assert not self.canvas_resizer.canvas_too_small(), "InitSizeAndObjs being called too early - please set up enclosing frame size first" self.umlworkspace = UmlWorkspace() self.layout = LayoutBasic(leftmargin=5, topmargin=5, verticalwhitespace=50, horizontalwhitespace=50, maxclassesperline=7) self.snapshot_mgr = GraphSnapshotMgr(graph=self.umlworkspace.graph, umlcanvas=self) self.coordmapper = CoordinateMapper(self.umlworkspace.graph, self.GetSize()) self.layouter = GraphLayoutSpring(self.umlworkspace.graph, gui=self) self.overlap_remover = OverlapRemoval(self.umlworkspace.graph, margin=50, gui=self) def AllToLayoutCoords(self): self.coordmapper.AllToLayoutCoords() def AllToWorldCoords(self): self.coordmapper.AllToWorldCoords() def onKeyPress(self, event): keycode = event.GetKeyCode( ) # http://www.wxpython.org/docs/api/wx.KeyEvent-class.html if self.working: event.Skip() return self.working = True if keycode == wx.WXK_ESCAPE: print "ESC key detected: Abort Layout" self.kill_layout = True if keycode == wx.WXK_RIGHT: self.app.run.CmdLayoutExpand(remove_overlaps=not event.ShiftDown()) elif keycode == wx.WXK_LEFT: self.app.run.CmdLayoutContract( remove_overlaps=not event.ShiftDown()) self.working = False event.Skip() def onKeyChar(self, event): """ These are secret keycodes not exposed on the menu Normally shortcuts added on the menu work fine. """ if event.GetKeyCode() >= 256: event.Skip() return if self.working: event.Skip() return self.working = True keycode = chr(event.GetKeyCode()) if keycode in ['q', 'Q']: self.NewEdgeMarkFrom() elif keycode in ['w', 'W']: self.NewEdgeMarkTo(edge_type='composition') elif keycode in ['e', 'E']: self.NewEdgeMarkTo(edge_type='generalisation') elif keycode in ['1', '2', '3', '4', '5', '6', '7', '8']: todisplay = ord(keycode) - ord('1') self.snapshot_mgr.Restore(todisplay) elif keycode == 'P': self.Refresh() elif keycode in ['d', 'D']: self.app.run.CmdDumpUmlWorkspace() elif keycode == 's': self.CmdTrimScrollbars() elif keycode == 'G': # and event.ShiftDown() and event.ControlDown(): self.app.run.CmdBuildColourChartWorkspace() elif keycode in ['h', 'H']: self.app.run.CmdColourSequential( color_range_offset=(keycode == 'H')) self.working = False event.Skip() def CmdTrimScrollbars(self): self.canvas_resizer.resize_virtual_canvas_tofit_bounds( shrinkage_leeway=0, bounds_dirty=True) def CmdRememberLayout1(self): self.snapshot_mgr.QuickSave(slot=1) def CmdRememberLayout2(self): self.snapshot_mgr.QuickSave(slot=2) def CmdRestoreLayout1(self): self.snapshot_mgr.QuickRestore(slot=1) def CmdRestoreLayout2(self): self.snapshot_mgr.QuickRestore(slot=2) def SelectNodeNow(self, shape): canvas = shape.GetCanvas() self.app.run.CmdDeselectAllShapes() dc = wx.ClientDC(canvas) canvas.PrepareDC(dc) shape.Select( True, dc ) # could pass None as dc if you don't want to trigger the OnDrawControlPoints(dc) handler immediately - e.g. if you want to do a complete redraw of everything later anyway # change colour when select #shape.SetBrush(wx.WHITE_BRUSH) #wx.Brush("WHEAT", wx.SOLID)) #canvas.Refresh(False) # works #canvas.Redraw(dc) # works too #shape.Draw(dc) # works too, most efficient #canvas.Refresh(False) # t/f or don't use - doesn't seem to make a difference #self.UpdateStatusBar(shape) # only available in the shape evt handler (this method used to live there...) def delete_shape_view(self, shape): # View self.app.run.CmdDeselectAllShapes() for line in shape.GetLines()[:]: line.Delete() shape.Delete() def Clear(self): print "Draw: Clear" self.GetDiagram().DeleteAllShapes() dc = wx.ClientDC(self) self.GetDiagram().Clear( dc ) # Clears screen - don't prepare the dc or it will only clear the top scrolled bit (see my mailing list discussion) self.umlworkspace.Clear() if "wxMac" in wx.PlatformInfo: # Hack on Mac so that onKeyChar bindings take hold properly. wx.CallAfter(self.SetFocus) elif 'wxGTK' in wx.PlatformInfo: # Hack on Linux so that onKeyChar bindings take hold properly. wx.CallLater(1500, self.app.context.wxapp.multiText.SetFocus) wx.CallLater(1500, self.SetFocus) def NewEdgeMarkFrom(self): selected = [ s for s in self.GetDiagram().GetShapeList() if s.Selected() ] if not selected: print "Please select a node" return self.new_edge_from = selected[0].node print "From", self.new_edge_from.id def NewEdgeMarkTo(self, edge_type='composition'): selected = [ s for s in self.GetDiagram().GetShapeList() if s.Selected() ] if not selected: print "Please select a node" return tonode = selected[0].node print "To", tonode.id if self.new_edge_from == None: print "Please set from node first" return if self.new_edge_from.id == tonode.id: print "Can't link to self" return if not self.umlworkspace.graph.FindNodeById(self.new_edge_from.id): print "From node %s doesn't seem to be in graph anymore!" % self.new_edge_from.id return edge = self.umlworkspace.graph.AddEdge( tonode, self.new_edge_from, weight=None) # swap direction as is a directional composition. # TODO should also arguably add to umlworkspace's associations_composition or associations_generalisation list (or create a new one for unlabelled associations like the one we are creating here) #edge['uml_edge_type'] = '' edge['uml_edge_type'] = edge_type self.CreateUmlEdge(edge) self.stateofthenation() def CreateImageShape(self, F): #shape = ogl.BitmapShape() shape = BitmapShapeResizable() img = wx.Image(F, wx.BITMAP_TYPE_ANY) #adjusted_img = img.AdjustChannels(factor_red = 1., factor_green = 1., factor_blue = 1., factor_alpha = 0.5) #adjusted_img = img.Rescale(10,10) adjusted_img = img bmp = wx.BitmapFromImage(adjusted_img) shape.SetBitmap(bmp) self.GetDiagram().AddShape(shape) shape.Show(True) evthandler = UmlShapeHandler( self.log, self.frame, self ) # just init the handler with whatever will be convenient for it to know. evthandler.SetShape(shape) evthandler.SetPreviousHandler(shape.GetEventHandler()) shape.SetEventHandler(evthandler) self.new_evthandler_housekeeping(evthandler) setpos(shape, 0, 0) #setpos(shape, node.left, node.top) #node.width, node.height = shape.GetBoundingBoxMax() #node.shape = shape #shape.node = node return shape def CreateUmlShape(self, node): def newRegion(font, name, textLst, maxWidth, totHeight=10): # Taken from Boa, but put into the canvas class instead of the scrolled window class. region = ogl.ShapeRegion() if len(textLst) == 0: return region, maxWidth, 0 dc = wx.ClientDC(self) # self is the canvas dc.SetFont(font) for text in textLst: w, h = dc.GetTextExtent(text) if w > maxWidth: maxWidth = w totHeight = totHeight + h + 0 # interline padding region.SetFont(font) region.SetText('\n'.join(textLst)) region.SetName(name) return region, maxWidth, totHeight shape = DividedShape(width=99, height=98, canvas=self) maxWidth = 10 # min node width, grows as we update it with text widths """ Future: Perhaps be able to show regions or not. Might need to totally reconstruct the shape. """ #if not self.showAttributes: classAttrs = [' '] #if not self.showMethods: classMeths = [' '] # Create each region. If height returned is 0 this means don't create this region. regionName, maxWidth, nameHeight = newRegion(self.font1, 'class_name', [node.classname], maxWidth) regionAttribs, maxWidth, attribsHeight = newRegion( self.font2, 'attributes', node.attrs, maxWidth) regionMeths, maxWidth, methsHeight = newRegion(self.font2, 'methods', node.meths, maxWidth) # Work out total height of shape totHeight = nameHeight + attribsHeight + methsHeight # Set regions to be a proportion of the total height of shape regionName.SetProportions(0.0, 1.0 * (nameHeight / float(totHeight))) regionAttribs.SetProportions(0.0, 1.0 * (attribsHeight / float(totHeight))) regionMeths.SetProportions(0.0, 1.0 * (methsHeight / float(totHeight))) # Add regions to the shape shape.AddRegion(regionName) if attribsHeight: # Dont' make a region unless we have to shape.AddRegion(regionAttribs) if methsHeight: # Dont' make a region unless we have to shape.AddRegion(regionMeths) shape.SetSize(maxWidth + 10, totHeight + 10) shape.SetCentreResize( False ) # Specify whether the shape is to be resized from the centre (the centre stands still) or from the corner or side being dragged (the other corner or side stands still). regionName.SetFormatMode(ogl.FORMAT_CENTRE_HORIZ) shape.region1 = regionName # Andy added, for later external reference to classname from just having the shape instance. shape.SetDraggable(True, True) shape.SetCanvas(self) shape.SetPen( wx.BLACK_PEN) # Controls the color of the border of the shape shape.SetBrush(wx.Brush("WHEAT", wx.SOLID)) setpos(shape, node.left, node.top) self.GetDiagram().AddShape( shape ) # self.AddShape is ok too, ShapeCanvas's AddShape is delegated back to Diagram's AddShape. ShapeCanvas-->Diagram shape.Show(True) evthandler = UmlShapeHandler( self.log, self.frame, self ) # just init the handler with whatever will be convenient for it to know. evthandler.SetShape(shape) evthandler.SetPreviousHandler(shape.GetEventHandler()) shape.SetEventHandler(evthandler) self.new_evthandler_housekeeping(evthandler) shape.FlushText() # Don't set the node left,top here as the shape needs to conform to the node. # On the other hand the node needs to conform to the shape's width,height. # ACTUALLY I now do set the pos of the shape, see above, # just before the AddShape() call. # node.width, node.height = shape.GetBoundingBoxMax( ) # TODO: Shouldn't this be in node coords not world coords? node.shape = shape shape.node = node return shape def createNodeShape(self, node): # FROM SPRING LAYOUT shape = ogl.RectangleShape(node.width, node.height) shape.AddText(node.id) setpos(shape, node.left, node.top) #shape.SetDraggable(True, True) self.AddShape(shape) node.shape = shape shape.node = node # wire in the event handler for the new shape evthandler = UmlShapeHandler( None, self.frame, self ) # just init the handler with whatever will be convenient for it to know. evthandler.SetShape(shape) evthandler.SetPreviousHandler(shape.GetEventHandler()) shape.SetEventHandler(evthandler) self.new_evthandler_housekeeping(evthandler) def createCommentShape(self, node): shape = ogl.TextShape(node.width, node.height) shape.SetCanvas(self) shape.SetPen(wx.BLACK_PEN) shape.SetBrush(wx.LIGHT_GREY_BRUSH) shape.SetBrush(wx.RED_BRUSH) for line in node.comment.split('\n'): shape.AddText(line) setpos(shape, node.left, node.top) #shape.SetDraggable(True, True) self.AddShape(shape) node.shape = shape shape.node = node # wire in the event handler for the new shape evthandler = UmlShapeHandler( None, self.frame, self ) # just init the handler with whatever will be convenient for it to know. evthandler.SetShape(shape) evthandler.SetPreviousHandler(shape.GetEventHandler()) shape.SetEventHandler(evthandler) self.new_evthandler_housekeeping(evthandler) def new_evthandler_housekeeping(self, evthandler): # notify app of this new evthandler so app can # assign the evthandler's .app attribute. # Or could have just done: # evthandler.app = self.app # here. But we may need observer for other things later. self.observers.NOTIFY_EVT_HANDLER_CREATED(evthandler) def CreateUmlEdge(self, edge): """ @startuml class ArrowHead { _arrowType __init__( type, end, size, xOffset, name, mf, arrowId) } class Shape { SetCanvas() SetPen() SetBrush() } class LineShape <<lib.ogl._lines.py>> { _from _to _arcArrows AddArrow(arrowtype) DrawArrow(self, dc, arrow, XOffset, proportionalOffset) MakeLineControlPoints(2) } class LineShapeCustom <<src.gui.uml_lines.py>> { DrawArrow(self, dc, arrow, XOffset, proportionalOffset) } Shape <|-- LineShape LineShape <|-- LineShapeCustom LineShape --> "0..*" ArrowHead : _arcArrows note as N1 custom line drawing introduced ARROW_UML_GENERALISATION ARROW_UML_COMPOSITION end note N1 .. LineShapeCustom @enduml """ from gui.uml_lines import LineShapeCustom, ARROW_UML_GENERALISATION, ARROW_UML_COMPOSITION fromShape = edge['source'].shape toShape = edge['target'].shape edge_label = edge.get('uml_edge_type', '') if edge_label == 'generalisation': arrowtype = ARROW_UML_GENERALISATION # used to be ogl.ARROW_ARROW elif edge_label == 'composition': arrowtype = ARROW_UML_COMPOSITION # used to be ogl.ARROW_FILLED_CIRCLE else: arrowtype = None line = LineShapeCustom() # used to be ogl.LineShape() line.SetCanvas(self) line.SetPen(wx.BLACK_PEN) line.SetBrush(wx.BLACK_BRUSH) if arrowtype: line.AddArrow(arrowtype) line.MakeLineControlPoints(2) fromShape.AddLine(line, toShape) self.GetDiagram().AddShape(line) line.Show(True) def OnWheelZoom(self, event): #print "OnWheelZoom" if self.working: return self.working = True SCROLL_AMOUNT = 40 if not event.ControlDown(): oldscrollx = self.GetScrollPos(wx.HORIZONTAL) oldscrolly = self.GetScrollPos(wx.VERTICAL) if event.GetWheelRotation() < 0: self.Scroll(oldscrollx, oldscrolly + SCROLL_AMOUNT) else: self.Scroll(oldscrollx, max(0, oldscrolly - SCROLL_AMOUNT)) else: if event.GetWheelRotation() < 0: self.app.run.CmdLayoutContract( remove_overlaps=not event.ShiftDown()) else: self.app.run.CmdLayoutExpand( remove_overlaps=not event.ShiftDown()) self.working = False # UTILITY - called by CmdFileImportSource, CmdFileLoadWorkspaceBase.LoadGraph def build_view(self, translatecoords=True): if translatecoords: self.AllToWorldCoords() # Clear existing visualisation for node in self.umlworkspace.graph.nodes: if node.shape: self.delete_shape_view(node.shape) node.shape = None # Create fresh visualisation for node in self.umlworkspace.graph.nodes: assert not node.shape shape = self.CreateUmlShape(node) self.umlworkspace.classnametoshape[ node. id] = shape # Record the name to shape map so that we can wire up the links later. for edge in self.umlworkspace.graph.edges: self.CreateUmlEdge(edge) # UTILITY - called by # #CmdInsertNewNodeClass, CmdInsertImage, CmdLayoutExpandContractBase, #umlwin.OnWheelZoom_OverlapRemoval_Defunct, #umlwin.layout_and_position_shapes, #UmlShapeHandler.OnEndDragLeft #UmlShapeHandler.OnSizingEndDragLeft #LayoutBlackboard.LayoutLoopTillNoChange # def remove_overlaps(self, watch_removals=True): """ Returns T/F if any overlaps found, so caller can decide whether to redraw the screen """ self.overlap_remover.RemoveOverlaps(watch_removals=watch_removals) return self.overlap_remover.GetStats()['total_overlaps_found'] > 0 # UTILITY - called by everyone!!?? # #CmdFileLoadWorkspaceBase, CmdInsertComment, CmdEditClass #CmdLayoutExpandContractBase, #CmdInsertNewNodeClass #umlwin.NewEdgeMarkTo #umlwin.OnWheelZoom_OverlapRemoval_Defunct #LayoutBlackboard.LayoutThenPickBestScale #LayoutBlackboard.Experiment1 #LayoutBlackboard.LayoutLoopTillNoChange #LayoutBlackboard.ScaleUpMadly #LayoutBlackboard.GetVitalStats (only if animate is true) #OverlapRemoval.RemoveOverlaps ( refresh gui if self.gui and watch_removals) #GraphSnapshotMgr.RestoreGraph # # these do an overlap removal first before calling here # #CmdInsertImage #umlwin.layout_and_position_shapes, #UmlShapeHandler.OnEndDragLeft #UmlShapeHandler.OnSizingEndDragLeft # # recalibrate = True - called by core spring layout self.gui.stateofthenation() # # RENAME?: dc_DiagramClearAndRedraw # def stateofthenation(self, recalibrate=False, auto_resize_canvas=True): if recalibrate: # was stateofthespring self.coordmapper.Recalibrate() self.AllToWorldCoords() dc = wx.ClientDC(self) self.PrepareDC(dc) for node in self.umlworkspace.graph.nodes: node.shape.Move2(dc, node.left, node.top, display=False) self.Refresh() self.Update( ) # or wx.SafeYield() # Without this the nodes don't paint during a "L" layout (edges do!?) # You need to be yielding or updating on a regular basis, so that when your OS/window manager sends repaint messages to your app, it can handle them. See http://stackoverflow.com/questions/10825128/wxpython-how-to-force-ui-refresh if auto_resize_canvas: self.canvas_resizer.resize_virtual_canvas_tofit_bounds( bounds_dirty=True) # UTILITY - used by CmdLayout and CmdFileImportBase def layout_and_position_shapes(self): self.canvas_resizer.frame_calibration( auto_resize_virtualcanvas=False ) # going to do a stateofthenation later so no point changing virt canvas now self.AllToLayoutCoords() self.layouter.layout(keep_current_positions=False, optimise=True) self.AllToWorldCoords() if self.remove_overlaps(): self.stateofthenation() def get_umlboxshapes(self): #return [s for s in self.GetDiagram().GetShapeList() if not isinstance(s, ogl.LineShape)] return [ s for s in self.GetDiagram().GetShapeList() if isinstance(s, DividedShape) ] # TODO take into account images and other shapes umlboxshapes = property(get_umlboxshapes) def OnDestroy(self, evt): for shape in self.GetDiagram().GetShapeList(): if shape.GetParent() == None: shape.SetCanvas(None) def OnLeftClick(self, x, y, keys): # Override of ShapeCanvas method # keys is a bit list of the following: KEY_SHIFT KEY_CTRL self.app.run.CmdDeselectAllShapes()
class GraphRendererOgl: def __init__(self, graph, oglcanvas): self.graph = graph self.oglcanvas = oglcanvas self.oglcanvas.graphrendererogl = self self.coordmapper = CoordinateMapper(self.graph, self.oglcanvas.GetSize()) self.oglcanvas.Bind(wx.EVT_MOUSEWHEEL, self.OnWheelZoom) self.oglcanvas.Bind(wx.EVT_RIGHT_DOWN, self.OnRightButtonMenu) self.oglcanvas.Bind(wx.EVT_KEY_DOWN, self.onKeyPress) self.oglcanvas.Bind(wx.EVT_CHAR, self.onKeyChar) self.oglcanvas.Bind(wx.EVT_SIZE, self.OnResizeFrame) self.popupmenu = None self.need_abort = False self.new_edge_from = None self.working = False self.snapshot_mgr = GraphSnapshotMgr(graph=self.graph, umlcanvas=self) if UNIT_TESTING_MODE: self.overlap_remover = OverlapRemoval(self.graph, margin=5, gui=self) else: self.overlap_remover = OverlapRemoval(self.graph, margin=50, gui=self) def AllToLayoutCoords(self): self.coordmapper.AllToLayoutCoords() def AllToWorldCoords(self): self.coordmapper.AllToWorldCoords() def OnResizeFrame (self, event): # ANDY interesting - GetVirtualSize grows when resize frame frame = self.oglcanvas.GetTopLevelParent() print "frame resize", frame.GetClientSize() self.coordmapper.Recalibrate(frame.GetClientSize()) # may need to call self.CalcVirtSize() if scrolled window def DeselectAllShapes(self): selected = [s for s in self.oglcanvas.GetDiagram().GetShapeList() if s.Selected()] if selected: s = selected[0] canvas = s.GetCanvas() dc = wx.ClientDC(canvas) canvas.PrepareDC(dc) s.Select(False, dc) canvas.Refresh(False) # Need this or else Control points ('handles') leave blank holes def InsertNewNode(self): id = 'D' + str(random.randint(1,99)) dialog = wx.TextEntryDialog ( None, 'Enter an id string:', 'Create a new node', id ) if dialog.ShowModal() == wx.ID_OK: id = dialog.GetValue() if self.graph.FindNodeById(id): id += str(random.randint(1,9999)) node = GraphNode(id, random.randint(0, 100),random.randint(0,100),random.randint(60, 160),random.randint(60,160)) node = self.graph.AddNode(node) self.createNodeShape(node) node.shape.Show(True) self.stateofthenation() dialog.Destroy() def DeleteSelectedNode(self): selected = [s for s in self.oglcanvas.GetDiagram().GetShapeList() if s.Selected()] if selected: shape = selected[0] print 'delete', shape.node.id # model self.graph.DeleteNodeById(shape.node.id) # view self.DeselectAllShapes() for line in shape.GetLines()[:]: line.Delete() shape.Delete() def NewEdgeMarkFrom(self): selected = [s for s in self.oglcanvas.GetDiagram().GetShapeList() if s.Selected()] if not selected: print "Please select a node" return self.new_edge_from = selected[0].node print "From", self.new_edge_from.id def NewEdgeMarkTo(self): selected = [s for s in self.oglcanvas.GetDiagram().GetShapeList() if s.Selected()] if not selected: print "Please select a node" return tonode = selected[0].node print "To", tonode.id if self.new_edge_from == None: print "Please set from node first" return if self.new_edge_from.id == tonode.id: print "Can't link to self" return if not self.graph.FindNodeById(self.new_edge_from.id): print "From node %s doesn't seem to be in graph anymore!" % self.new_edge_from.id return edge = self.graph.AddEdge(self.new_edge_from, tonode, weight=None) self.createEdgeShape(edge) self.stateofthenation() def OnWheelZoom(self, event): if self.working: return self.working = True if event.GetWheelRotation() < 0: self.stage2() print self.overlap_remover.GetStats() else: self.stateofthenation() self.working = False def onKeyPress(self, event): keycode = event.GetKeyCode() # http://www.wxpython.org/docs/api/wx.KeyEvent-class.html if self.working: event.Skip() return self.working = True if keycode == wx.WXK_DOWN: optimise = not event.ShiftDown() self.ReLayout(keep_current_positions=True, gui=self, optimise=optimise) elif keycode == wx.WXK_UP: optimise = not event.ShiftDown() self.ReLayout(keep_current_positions=False, gui=self, optimise=optimise) elif keycode == wx.WXK_RIGHT: if self.coordmapper.scale > 0.8: self.ChangeScale(-0.2, remap_world_to_layout=event.ShiftDown(), removeoverlaps=not event.ControlDown()) print "expansion ", self.coordmapper.scale else: print "Max expansion prevented.", self.coordmapper.scale print "LL/raw %d/%d" % (len(self.graph.CountLineOverLineIntersections(ignore_nodes=False)), \ len(self.graph.CountLineOverLineIntersections(ignore_nodes=True))) elif keycode == wx.WXK_LEFT: if self.coordmapper.scale < 3: self.ChangeScale(0.2, remap_world_to_layout=event.ShiftDown(), removeoverlaps=not event.ControlDown()) print "contraction ", self.coordmapper.scale else: print "Min expansion thwarted.", self.coordmapper.scale print "LL/raw %d/%d" % (len(self.graph.CountLineOverLineIntersections(ignore_nodes=False)), \ len(self.graph.CountLineOverLineIntersections(ignore_nodes=True))) elif keycode == wx.WXK_DELETE: self.DeleteSelectedNode() elif keycode == wx.WXK_INSERT: self.InsertNewNode() self.working = False event.Skip() def onKeyChar(self, event): if event.GetKeyCode() >= 256: event.Skip() return if self.working: event.Skip() return self.working = True keycode = chr(event.GetKeyCode()) if keycode == 'q': self.NewEdgeMarkFrom() elif keycode == 'w': self.NewEdgeMarkTo() elif keycode == '(': self.snapshot_mgr.QuickSave(slot=1) elif keycode == ')': self.snapshot_mgr.QuickSave(slot=2) elif keycode == '9': self.snapshot_mgr.QuickRestore(slot=1) elif keycode == '0': self.snapshot_mgr.QuickRestore(slot=2) elif keycode in ['1','2','3','4','5','6','7','8']: todisplay = ord(keycode) - ord('1') self.snapshot_mgr.Restore(todisplay) elif keycode in ['x', 'X', 'z', 'Z', 'c', 'C']: if keycode in ['Z','z']: strategy = ":reduce pre overlap removal NN overlaps" elif keycode in ['X','x']: strategy = ":reduce post overlap removal LN crossings" elif keycode in ['C','c']: strategy = ":reduce post overlap removal LN and LL crossings" b = LayoutBlackboard(graph=self.graph, umlwin=self) b.LayoutThenPickBestScale(scramble=keycode in ['Z','X','C'], strategy=strategy) elif keycode in ['e',]: b = LayoutBlackboard(graph=self.graph, umlwin=self) b.Experiment1() elif keycode in ['r', 'R']: b = LayoutBlackboard(graph=self.graph, umlwin=self) b.LayoutLoopTillNoChange(scramble=keycode == 'R') elif keycode in ['b', 'B']: b = LayoutBlackboard(graph=self.graph, umlwin=self) b.LayoutMultipleChooseBest(4) elif keycode in ['?',]: self.DumpStatus() self.working = False event.Skip() def DumpStatus(self): #print "-"*50 print "scale", self.coordmapper.scale print "line-line intersections", len(self.graph.CountLineOverLineIntersections()) print "node-node overlaps", self.overlap_remover.CountOverlaps() print "line-node crossings", self.graph.CountLineOverNodeCrossings()['ALL']/2 #, self.graph.CountLineOverNodeCrossings() print "bounds", self.graph.GetBounds() def draw(self, translatecoords=True): self.stage1(translatecoords=translatecoords) #thread.start_new_thread(self.DoSomeLongTask, ()) def stage1(self, translatecoords=True): import time if translatecoords: self.AllToWorldCoords() for node in self.graph.nodes: self.createNodeShape(node) for edge in self.graph.edges: self.createEdgeShape(edge) self.Redraw() def stage2(self, force_stateofthenation=False, watch_removals=True): ANIMATION = False if ANIMATION: self.graph.SaveOldPositionsForAnimationPurposes() watch_removals = False # added this when I turned animation on. self.overlap_remover.RemoveOverlaps(watch_removals=watch_removals) if self.overlap_remover.GetStats()['total_overlaps_found'] > 0 or force_stateofthenation: self.stateofthenation(animate=ANIMATION) def stateofthenation(self, animate=False, recalibrate=False): if recalibrate: # was stateofthespring self.coordmapper.Recalibrate() self.AllToWorldCoords() if animate: from animation import GeneratePoints for node in self.graph.nodes: node.anilist = GeneratePoints((node.previous_left, node.previous_top), (node.left, node.top), steps=5) dc = wx.ClientDC(self.oglcanvas) self.oglcanvas.PrepareDC(dc) for i in range(len(self.graph.nodes[0].anilist)): for node in self.graph.nodes: point = node.anilist.pop(0) x, y = point #node.shape.Move(dc, x, y, True) # don't do this or it will flicker setpos(node.shape, x, y) node.shape.MoveLinks(dc) self.Redraw(clear=False) self.Redraw() else: for node in self.graph.nodes: self.AdjustShapePosition(node) self.Redraw() wx.SafeYield() #def stateofthespring(self): # self.coordmapper.Recalibrate() # self.AllToWorldCoords() # self.stateofthenation() # DON'T do overlap removal or it will get mad! def ChangeScale(self, delta, remap_world_to_layout=False, removeoverlaps=True): if remap_world_to_layout: self.AllToLayoutCoords() # Experimental - only needed when you've done world coord changes self.coordmapper.Recalibrate(scale=self.coordmapper.scale+delta) self.AllToWorldCoords() numoverlaps = self.overlap_remover.CountOverlaps() if removeoverlaps: self.stage2(force_stateofthenation=True, watch_removals=False) # does overlap removal and stateofthenation else: self.stateofthenation() def ReLayout(self, keep_current_positions=False, gui=None, optimise=True): self.AllToLayoutCoords() layouter = GraphLayoutSpring(self.graph, gui) # should keep this around layouter.layout(keep_current_positions, optimise=optimise) self.AllToWorldCoords() self.stage2() # does overlap removal and stateofthenation def AdjustShapePosition(self, node, point=None): assert node.shape if point: x, y = point else: x, y = node.left, node.top # Don't need to use node.shape.Move(dc, x, y, False) setpos(node.shape, x, y) # But you DO need to use a dc to adjust the links dc = wx.ClientDC(self.oglcanvas) self.oglcanvas.PrepareDC(dc) node.shape.MoveLinks(dc) def Redraw(self, clear=True): diagram = self.oglcanvas.GetDiagram() canvas = self.oglcanvas assert canvas == diagram.GetCanvas() dc = wx.ClientDC(canvas) canvas.PrepareDC(dc) #for node in self.graph.nodes: # TODO am still moving nodes in the pynsourcegui version? # shape = node.shape # shape.Move(dc, shape.GetX(), shape.GetY()) if clear: diagram.Clear(dc) diagram.Redraw(dc) def createNodeShape(self, node): shape = ogl.RectangleShape( node.width, node.height ) shape.AddText(node.id) setpos(shape, node.left, node.top) #shape.SetDraggable(True, True) self.oglcanvas.AddShape( shape ) node.shape = shape shape.node = node # wire in the event handler for the new shape evthandler = MyEvtHandler(None, self.oglcanvas) evthandler.SetShape(shape) evthandler.SetPreviousHandler(shape.GetEventHandler()) shape.SetEventHandler(evthandler) def createEdgeShape(self, edge): line = ogl.LineShape() line.SetCanvas(self.oglcanvas) line.SetPen(wx.BLACK_PEN) line.SetBrush(wx.BLACK_BRUSH) line.MakeLineControlPoints(2) fromShape = edge['source'].shape toShape = edge['target'].shape fromShape.AddLine(line, toShape) self.oglcanvas.GetDiagram().AddShape(line) line.Show(True) def OnRightButtonMenu(self, event): # Menu x, y = event.GetPosition() frame = self.oglcanvas.GetTopLevelParent() if self.popupmenu: self.popupmenu.Destroy() # wx.Menu objects need to be explicitly destroyed (e.g. menu.Destroy()) in this situation. Otherwise, they will rack up the USER Objects count on Windows; eventually crashing a program when USER Objects is maxed out. -- U. Artie Eoff http://wiki.wxpython.org/index.cgi/PopupMenuOnRightClick self.popupmenu = wx.Menu() # Create a menu item = self.popupmenu.Append(2015, "Load Graph from text...") frame.Bind(wx.EVT_MENU, self.OnLoadGraphFromText, item) item = self.popupmenu.Append(2017, "Dump Graph to console") frame.Bind(wx.EVT_MENU, self.OnSaveGraphToConsole, item) self.popupmenu.AppendSeparator() item = self.popupmenu.Append(2011, "Load Graph...") frame.Bind(wx.EVT_MENU, self.OnLoadGraph, item) item = self.popupmenu.Append(2012, "Save Graph...") frame.Bind(wx.EVT_MENU, self.OnSaveGraph, item) self.popupmenu.AppendSeparator() imp = wx.Menu() item = imp.Append(2021, "Load Test Graph 1"); frame.Bind(wx.EVT_MENU, self.OnLoadTestGraph1, item) item = imp.Append(2022, "Load Test Graph 2"); frame.Bind(wx.EVT_MENU, self.OnLoadTestGraph2, item) item = imp.Append(2023, "Load Test Graph 3"); frame.Bind(wx.EVT_MENU, self.OnLoadTestGraph3, item) item = imp.Append(2043, "Load Test Graph 3a"); frame.Bind(wx.EVT_MENU, self.OnLoadTestGraph3a, item) item = imp.Append(2024, "Load Test Graph 4"); frame.Bind(wx.EVT_MENU, self.OnLoadTestGraph4, item) item = imp.Append(2025, "Load Test Graph 6 (line overlaps)"); frame.Bind(wx.EVT_MENU, self.OnLoadTestGraph6, item) item = imp.Append(2026, "Load Test Graph 7"); frame.Bind(wx.EVT_MENU, self.OnLoadTestGraph7, item) item = imp.Append(2046, "Load Test Graph 8 (up snug)"); frame.Bind(wx.EVT_MENU, self.OnLoadTestGraph8, item) self.popupmenu.AppendMenu(-1, 'Unit Test Graphs', imp) imp = wx.Menu() item = imp.Append(2031, "Spring 2"); frame.Bind(wx.EVT_MENU, self.OnLoadSpring2, item) item = imp.Append(2032, "Spring 3"); frame.Bind(wx.EVT_MENU, self.OnLoadSpring3, item) imp.AppendSeparator() item = imp.Append(2033, "Initial Boot"); frame.Bind(wx.EVT_MENU, self.OnLoadInitialBoot, item) self.popupmenu.AppendMenu(-1, 'Other Test Graphs', imp) self.popupmenu.AppendSeparator() item = self.popupmenu.Append(2014, "Clear") frame.Bind(wx.EVT_MENU, self.OnClear, item) # must pass item item = self.popupmenu.Append(2013, "Cancel") #frame.Bind(wx.EVT_MENU, self.OnPopupItemSelected, item) frame.PopupMenu(self.popupmenu, wx.Point(x,y)) def OnLoadTestGraph1(self, event): self.LoadGraph(TEST_GRAPH1) def OnLoadTestGraph2(self, event): self.LoadGraph(TEST_GRAPH2) def OnLoadTestGraph3(self, event): self.LoadGraph(TEST_GRAPH3) def OnLoadTestGraph3a(self, event): self.LoadGraph(TEST_GRAPH3A) def OnLoadTestGraph4(self, event): self.LoadGraph(TEST_GRAPH4) def OnLoadTestGraph6(self, event): self.LoadGraph(TEST_GRAPH6) def OnLoadTestGraph7(self, event): self.LoadGraph(TEST_GRAPH7) def OnLoadTestGraph8(self, event): self.LoadGraph(TEST_GRAPH8) def OnLoadSpring2(self, event): self.LoadGraph(GRAPH_SPRING2) def OnLoadSpring3(self, event): self.LoadGraph(GRAPH_SPRING3) def OnLoadInitialBoot(self, event): self.LoadGraph(GRAPH_INITIALBOOT) def OnSaveGraphToConsole(self, event): print self.graph.GraphToString() def OnSaveGraph(self, event): frame = self.oglcanvas.GetTopLevelParent() dlg = wx.FileDialog(parent=frame, message="choose", defaultDir='.', defaultFile="", wildcard="*.txt", style=wx.FD_SAVE, pos=wx.DefaultPosition) if dlg.ShowModal() == wx.ID_OK: filename = dlg.GetPath() fp = open(filename, "w") fp.write(self.graph.GraphToString()) fp.close() dlg.Destroy() def OnLoadGraphFromText(self, event): eg = "{'type':'node', 'id':'A', 'x':142, 'y':129, 'width':250, 'height':250}" dialog = wx.TextEntryDialog ( None, 'Enter an node/edge persistence strings:', 'Create a new node', eg, style=wx.OK|wx.CANCEL|wx.TE_MULTILINE ) if dialog.ShowModal() == wx.ID_OK: txt = dialog.GetValue() self.LoadGraph(txt) def OnLoadGraph(self, event): frame = self.oglcanvas.GetTopLevelParent() dlg = wx.FileDialog(parent=frame, message="choose", defaultDir='.', defaultFile="", wildcard="*.txt", style=wx.OPEN, pos=wx.DefaultPosition) if dlg.ShowModal() == wx.ID_OK: filename = dlg.GetPath() fp = open(filename, "r") s = fp.read() fp.close() self.LoadGraph(s) dlg.Destroy() def LoadGraph(self, filedata=""): self.Clear() self.graph.LoadGraphFromStrings(filedata) # build view from model self.draw(translatecoords=False) # set layout coords to be in sync with world, so that if expand scale things will work self.coordmapper.Recalibrate() self.AllToLayoutCoords() # refresh view self.oglcanvas.GetDiagram().ShowAll(1) # need this, yes self.stateofthenation() def OnClear(self, event): self.Clear() def Clear(self): # Clear view self.oglcanvas.GetDiagram().DeleteAllShapes() dc = wx.ClientDC(self.oglcanvas) self.oglcanvas.GetDiagram().Clear(dc) # only ends up calling dc.Clear() - I wonder if this clears the screen? # clear model self.graph.Clear() def DoSomeLongTask(self): for i in range(1,50): if self.need_abort: print "aborted." return wx.CallAfter(self.stage2) #print '*', #wx.CallAfter(self.DoStuff) time.sleep(2) # lets events through to the main wx thread and paints/messages get through ok print "Done." """