class Editor: # Canvas size is used by the snap grid to determine how far to draw the grid # Actually, should be called the canvas bounding box :p # BTW: Using negative values, or values that are too small, doesn't work well :p CANVAS_SIZE_TUPLE = [0, 0, 2000, 2000] def __init__(self, semObject, className, modelPath=None): # Use the AToM3 Tkinter root window self.className = className root = self.rootInitilization(semObject, modelPath) if( not root ): return self.root = root self.mainHandler = MainHandler.MainHandler(self) root.bind("<Key>", self.mainHandler.onKey) root.bind("<Shift-Key>", self.mainHandler.onShiftKey) root.bind("<Control-Key>", self.mainHandler.onControlKey) zoom = 1.0 self.menuBar = Tools.MenuBar(root, self, self.mainHandler) #goes to top by itself self.statusBar = Tools.StatusBar(root, "", zoom, 0, 0) #goes to bottom by itself self.colorSelector = Colors.ColorSelector(root, self.mainHandler) #goes to bottom by itself self.toolFrame = Tkinter.Frame(root, relief=Tkinter.RAISED, bd=1) self.toolSelector = Tools.ToolSelector(self.toolFrame, self.mainHandler, Tkinter.TOP) self.outlineFillOptionSelector = Tools.OutlineFillOptionSelector(self.toolFrame, self.mainHandler, Tkinter.TOP) self.lineWidthSelector = Tools.LineWidthSelector(self.toolFrame, self.mainHandler, Tkinter.TOP) self.toolFrame.pack(side=Tkinter.LEFT) self.workspace = Workspace(self, self.CANVAS_SIZE_TUPLE[2], self.CANVAS_SIZE_TUPLE[3], self.CANVAS_SIZE_TUPLE[2], self.CANVAS_SIZE_TUPLE[3], self.mainHandler, zoom) #goes to the right by itself self.workspace.setZoom(zoom, 0, 0) self.canvas = self.workspace.getCanvas() self.scripting = Scripting() ## self.GFs = self.open() self.GFs = [] self.extendedInitilization( semObject ) self.clipboardList = [] self.undoStack = [] self.mainHandler.start() self.compositionVisitor = GFVisitors.CompositionVisitor(self) self.colorVisitor = GFVisitors.ColorVisitor() self.widthVisitor = GFVisitors.WidthVisitor() self.optionVisitor = GFVisitors.OptionVisitor(self) # Carefully try to load the GF model (may fail for random reasons) try: self.GFs = self.open() except: raise def rootInitilization(self, semObject, modelPathAndFile): """ Extension to the old initilization routine This directly sets up the editor for generating icons for AToM3 """ # Do we have everything we need? Check the minimum requirements: try: atom3i = semObject.parent TkRoot = atom3i.parent statusbar = atom3i.statusbar except: print "ERROR: Unable to start editor, lacking information (atom3i,TkRoot, or statusbar)" return None self.atom3i = atom3i # Do we have the fileName we'll be using to save the graphicalAppearence? if(not modelPathAndFile): modelPathAndFile = statusbar.getState(statusbar.MODEL)[1][0] self.modelPath = os.path.split(modelPathAndFile)[0] if( self.modelPath == '' ): tkMessageBox.showerror( "Icon-Editor model path error", "Please save your ER model before modifying graphical appearences.", parent=TkRoot) return None # Setup a new toplevel window for the editor root = Tkinter.Toplevel( TkRoot ) root.title("Icon Editor - AToM3") root.geometry("%dx%d%+d%+d" % (800, 600, 100, 0)) root.transient( TkRoot ) root.grab_set() return root def extendedInitilization( self, semObject ): # SnapGrid: since this is a singleton, must first disable old stuff self.snapGridInfoTuple = None self.snapGridInfoBackup = None SnapGrid.applyLayout( self, disableForPrinting = True ) SnapGrid.applyLayout( self ) # Useful binds self.root.bind("<Alt-x>", lambda event: self.mainHandler.onExit() ) self.root.bind("<F1>", lambda event: self.mainHandler.onSnapSetting() ) self.root.bind("<Control-e>", lambda event: self.mainHandler.onExport() ) self.root.protocol("WM_DELETE_WINDOW", self.mainHandler.onExit ) # AToM3 graphical appearence exporter self.exportVisitor = SaveGFVisitor.ATOM3_Export_Visitor(semObject, self) # Extra info: includes text attributes that change dynamically & named ports self.attributes = semObject.attributesToDraw() # Icon Positioning System (not quite GPS) self.iconPositioner = IconPositioner( self ) self.root.bind("<F2>", lambda event: self.iconPositioner.createDialog() ) def iconPlacer(self, anchor = 'nw' ): """ Places the graphical icon at a position determined by 'anchor' If anchor is 'nw', then the icon will be moved so that when the user creates an item with this icon, the top-left edge of the icon appears at the position the user clicked. If anchor is 'origin', then the icon appears exactly centered on origin. If anchor is of type 'float', then use the float value directly. """ x0,y0,x1,y1 = self.getBoundingBox( self.getGFs() ) # Top-Left positioning if( anchor == 'nw' ): dx = -x0 dy = -y0 # Center positioning elif( anchor == 'origin' ): dx = -x0 - ( x1 - x0 ) / 2 dy = -y0 - ( y1 - y0 ) / 2 # Manual offset elif( type( anchor ) == type( float() ) ): dx = dy = anchor # Invalid use else: raise Exception, "Wake up and smell the API you lopsided kitten burger! j/k" for gf in self.getGFs(): gf.translate( dx,dy ) self.addUndoTranslate( self.getGFs(), dx, dy ) def getAttributes( self ): return self.attributes def exit( self, event=None ): """ Exit point for the Icon-Editor """ # Clean up the snap grid SnapGrid.applyLayout( self, disableForPrinting = True ) # Did AToM3 want its snap grid back? :D self.atom3i.toggleSnapGrid() # Make this window go boom self.root.destroy() def getRoot(self): """get a reference to the root window of the editor""" return self.root def save(self, event=None): """ 1) Exports a graphical appearence file readable by AToM3 2) Saves everything in a pickled file (not cross-platform compatible) """ # Make sure that Text is at the end of the list gfListSorted = SaveGFVisitor.TextSortVisitor().sortGraphicalForms( self.getGFs() ) # Uber-cool save dialog :D if( 1 ): text = 'The following AToM3 file will be generated:\n\n'+\ os.path.normpath( os.path.join( self.modelPath, 'graph_' + self.className + '.py' ) ) +\ '\n\nIf you have not already done so, you should position the icon '+\ 'so that it appears where you expect it to in your models.' saveDialog = Dialog.Dialog(None, {'title': 'Saving graphical appearence file...', 'text': text,'bitmap': '', 'default': 0, 'strings': ('Position icon','Save','Save & Exit','Cancel')}) # Position icon if( saveDialog.num == 0 ): self.iconPositioner.createDialog() return self.save() # Save if( saveDialog.num == 1 ): self.exportVisitor.AToM3_export( gfListSorted ) return self.root.focus_force() # Save & Exit if( saveDialog.num == 2 ): self.exportVisitor.AToM3_export( gfListSorted ) return self.exit() # Cancel elif( saveDialog.num == 3 ): return self.root.focus_force() # Lousy Dialog save, m'kay elif( 0 ): self.exportVisitor.AToM3_export( gfListSorted ) tkMessageBox.showinfo( "Saving Graphical Appearence", "The following AToM3 file has been generated:\n\n" + os.path.normpath( os.path.join( self.modelPath, 'graph_' + self.className + '.py' ) ), parent=self.root) # Pickle save.... EWWWWWWWWWWWWW! elif( 0 ): gfList = self.getGFs() gfListCopy = [] for gf in gfList: gfListCopy.append(gf.copy()) for gf in gfListCopy: gf.setEventHandler(None) gf.setCanvas(None) gf.setZoom(1.0) # Store extra scripting info gfListCopy = [ self.scripting ] + gfListCopy fileName = os.path.normpath( os.path.join( self.modelPath, 'graph_' + self.className + '.gf1' ) ) file=open( fileName, "w") pickle.dump(gfListCopy, file) file.close() tkMessageBox.showinfo( "Saving Graphical Appearence", "The following icon-editor file has been generated:\n\n"+ fileName, parent=self.root) def export(self): filename = tkFileDialog.asksaveasfilename(title="Export",filetypes=[("Postscript","*.ps")]) if filename != "": self.canvas.postscript(file=filename) def debug(self): """ Many things can go wrong when openning a graphical file this gives the user more flexibility in figuring out wtf went wrong """ print "Error occured in importer", __file__ from tkMessageBox import askokcancel res = askokcancel("Import Error", "The existing graphical file could not be imported\n" \ "Press Ok to proceed normally, Cancel to dump error to console") # Raise the 'caught' error if(res == False): raise def open( self ): """ Tries to open an existing graphical form in the AToM3 format and to reproduce it on canvas """ fileName = os.path.normpath( os.path.join( self.modelPath, 'graph_' + self.className + '.py' ) ) if( not os.path.exists( fileName ) ): return [] nameClass = "graph_"+self.className dc = Tkinter.Canvas(self.root) # File is already loaded if nameClass in sys.modules.keys(): del sys.modules[nameClass] # Make sure we can reach the path and import from it sys.path.append( self.modelPath ) # Load it in memory try: exec "import graph_"+self.className+"\n" sys.path = sys.path[:-1] except SyntaxError: # class Name not valid sys.path = sys.path[:-1] print "Syntax Error, Could not open graphical file", self.className self.debug() return [] except IOError: # could not open file (?) sys.path = sys.path[:-1] print "IO Error, Could not open graphical file", self.className self.debug() return [] except ImportError: # could not open file... print "Import Error, Could not open graphical file", self.className self.debug() sys.path = sys.path[:-1] return [] try: # obtain the class object className = eval('graph_'+self.className+'.graph_'+self.className) except: print 'WARNING:', 'graph_'+self.className+'.graph_'+self.className, \ 'not found' return [] new_obj = className(0, 0) # create an instance of the new class new_obj.DrawObject(dc) # draw the object # Get the constraints constraintList = [] for constraint in new_obj.constraintList: constraintList.append( constraint.getValue() ) self.scripting.setConstraintList( constraintList ) self.scripting.setRunTimeChange( new_obj.ChangesAtRunTime ) GFlist = [] # List with the handles of all the shapes drawn handleList = [] for handle in dc.find_withtag(new_obj.tag): if( not handle in handleList): handleList.append(handle) # Get the connectors in self.connectors for handle in new_obj.connectors: x0, y0, x1, y1 = dc.coords(handle) if( new_obj.namedConnectors.has_key( handle) ): name = new_obj.namedConnectors[ handle ] gf = Graphics.NamedConnector(x0, y0, canvas=self.canvas, eventHandler=self.mainHandler, name=name) else: gf = Graphics.Connector(x0, y0, canvas=self.canvas, eventHandler=self.mainHandler) GFlist.append( gf ) # Get the drawn semantic attributes... handleAttributeDict = dict() for attribute in new_obj.attr_display.keys(): handleAttributeDict[ new_obj.attr_display[ attribute ] ] = attribute # Get the image dict, if any if( hasattr( new_obj, 'imageDict' ) ): Graphics.Image.IMAGE_DICT = new_obj.imageDict # Get the GraphicalForm objects objectNumberPattern = re.compile( '\Agf(\d*)\Z' ) for graphicalForm in new_obj.graphForms: handle = graphicalForm.getHandler() objectNumber = int( objectNumberPattern.search( graphicalForm.getName() ).group(1) ) coords = dc.coords( handle ) objectType = dc.type(handle) # Attribute --- Special Text if( handleAttributeDict.has_key( handle ) ): attribute = handleAttributeDict[ handle ] fontObject = graphicalForm.getFont() if( fontObject ): if( fontObject.cget( 'weight' ) == 'bold' ): bold = True else: bold = False if( fontObject.cget( 'slant' ) == 'bold' ): italic = True else: italic = False GFlist.append( Graphics.Attribute(coords[0],coords[1], canvas=self.canvas, eventHandler=self.mainHandler, fill=dc.itemcget(handle, "fill"),text=attribute, anchor=dc.itemcget(handle, "anchor"), family=fontObject.cget( 'family' ), size=int(float(fontObject.cget( 'size' ))),bold=bold,savedNumber=objectNumber, width=int(float(dc.itemcget(handle, "width"))), italic=italic,underline=int(float(fontObject.cget( 'underline' )))) ) # Backward compatibility with graphical appearences that don't have # a font object. NOTE: Font type & size info is neccessarily lost. else: GFlist.append( Graphics.Attribute(coords[0],coords[1], canvas=self.canvas, eventHandler=self.mainHandler, fill=dc.itemcget(handle, "fill"),text=attribute, width=int(float(dc.itemcget(handle, "width"))), anchor=dc.itemcget(handle, "anchor"), savedNumber=objectNumber ) ) elif( objectType == 'text' ): fontObject = graphicalForm.getFont() if( fontObject ): if( fontObject.cget( 'weight' ) == 'bold' ): bold = True else: bold = False if( fontObject.cget( 'slant' ) == 'bold' ): italic = True else: italic = False GFlist.append( Graphics.Text(coords[0],coords[1], canvas=self.canvas, eventHandler=self.mainHandler, fill=dc.itemcget(handle, "fill"),text=dc.itemcget(handle, "text"), anchor=dc.itemcget(handle, "anchor"), family=fontObject.cget( 'family' ), size=int(float(fontObject.cget( 'size' ))),bold=bold,savedNumber=objectNumber, width=int(float(dc.itemcget(handle, "width"))), italic=italic,underline=int(float(fontObject.cget( 'underline' )))) ) # Backward compatibility with graphical appearences that don't have # a font object. NOTE: Font type & size info is neccessarily lost. else: GFlist.append( Graphics.Text(coords[0],coords[1], canvas=self.canvas, eventHandler=self.mainHandler, fill=dc.itemcget(handle, "fill"),text=dc.itemcget(handle, "text"), width=int(float(dc.itemcget(handle, "width"))), anchor=dc.itemcget(handle, "anchor"), savedNumber=objectNumber ) ) elif( objectType == 'line' ): if( dc.itemcget(handle, "smooth") == 'bezier' ): smooth = True else: smooth = False GFlist.append( Graphics.Line( coords, canvas=self.canvas, eventHandler=self.mainHandler, fill=dc.itemcget(handle, "fill"), stipple=dc.itemcget(handle, "stipple"), arrow=dc.itemcget(handle, "arrow"),capstyle=dc.itemcget(handle, "capstyle"), joinstyle=dc.itemcget(handle, "joinstyle"),smooth=smooth,savedNumber=objectNumber, width=int(float(dc.itemcget(handle, "width"))) ) ) elif( objectType == 'polygon' ): if( dc.itemcget(handle, "smooth") == 'bezier' ): smooth = True else: smooth = False GFlist.append( Graphics.Polygon( coords, canvas=self.canvas, eventHandler=self.mainHandler, fill=dc.itemcget(handle, "fill"), stipple=dc.itemcget(handle, "stipple"), smooth=smooth,outline=dc.itemcget(handle, "outline"),savedNumber=objectNumber, width=int(float(dc.itemcget(handle, "width"))) ) ) elif( objectType == 'oval' ): x0,y0,x1,y1 = coords GFlist.append( Graphics.Oval( x0,y0,x1,y1, canvas=self.canvas, eventHandler=self.mainHandler, fill=dc.itemcget(handle, "fill"),outline=dc.itemcget(handle, "outline"), width=int(float(dc.itemcget(handle, "width" ))), savedNumber=objectNumber, stipple=dc.itemcget(handle, "stipple") ) ) elif( objectType == 'rectangle' ): x0,y0,x1,y1 = coords GFlist.append( Graphics.Rectangle( x0,y0,x1,y1, canvas=self.canvas, eventHandler=self.mainHandler, fill=dc.itemcget(handle, "fill"),outline=dc.itemcget(handle, "outline"), width=int(float(dc.itemcget(handle, "width" ))), savedNumber=objectNumber, stipple=dc.itemcget(handle, "stipple") ) ) elif( objectType == 'image' ): fileName = graphicalForm.getImageFilename() if( not Graphics.Image.IMAGE_DICT.has_key( fileName ) ): print "ERROR: could not load image " + fileName + "... SKIPPED" continue ''' pathName = '' for path in sys.path: if( not os.path.isdir( path ) ): continue if( pathName ): break for file in os.listdir( path ): if( file == fileName ): pathName = os.path.join( path, fileName ) break if( not fileName ): print "ERROR: could not load image " + fileName + "... SKIPPED" continue ''' GFlist.append( Graphics.Image( coords[0], coords[1], canvas=self.canvas, eventHandler=self.mainHandler, savedNumber=objectNumber, filename=fileName ) ) else: print "WARNING: Attempted to load unsupported objectType: " + str( objectType) #print GFlist dc.destroy() return GFlist def openOLD(self): if( 0 ): return [] elif( 1 ): fileName = os.path.join( self.modelPath, 'graph_' + self.className + '.gf1' ) if( os.path.exists( fileName ) ): f = open( fileName, 'r' ) else: return [] try: gfList = pickle.load(f) except ImportError: print "Failed to load pickled graphic data" return [] # Get the scripting stuff out of the way... self.scripting = gfList[0] if( self.scripting.getRunTimeChange() ): self.menuBar.getModelMenu().entryconfigure( 0, label = "Changes at run-time ENABLED" ) else: self.menuBar.getModelMenu().entryconfigure( 0, label = "Changes at run-time DISABLED" ) for gf in gfList[1:]: gf.setCanvas(self.canvas) gf.setEventHandler(self.mainHandler) return gfList[1:] elif( 0 ): return [Graphics.Rectangle(50, 50, 90, 90, canvas=self.canvas, outline="black", fill="blue", width=2, eventHandler=self.mainHandler), Graphics.Oval(50, 50, 90, 90, canvas=self.canvas, outline="black", fill="green", eventHandler=self.mainHandler), Graphics.Text(70, 90, canvas=self.canvas, zoom=1.00, eventHandler=self.mainHandler, text="text", fill="red"), Graphics.Connector(100, 100, canvas=self.canvas, zoom=1.00, eventHandler=self.mainHandler), Graphics.Polygon([50, 50, 90, 90, 32, 2], canvas=self.canvas, outline="green", fill="purple", width=2, eventHandler=self.mainHandler), Graphics.Line([50, 10, 50, 50, 132, 50], canvas=self.canvas, fill="black", eventHandler=self.mainHandler), Graphics.Composite([Graphics.Rectangle(50, 20, 140, 100, canvas=self.canvas, outline="black", fill="yellow", width=2, eventHandler=self.mainHandler), Graphics.Rectangle(50, 50, 100, 120, canvas=self.canvas, outline="black", fill="purple", width=2, eventHandler=self.mainHandler), Graphics.Oval(70, 20, 40, 90, canvas=self.canvas, outline="black", fill="green", width=2, eventHandler=self.mainHandler), Graphics.Oval(50, 50, 40, 20, canvas=self.canvas, outline="black", fill="gray", width=2, eventHandler=self.mainHandler)], canvas=self.canvas, zoom=100, eventHandler=self.mainHandler)] def cut(self, gfList): if len(gfList) > 0: previousClipboard = self.clipboardList self.clipboardList = gfList indexList = [] for gf in gfList: gf.setCanvas(None) indexList.append(self.removeGF(gf)) pairList = map(None, gfList, indexList) self.undoStack.append((self.undo_cut, [previousClipboard, pairList])) def copy(self, gfList): if len(gfList) > 0: previousClipboard = self.clipboardList self.clipboardList = [] for gf in gfList: cgf = gf.copy() self.clipboardList.append(cgf) cgf.setCanvas(None) def paste(self): clipboardCopy = [] for gf in self.clipboardList: c = gf.copy() c.setZoom(self.getZoom()) clipboardCopy.append(c) for gf in clipboardCopy: gf.translate(10, 10) gf.setCanvas(self.canvas) self.appendGF(gf) self.undoStack.append((self.undo_paste, [clipboardCopy])) return clipboardCopy #The GFs in gfList are not necessarily in the order in which they are drawn. #Since we want the relative order of the selection to be preserved, we first bring #to top the GF which is drawn first. def bringToTop(self, gfList): allGFs = self.getGFs() pairList = [] for gf in allGFs: if gf in gfList: pairList.append((gf, self.bringToTopGF(gf))) if len(pairList) > 0: self.undoStack.append((self.undo_bringPush, [pairList])) #(see bringToTop) Here we need to reverse the list to push the GFs in the right order. def pushToBottom(self, gfList): allGFs = self.getGFs() allGFs.reverse() pairList = [] for gf in allGFs: if gf in gfList: pairList.append((gf, self.pushToBottomGF(gf))) if len(pairList) > 0: self.undoStack.append((self.undo_bringPush, [pairList])) def compose(self, gfList): if len(gfList) > 1: # create a composite only if it's worth it allGFs = self.getGFs() sortedList = [] for gf in allGFs: if gf in gfList: sortedList.append(gf) indexList = [] for gf in sortedList: indexList.append(self.removeGF(gf)) composite = Graphics.Composite(sortedList, canvas=self.canvas, zoom=self.getZoom(), eventHandler=self.mainHandler) self.insertGF(indexList[len(indexList)-1], composite) self.undoStack.append((self.undo_compose, [composite, indexList])) return [composite] else: return gfList def decompose(self, gfList): compositeList = [] for gf in gfList: componentList = self.compositionVisitor.decompose(gf) if len(componentList) > 0: #if gf was decomposed compositeList.append(gf) #add the composite to the list of composites self.undoStack.append((self.undo_decompose, [compositeList])) def delete(self, gfList): indexList = [] for gf in gfList: gf.setCanvas(None) indexList.append(self.removeGF(gf)) pairList = map(None, gfList, indexList) self.undoStack.append((self.undo_delete, [pairList])) def getBoundingBox(self, gfList): boxes = [] for gf in gfList: if gf.getCanvas() != None: boxes.append(gf.getApproxBoundingBox()) if len(boxes) == 0: raise TypeError, "gfList contains no active GF" xMin = boxes[0][0] yMin = boxes[0][1] xMax = boxes[0][2] yMax = boxes[0][3] for box in boxes: if box[0] < xMin: xMin = box[0] if box[1] < yMin: yMin = box[1] if box[2] > xMax: xMax = box[2] if box[3] > yMax: yMax = box[3] return [xMin, yMin, xMax, yMax] def setFillColor(self, gfList, color): undoList = self.colorVisitor.setFillColor(gfList, color) if len(undoList) > 0: self.undoStack.append((self.undo_setFillColor, [undoList])) def setOutlineColor(self, gfList, color): undoList = self.colorVisitor.setOutlineColor(gfList, color) if len(undoList) > 0: self.undoStack.append((self.undo_setOutlineColor, [undoList])) def setLineWidth(self, gfList, lineWidth): undoList = self.widthVisitor.setWidth(gfList, lineWidth) if len(undoList) > 0: self.undoStack.append((self.undo_setLineWidth, [undoList])) def setOutlineFillOption(self, gfList, option): undoList = self.optionVisitor.changeOption(gfList, option) if len(undoList) > 0: self.undoStack.append((self.undo_setOutlineFillOption, [undoList])) def createRectangle(self, xy): gf = Graphics.Rectangle(xy[0], xy[1], xy[2], xy[3], canvas=self.canvas, outline=self.getOutlineColor(), outlineOption=self.hasOutline(), fill=self.getFillColor(), fillOption=self.hasFill(), width=self.getLineWidth(), zoom=self.getZoom(), eventHandler=self.mainHandler) self.appendGF(gf) self.undoStack.append((self.undo_create, [[gf]])) return gf def createOval(self, xy): gf = Graphics.Oval(xy[0], xy[1], xy[2], xy[3], canvas=self.canvas, outline=self.getOutlineColor(), outlineOption=self.hasOutline(), fill=self.getFillColor(), fillOption=self.hasFill(), width=self.getLineWidth(), zoom=self.getZoom(), eventHandler=self.mainHandler) self.appendGF(gf) self.undoStack.append((self.undo_create, [[gf]])) return gf def createLine(self, xy, smooth=0): gf = Graphics.Line(xy, canvas=self.canvas, fill=self.getFillColor(), width=self.getLineWidth(), zoom=self.getZoom(), eventHandler=self.mainHandler, smooth = smooth) self.appendGF(gf) self.undoStack.append((self.undo_create, [[gf]])) return gf def createPolygon(self, xy, smooth=0): gf = Graphics.Polygon(xy, canvas=self.canvas, outline=self.getOutlineColor(), outlineOption=self.hasOutline(), fill=self.getFillColor(), fillOption=self.hasFill(), width=self.getLineWidth(), zoom=self.getZoom(), eventHandler=self.mainHandler, smooth = smooth) self.appendGF(gf) self.undoStack.append((self.undo_create, [[gf]])) return gf def createConnector(self, xy): gf = Graphics.Connector(xy[0], xy[1], canvas=self.canvas, zoom=self.getZoom(), eventHandler=self.mainHandler) self.appendGF(gf) self.undoStack.append((self.undo_create, [[gf]])) return gf def createImage(self, xy, filename): gf = Graphics.Image(xy[0], xy[1], filename, canvas=self.canvas, zoom=self.getZoom(), eventHandler=self.mainHandler) self.appendGF(gf) self.undoStack.append((self.undo_create, [[gf]])) return gf def createText(self, xy, text): gf = Graphics.Text(xy[0], xy[1], canvas=self.canvas, zoom=self.getZoom(), eventHandler=self.mainHandler, text=text, fill=self.getFillColor()) self.appendGF(gf) self.undoStack.append((self.undo_create, [[gf]])) return gf def createNamedConnector(self, xy): gf = Graphics.NamedConnector(xy[0], xy[1], canvas=self.canvas, zoom=self.getZoom(), eventHandler=self.mainHandler) self.appendGF(gf) self.undoStack.append((self.undo_create, [[gf]])) return gf def createAttribute(self, xy, text ): gf = Graphics.Attribute(xy[0], xy[1], canvas=self.canvas, zoom=self.getZoom(), eventHandler=self.mainHandler, text=text ) self.appendGF(gf) self.undoStack.append((self.undo_create, [[gf]])) return gf def isUndoStackEmpty(self): if len(self.undoStack) == 0: return 1 else: return 0 def isClipboardEmpty(self): if len(self.clipboardList) == 0: return 1 else: return 0 def translate(self, gfList, dx, dy): for gf in gfList: gf.translate(dx, dy) self.undoStack.append((self.undo_translate, [gfList, -dx,-dy])) def rotate(self, gfList, centerX, centerY, angle): for gf in gfList: gf.rotate(centerX, centerY, angle) self.undoStack.append((self.undo_rotate, [gfList, centerX, centerY, -angle])) def scale(self, gfList, centerX, centerY, factorX, factorY): for gf in gfList: gf.scale(centerX, centerY, factorX, factorY) self.undoStack.append((self.undo_scale, [gfList, centerX, centerY, 1/factorX, 1/factorY])) # the following method is used by the translation handler to add an undo command. # Instead of creating an undo for each small translation (by using the normal translate method of the editor), # the handler calls the "add undo translation" method of the editor when the translation is complete. def addUndoTranslate(self, gfList, dx, dy): if( not (dx == 0 and dy == 0) ): self.undoStack.append((self.undo_translate, [gfList, -dx,-dy])) # see addUndoTranslate def addUndoRotate(self, gfList, centerX, centerY, angle): self.undoStack.append((self.undo_rotate, [gfList, centerX, centerY, -angle])) # see addUndoTranslate def addUndoScale(self, gfList, centerX, centerY, factorX, factorY): self.undoStack.append((self.undo_scale, [gfList, centerX, centerY, 1/factorX, 1/factorY])) def addUndoSetCoords(self, gf, oldCoords): self.undoStack.append((self.undo_setCoords, [gf, oldCoords])) def undo(self): method, args = self.undoStack.pop() apply(method, args) ####################### # undo commands def undo_create(self, gfList): for gf in gfList: gf.setCanvas(None) self.removeGF(gf) def undo_delete(self, pairList): # pairList is a list of pairs, where each # pair contains an inactive GF and its position in the canvas. # pairList must be reversed because the index is valid # for the editor's list after the other gfs are deleted. pairList.reverse() for gf, index in pairList: gf.setCanvas(self.canvas) self.insertGF(index, gf) def undo_cut(self, previousClipboard, pairList): # pairList is a list of pairs, where each # pair contains an inactive GF and its position in the canvas. # pairList must be reversed because the index is valid # for the editor's list after the other gfs are deleted. pairList.reverse() for gf, index in pairList: gf.setCanvas(self.canvas) self.insertGF(index, gf) self.clipboardList = previousClipboard def undo_copy(self, previousClipboard): self.clipboardList = previousClipboard def undo_paste(self, pastedList): for gf in pastedList: gf.setCanvas(None) self.removeGF(gf) def undo_bringPush(self, pairList): pairList.reverse() for gf, index in pairList: self.removeGF(gf) self.insertGF(index, gf) def undo_compose(self, compositeGF, indexList): gfList = compositeGF.getComponents() self.removeGF(compositeGF) pairList = map(None, gfList, indexList) pairList.reverse() for gf, index in pairList: gf.setEventHandler(self.mainHandler) self.insertGF(index, gf) def undo_decompose(self, compositeList): for composite in compositeList: gfList = composite.getComponents() for gf in gfList: index = self.removeGF(gf) gf.setEventHandler(composite) self.insertGF(index, composite) def undo_translate(self, gfList, dx, dy): for gf in gfList: gf.translate(dx, dy) def undo_rotate(self, gfList, centerX, centerY, angle): for gf in gfList: gf.rotate(centerX, centerY, angle) def undo_scale(self, gfList, centerX, centerY, factorX, factorY): for gf in gfList: gf.scale(centerX, centerY, factorX, factorY) def undo_setCoords(self, gf, oldCoords): gf.setCoords(oldCoords) def undo_setFillColor(self, undoList): for gf, color in undoList: gf.setFillColor(color) def undo_setOutlineColor(self, undoList): for gf, color in undoList: gf.setOutlineColor(color) def undo_setLineWidth(self, undoList): for gf, width in undoList: gf.setWidth(width) def undo_setOutlineFillOption(self, undoList): for gf, (outlineOption, fillOption) in undoList: gf.setOutlineOption(outlineOption) gf.setFillOption(fillOption) ###################### def appendGF(self, gf): self.GFs.append(gf) def insertGF(self, index, gf): self.GFs.insert(index, gf) for gf in self.GFs: gf.bringToTop() def getIndex(self, gf): return self.GFs.index(gf) def removeGF(self, gf): index = self.GFs.index(gf) self.GFs.remove(gf) return index #return the index the gf had in the list (which was also its relative position on the canvas) # convention: the first gfs in the list are drawn first. # The order in which the GFs are drawn is useful when we want to save. def bringToTopGF(self, gf): gf.bringToTop() index = self.removeGF(gf) self.GFs.append(gf) return index def pushToBottomGF(self, gf): gf.pushToBottom() index = self.removeGF(gf) self.GFs.insert(0, gf) return index def getGFs(self): return self.GFs[:] def setGFs(self, gfList ): self.GFs = gfList def getEventHandler(self): return self.mainHandler def getCanvasWidth(self): return self.workspace.getCanvasWidth() def getCanvasHeight(self): return self.workspace.getCanvasHeight() def getCanvas(self): return self.workspace.getCanvas() def getZoom(self): return self.workspace.getZoom() def setZoom(self, zoom, x, y): self.workspace.setZoom(zoom, x, y) # update statusBar and GFs to the new zoom (whether it changed or not) newZoom = self.workspace.getZoom() self.statusBar.setZoom(newZoom) for gf in self.GFs: gf.setZoom(newZoom) def setToolSelector(self, tool): self.toolSelector.set(tool) def getLineWidth(self): return self.lineWidthSelector.get() def getOutlineColor(self): return self.colorSelector.getOutlineColor() def getFillColor(self): return self.colorSelector.getFillColor() def hasOutline(self): option = self.outlineFillOptionSelector.get() return option[0] def hasFill(self): option = self.outlineFillOptionSelector.get() return option[1]
class Editor: # Canvas size is used by the snap grid to determine how far to draw the grid # Actually, should be called the canvas bounding box :p # BTW: Using negative values, or values that are too small, doesn't work well :p CANVAS_SIZE_TUPLE = [0, 0, 2000, 2000] def __init__(self, semObject, className, modelPath=None): # Use the AToM3 Tkinter root window self.className = className root = self.rootInitilization(semObject, modelPath) if (not root): return self.root = root self.mainHandler = MainHandler.MainHandler(self) root.bind("<Key>", self.mainHandler.onKey) root.bind("<Shift-Key>", self.mainHandler.onShiftKey) root.bind("<Control-Key>", self.mainHandler.onControlKey) zoom = 1.0 self.menuBar = Tools.MenuBar(root, self, self.mainHandler) #goes to top by itself self.statusBar = Tools.StatusBar(root, "", zoom, 0, 0) #goes to bottom by itself self.colorSelector = Colors.ColorSelector( root, self.mainHandler) #goes to bottom by itself self.toolFrame = Tkinter.Frame(root, relief=Tkinter.RAISED, bd=1) self.toolSelector = Tools.ToolSelector(self.toolFrame, self.mainHandler, Tkinter.TOP) self.outlineFillOptionSelector = Tools.OutlineFillOptionSelector( self.toolFrame, self.mainHandler, Tkinter.TOP) self.lineWidthSelector = Tools.LineWidthSelector( self.toolFrame, self.mainHandler, Tkinter.TOP) self.toolFrame.pack(side=Tkinter.LEFT) self.workspace = Workspace(self, self.CANVAS_SIZE_TUPLE[2], self.CANVAS_SIZE_TUPLE[3], self.CANVAS_SIZE_TUPLE[2], self.CANVAS_SIZE_TUPLE[3], self.mainHandler, zoom) #goes to the right by itself self.workspace.setZoom(zoom, 0, 0) self.canvas = self.workspace.getCanvas() self.scripting = Scripting() ## self.GFs = self.open() self.GFs = [] self.extendedInitilization(semObject) self.clipboardList = [] self.undoStack = [] self.mainHandler.start() self.compositionVisitor = GFVisitors.CompositionVisitor(self) self.colorVisitor = GFVisitors.ColorVisitor() self.widthVisitor = GFVisitors.WidthVisitor() self.optionVisitor = GFVisitors.OptionVisitor(self) # Carefully try to load the GF model (may fail for random reasons) try: self.GFs = self.open() except: raise def rootInitilization(self, semObject, modelPathAndFile): """ Extension to the old initilization routine This directly sets up the editor for generating icons for AToM3 """ # Do we have everything we need? Check the minimum requirements: try: atom3i = semObject.parent TkRoot = atom3i.parent statusbar = atom3i.statusbar except: print "ERROR: Unable to start editor, lacking information (atom3i,TkRoot, or statusbar)" return None self.atom3i = atom3i # Do we have the fileName we'll be using to save the graphicalAppearence? if (not modelPathAndFile): modelPathAndFile = statusbar.getState(statusbar.MODEL)[1][0] self.modelPath = os.path.split(modelPathAndFile)[0] if (self.modelPath == ''): tkMessageBox.showerror( "Icon-Editor model path error", "Please save your ER model before modifying graphical appearences.", parent=TkRoot) return None # Setup a new toplevel window for the editor root = Tkinter.Toplevel(TkRoot) root.title("Icon Editor - AToM3") root.geometry("%dx%d%+d%+d" % (800, 600, 100, 0)) root.transient(TkRoot) root.grab_set() return root def extendedInitilization(self, semObject): # SnapGrid: since this is a singleton, must first disable old stuff self.snapGridInfoTuple = None self.snapGridInfoBackup = None SnapGrid.applyLayout(self, disableForPrinting=True) SnapGrid.applyLayout(self) # Useful binds self.root.bind("<Alt-x>", lambda event: self.mainHandler.onExit()) self.root.bind("<F1>", lambda event: self.mainHandler.onSnapSetting()) self.root.bind("<Control-e>", lambda event: self.mainHandler.onExport()) self.root.protocol("WM_DELETE_WINDOW", self.mainHandler.onExit) # AToM3 graphical appearence exporter self.exportVisitor = SaveGFVisitor.ATOM3_Export_Visitor( semObject, self) # Extra info: includes text attributes that change dynamically & named ports self.attributes = semObject.attributesToDraw() # Icon Positioning System (not quite GPS) self.iconPositioner = IconPositioner(self) self.root.bind("<F2>", lambda event: self.iconPositioner.createDialog()) def iconPlacer(self, anchor='nw'): """ Places the graphical icon at a position determined by 'anchor' If anchor is 'nw', then the icon will be moved so that when the user creates an item with this icon, the top-left edge of the icon appears at the position the user clicked. If anchor is 'origin', then the icon appears exactly centered on origin. If anchor is of type 'float', then use the float value directly. """ x0, y0, x1, y1 = self.getBoundingBox(self.getGFs()) # Top-Left positioning if (anchor == 'nw'): dx = -x0 dy = -y0 # Center positioning elif (anchor == 'origin'): dx = -x0 - (x1 - x0) / 2 dy = -y0 - (y1 - y0) / 2 # Manual offset elif (type(anchor) == type(float())): dx = dy = anchor # Invalid use else: raise Exception, "Wake up and smell the API you lopsided kitten burger! j/k" for gf in self.getGFs(): gf.translate(dx, dy) self.addUndoTranslate(self.getGFs(), dx, dy) def getAttributes(self): return self.attributes def exit(self, event=None): """ Exit point for the Icon-Editor """ # Clean up the snap grid SnapGrid.applyLayout(self, disableForPrinting=True) # Did AToM3 want its snap grid back? :D self.atom3i.toggleSnapGrid() # Make this window go boom self.root.destroy() def getRoot(self): """get a reference to the root window of the editor""" return self.root def save(self, event=None): """ 1) Exports a graphical appearence file readable by AToM3 2) Saves everything in a pickled file (not cross-platform compatible) """ # Make sure that Text is at the end of the list gfListSorted = SaveGFVisitor.TextSortVisitor().sortGraphicalForms( self.getGFs()) # Uber-cool save dialog :D if (1): text = 'The following AToM3 file will be generated:\n\n'+\ os.path.normpath( os.path.join( self.modelPath, 'graph_' + self.className + '.py' ) ) +\ '\n\nIf you have not already done so, you should position the icon '+\ 'so that it appears where you expect it to in your models.' saveDialog = Dialog.Dialog( None, { 'title': 'Saving graphical appearence file...', 'text': text, 'bitmap': '', 'default': 0, 'strings': ('Position icon', 'Save', 'Save & Exit', 'Cancel') }) # Position icon if (saveDialog.num == 0): self.iconPositioner.createDialog() return self.save() # Save if (saveDialog.num == 1): self.exportVisitor.AToM3_export(gfListSorted) return self.root.focus_force() # Save & Exit if (saveDialog.num == 2): self.exportVisitor.AToM3_export(gfListSorted) return self.exit() # Cancel elif (saveDialog.num == 3): return self.root.focus_force() # Lousy Dialog save, m'kay elif (0): self.exportVisitor.AToM3_export(gfListSorted) tkMessageBox.showinfo( "Saving Graphical Appearence", "The following AToM3 file has been generated:\n\n" + os.path.normpath( os.path.join(self.modelPath, 'graph_' + self.className + '.py')), parent=self.root) # Pickle save.... EWWWWWWWWWWWWW! elif (0): gfList = self.getGFs() gfListCopy = [] for gf in gfList: gfListCopy.append(gf.copy()) for gf in gfListCopy: gf.setEventHandler(None) gf.setCanvas(None) gf.setZoom(1.0) # Store extra scripting info gfListCopy = [self.scripting] + gfListCopy fileName = os.path.normpath( os.path.join(self.modelPath, 'graph_' + self.className + '.gf1')) file = open(fileName, "w") pickle.dump(gfListCopy, file) file.close() tkMessageBox.showinfo( "Saving Graphical Appearence", "The following icon-editor file has been generated:\n\n" + fileName, parent=self.root) def export(self): filename = tkFileDialog.asksaveasfilename(title="Export", filetypes=[("Postscript", "*.ps")]) if filename != "": self.canvas.postscript(file=filename) def debug(self): """ Many things can go wrong when openning a graphical file this gives the user more flexibility in figuring out wtf went wrong """ print "Error occured in importer", __file__ from tkMessageBox import askokcancel res = askokcancel("Import Error", "The existing graphical file could not be imported\n" \ "Press Ok to proceed normally, Cancel to dump error to console") # Raise the 'caught' error if (res == False): raise def open(self): """ Tries to open an existing graphical form in the AToM3 format and to reproduce it on canvas """ fileName = os.path.normpath( os.path.join(self.modelPath, 'graph_' + self.className + '.py')) if (not os.path.exists(fileName)): return [] nameClass = "graph_" + self.className dc = Tkinter.Canvas(self.root) # File is already loaded if nameClass in sys.modules.keys(): del sys.modules[nameClass] # Make sure we can reach the path and import from it sys.path.append(self.modelPath) # Load it in memory try: exec "import graph_" + self.className + "\n" sys.path = sys.path[:-1] except SyntaxError: # class Name not valid sys.path = sys.path[:-1] print "Syntax Error, Could not open graphical file", self.className self.debug() return [] except IOError: # could not open file (?) sys.path = sys.path[:-1] print "IO Error, Could not open graphical file", self.className self.debug() return [] except ImportError: # could not open file... print "Import Error, Could not open graphical file", self.className self.debug() sys.path = sys.path[:-1] return [] try: # obtain the class object className = eval('graph_' + self.className + '.graph_' + self.className) except: print 'WARNING:', 'graph_'+self.className+'.graph_'+self.className, \ 'not found' return [] new_obj = className(0, 0) # create an instance of the new class new_obj.DrawObject(dc) # draw the object # Get the constraints constraintList = [] for constraint in new_obj.constraintList: constraintList.append(constraint.getValue()) self.scripting.setConstraintList(constraintList) self.scripting.setRunTimeChange(new_obj.ChangesAtRunTime) GFlist = [] # List with the handles of all the shapes drawn handleList = [] for handle in dc.find_withtag(new_obj.tag): if (not handle in handleList): handleList.append(handle) # Get the connectors in self.connectors for handle in new_obj.connectors: x0, y0, x1, y1 = dc.coords(handle) if (new_obj.namedConnectors.has_key(handle)): name = new_obj.namedConnectors[handle] gf = Graphics.NamedConnector(x0, y0, canvas=self.canvas, eventHandler=self.mainHandler, name=name) else: gf = Graphics.Connector(x0, y0, canvas=self.canvas, eventHandler=self.mainHandler) GFlist.append(gf) # Get the drawn semantic attributes... handleAttributeDict = dict() for attribute in new_obj.attr_display.keys(): handleAttributeDict[new_obj.attr_display[attribute]] = attribute # Get the image dict, if any if (hasattr(new_obj, 'imageDict')): Graphics.Image.IMAGE_DICT = new_obj.imageDict # Get the GraphicalForm objects objectNumberPattern = re.compile('\Agf(\d*)\Z') for graphicalForm in new_obj.graphForms: handle = graphicalForm.getHandler() objectNumber = int( objectNumberPattern.search(graphicalForm.getName()).group(1)) coords = dc.coords(handle) objectType = dc.type(handle) # Attribute --- Special Text if (handleAttributeDict.has_key(handle)): attribute = handleAttributeDict[handle] fontObject = graphicalForm.getFont() if (fontObject): if (fontObject.cget('weight') == 'bold'): bold = True else: bold = False if (fontObject.cget('slant') == 'bold'): italic = True else: italic = False GFlist.append( Graphics.Attribute( coords[0], coords[1], canvas=self.canvas, eventHandler=self.mainHandler, fill=dc.itemcget(handle, "fill"), text=attribute, anchor=dc.itemcget(handle, "anchor"), family=fontObject.cget('family'), size=int(float(fontObject.cget('size'))), bold=bold, savedNumber=objectNumber, width=int(float(dc.itemcget(handle, "width"))), italic=italic, underline=int(float( fontObject.cget('underline'))))) # Backward compatibility with graphical appearences that don't have # a font object. NOTE: Font type & size info is neccessarily lost. else: GFlist.append( Graphics.Attribute( coords[0], coords[1], canvas=self.canvas, eventHandler=self.mainHandler, fill=dc.itemcget(handle, "fill"), text=attribute, width=int(float(dc.itemcget(handle, "width"))), anchor=dc.itemcget(handle, "anchor"), savedNumber=objectNumber)) elif (objectType == 'text'): fontObject = graphicalForm.getFont() if (fontObject): if (fontObject.cget('weight') == 'bold'): bold = True else: bold = False if (fontObject.cget('slant') == 'bold'): italic = True else: italic = False GFlist.append( Graphics.Text( coords[0], coords[1], canvas=self.canvas, eventHandler=self.mainHandler, fill=dc.itemcget(handle, "fill"), text=dc.itemcget(handle, "text"), anchor=dc.itemcget(handle, "anchor"), family=fontObject.cget('family'), size=int(float(fontObject.cget('size'))), bold=bold, savedNumber=objectNumber, width=int(float(dc.itemcget(handle, "width"))), italic=italic, underline=int(float( fontObject.cget('underline'))))) # Backward compatibility with graphical appearences that don't have # a font object. NOTE: Font type & size info is neccessarily lost. else: GFlist.append( Graphics.Text(coords[0], coords[1], canvas=self.canvas, eventHandler=self.mainHandler, fill=dc.itemcget(handle, "fill"), text=dc.itemcget(handle, "text"), width=int( float(dc.itemcget(handle, "width"))), anchor=dc.itemcget(handle, "anchor"), savedNumber=objectNumber)) elif (objectType == 'line'): if (dc.itemcget(handle, "smooth") == 'bezier'): smooth = True else: smooth = False GFlist.append( Graphics.Line(coords, canvas=self.canvas, eventHandler=self.mainHandler, fill=dc.itemcget(handle, "fill"), stipple=dc.itemcget(handle, "stipple"), arrow=dc.itemcget(handle, "arrow"), capstyle=dc.itemcget(handle, "capstyle"), joinstyle=dc.itemcget(handle, "joinstyle"), smooth=smooth, savedNumber=objectNumber, width=int(float(dc.itemcget(handle, "width"))))) elif (objectType == 'polygon'): if (dc.itemcget(handle, "smooth") == 'bezier'): smooth = True else: smooth = False GFlist.append( Graphics.Polygon(coords, canvas=self.canvas, eventHandler=self.mainHandler, fill=dc.itemcget(handle, "fill"), stipple=dc.itemcget(handle, "stipple"), smooth=smooth, outline=dc.itemcget(handle, "outline"), savedNumber=objectNumber, width=int( float(dc.itemcget(handle, "width"))))) elif (objectType == 'oval'): x0, y0, x1, y1 = coords GFlist.append( Graphics.Oval(x0, y0, x1, y1, canvas=self.canvas, eventHandler=self.mainHandler, fill=dc.itemcget(handle, "fill"), outline=dc.itemcget(handle, "outline"), width=int(float(dc.itemcget(handle, "width"))), savedNumber=objectNumber, stipple=dc.itemcget(handle, "stipple"))) elif (objectType == 'rectangle'): x0, y0, x1, y1 = coords GFlist.append( Graphics.Rectangle(x0, y0, x1, y1, canvas=self.canvas, eventHandler=self.mainHandler, fill=dc.itemcget(handle, "fill"), outline=dc.itemcget(handle, "outline"), width=int( float(dc.itemcget(handle, "width"))), savedNumber=objectNumber, stipple=dc.itemcget(handle, "stipple"))) elif (objectType == 'image'): fileName = graphicalForm.getImageFilename() if (not Graphics.Image.IMAGE_DICT.has_key(fileName)): print "ERROR: could not load image " + fileName + "... SKIPPED" continue ''' pathName = '' for path in sys.path: if( not os.path.isdir( path ) ): continue if( pathName ): break for file in os.listdir( path ): if( file == fileName ): pathName = os.path.join( path, fileName ) break if( not fileName ): print "ERROR: could not load image " + fileName + "... SKIPPED" continue ''' GFlist.append( Graphics.Image(coords[0], coords[1], canvas=self.canvas, eventHandler=self.mainHandler, savedNumber=objectNumber, filename=fileName)) else: print "WARNING: Attempted to load unsupported objectType: " + str( objectType) #print GFlist dc.destroy() return GFlist def openOLD(self): if (0): return [] elif (1): fileName = os.path.join(self.modelPath, 'graph_' + self.className + '.gf1') if (os.path.exists(fileName)): f = open(fileName, 'r') else: return [] try: gfList = pickle.load(f) except ImportError: print "Failed to load pickled graphic data" return [] # Get the scripting stuff out of the way... self.scripting = gfList[0] if (self.scripting.getRunTimeChange()): self.menuBar.getModelMenu().entryconfigure( 0, label="Changes at run-time ENABLED") else: self.menuBar.getModelMenu().entryconfigure( 0, label="Changes at run-time DISABLED") for gf in gfList[1:]: gf.setCanvas(self.canvas) gf.setEventHandler(self.mainHandler) return gfList[1:] elif (0): return [ Graphics.Rectangle(50, 50, 90, 90, canvas=self.canvas, outline="black", fill="blue", width=2, eventHandler=self.mainHandler), Graphics.Oval(50, 50, 90, 90, canvas=self.canvas, outline="black", fill="green", eventHandler=self.mainHandler), Graphics.Text(70, 90, canvas=self.canvas, zoom=1.00, eventHandler=self.mainHandler, text="text", fill="red"), Graphics.Connector(100, 100, canvas=self.canvas, zoom=1.00, eventHandler=self.mainHandler), Graphics.Polygon([50, 50, 90, 90, 32, 2], canvas=self.canvas, outline="green", fill="purple", width=2, eventHandler=self.mainHandler), Graphics.Line([50, 10, 50, 50, 132, 50], canvas=self.canvas, fill="black", eventHandler=self.mainHandler), Graphics.Composite([ Graphics.Rectangle(50, 20, 140, 100, canvas=self.canvas, outline="black", fill="yellow", width=2, eventHandler=self.mainHandler), Graphics.Rectangle(50, 50, 100, 120, canvas=self.canvas, outline="black", fill="purple", width=2, eventHandler=self.mainHandler), Graphics.Oval(70, 20, 40, 90, canvas=self.canvas, outline="black", fill="green", width=2, eventHandler=self.mainHandler), Graphics.Oval(50, 50, 40, 20, canvas=self.canvas, outline="black", fill="gray", width=2, eventHandler=self.mainHandler) ], canvas=self.canvas, zoom=100, eventHandler=self.mainHandler) ] def cut(self, gfList): if len(gfList) > 0: previousClipboard = self.clipboardList self.clipboardList = gfList indexList = [] for gf in gfList: gf.setCanvas(None) indexList.append(self.removeGF(gf)) pairList = map(None, gfList, indexList) self.undoStack.append( (self.undo_cut, [previousClipboard, pairList])) def copy(self, gfList): if len(gfList) > 0: previousClipboard = self.clipboardList self.clipboardList = [] for gf in gfList: cgf = gf.copy() self.clipboardList.append(cgf) cgf.setCanvas(None) def paste(self): clipboardCopy = [] for gf in self.clipboardList: c = gf.copy() c.setZoom(self.getZoom()) clipboardCopy.append(c) for gf in clipboardCopy: gf.translate(10, 10) gf.setCanvas(self.canvas) self.appendGF(gf) self.undoStack.append((self.undo_paste, [clipboardCopy])) return clipboardCopy #The GFs in gfList are not necessarily in the order in which they are drawn. #Since we want the relative order of the selection to be preserved, we first bring #to top the GF which is drawn first. def bringToTop(self, gfList): allGFs = self.getGFs() pairList = [] for gf in allGFs: if gf in gfList: pairList.append((gf, self.bringToTopGF(gf))) if len(pairList) > 0: self.undoStack.append((self.undo_bringPush, [pairList])) #(see bringToTop) Here we need to reverse the list to push the GFs in the right order. def pushToBottom(self, gfList): allGFs = self.getGFs() allGFs.reverse() pairList = [] for gf in allGFs: if gf in gfList: pairList.append((gf, self.pushToBottomGF(gf))) if len(pairList) > 0: self.undoStack.append((self.undo_bringPush, [pairList])) def compose(self, gfList): if len(gfList) > 1: # create a composite only if it's worth it allGFs = self.getGFs() sortedList = [] for gf in allGFs: if gf in gfList: sortedList.append(gf) indexList = [] for gf in sortedList: indexList.append(self.removeGF(gf)) composite = Graphics.Composite(sortedList, canvas=self.canvas, zoom=self.getZoom(), eventHandler=self.mainHandler) self.insertGF(indexList[len(indexList) - 1], composite) self.undoStack.append((self.undo_compose, [composite, indexList])) return [composite] else: return gfList def decompose(self, gfList): compositeList = [] for gf in gfList: componentList = self.compositionVisitor.decompose(gf) if len(componentList) > 0: #if gf was decomposed compositeList.append( gf) #add the composite to the list of composites self.undoStack.append((self.undo_decompose, [compositeList])) def delete(self, gfList): indexList = [] for gf in gfList: gf.setCanvas(None) indexList.append(self.removeGF(gf)) pairList = map(None, gfList, indexList) self.undoStack.append((self.undo_delete, [pairList])) def getBoundingBox(self, gfList): boxes = [] for gf in gfList: if gf.getCanvas() != None: boxes.append(gf.getApproxBoundingBox()) if len(boxes) == 0: raise TypeError, "gfList contains no active GF" xMin = boxes[0][0] yMin = boxes[0][1] xMax = boxes[0][2] yMax = boxes[0][3] for box in boxes: if box[0] < xMin: xMin = box[0] if box[1] < yMin: yMin = box[1] if box[2] > xMax: xMax = box[2] if box[3] > yMax: yMax = box[3] return [xMin, yMin, xMax, yMax] def setFillColor(self, gfList, color): undoList = self.colorVisitor.setFillColor(gfList, color) if len(undoList) > 0: self.undoStack.append((self.undo_setFillColor, [undoList])) def setOutlineColor(self, gfList, color): undoList = self.colorVisitor.setOutlineColor(gfList, color) if len(undoList) > 0: self.undoStack.append((self.undo_setOutlineColor, [undoList])) def setLineWidth(self, gfList, lineWidth): undoList = self.widthVisitor.setWidth(gfList, lineWidth) if len(undoList) > 0: self.undoStack.append((self.undo_setLineWidth, [undoList])) def setOutlineFillOption(self, gfList, option): undoList = self.optionVisitor.changeOption(gfList, option) if len(undoList) > 0: self.undoStack.append((self.undo_setOutlineFillOption, [undoList])) def createRectangle(self, xy): gf = Graphics.Rectangle(xy[0], xy[1], xy[2], xy[3], canvas=self.canvas, outline=self.getOutlineColor(), outlineOption=self.hasOutline(), fill=self.getFillColor(), fillOption=self.hasFill(), width=self.getLineWidth(), zoom=self.getZoom(), eventHandler=self.mainHandler) self.appendGF(gf) self.undoStack.append((self.undo_create, [[gf]])) return gf def createOval(self, xy): gf = Graphics.Oval(xy[0], xy[1], xy[2], xy[3], canvas=self.canvas, outline=self.getOutlineColor(), outlineOption=self.hasOutline(), fill=self.getFillColor(), fillOption=self.hasFill(), width=self.getLineWidth(), zoom=self.getZoom(), eventHandler=self.mainHandler) self.appendGF(gf) self.undoStack.append((self.undo_create, [[gf]])) return gf def createLine(self, xy, smooth=0): gf = Graphics.Line(xy, canvas=self.canvas, fill=self.getFillColor(), width=self.getLineWidth(), zoom=self.getZoom(), eventHandler=self.mainHandler, smooth=smooth) self.appendGF(gf) self.undoStack.append((self.undo_create, [[gf]])) return gf def createPolygon(self, xy, smooth=0): gf = Graphics.Polygon(xy, canvas=self.canvas, outline=self.getOutlineColor(), outlineOption=self.hasOutline(), fill=self.getFillColor(), fillOption=self.hasFill(), width=self.getLineWidth(), zoom=self.getZoom(), eventHandler=self.mainHandler, smooth=smooth) self.appendGF(gf) self.undoStack.append((self.undo_create, [[gf]])) return gf def createConnector(self, xy): gf = Graphics.Connector(xy[0], xy[1], canvas=self.canvas, zoom=self.getZoom(), eventHandler=self.mainHandler) self.appendGF(gf) self.undoStack.append((self.undo_create, [[gf]])) return gf def createImage(self, xy, filename): gf = Graphics.Image(xy[0], xy[1], filename, canvas=self.canvas, zoom=self.getZoom(), eventHandler=self.mainHandler) self.appendGF(gf) self.undoStack.append((self.undo_create, [[gf]])) return gf def createText(self, xy, text): gf = Graphics.Text(xy[0], xy[1], canvas=self.canvas, zoom=self.getZoom(), eventHandler=self.mainHandler, text=text, fill=self.getFillColor()) self.appendGF(gf) self.undoStack.append((self.undo_create, [[gf]])) return gf def createNamedConnector(self, xy): gf = Graphics.NamedConnector(xy[0], xy[1], canvas=self.canvas, zoom=self.getZoom(), eventHandler=self.mainHandler) self.appendGF(gf) self.undoStack.append((self.undo_create, [[gf]])) return gf def createAttribute(self, xy, text): gf = Graphics.Attribute(xy[0], xy[1], canvas=self.canvas, zoom=self.getZoom(), eventHandler=self.mainHandler, text=text) self.appendGF(gf) self.undoStack.append((self.undo_create, [[gf]])) return gf def isUndoStackEmpty(self): if len(self.undoStack) == 0: return 1 else: return 0 def isClipboardEmpty(self): if len(self.clipboardList) == 0: return 1 else: return 0 def translate(self, gfList, dx, dy): for gf in gfList: gf.translate(dx, dy) self.undoStack.append((self.undo_translate, [gfList, -dx, -dy])) def rotate(self, gfList, centerX, centerY, angle): for gf in gfList: gf.rotate(centerX, centerY, angle) self.undoStack.append( (self.undo_rotate, [gfList, centerX, centerY, -angle])) def scale(self, gfList, centerX, centerY, factorX, factorY): for gf in gfList: gf.scale(centerX, centerY, factorX, factorY) self.undoStack.append( (self.undo_scale, [gfList, centerX, centerY, 1 / factorX, 1 / factorY])) # the following method is used by the translation handler to add an undo command. # Instead of creating an undo for each small translation (by using the normal translate method of the editor), # the handler calls the "add undo translation" method of the editor when the translation is complete. def addUndoTranslate(self, gfList, dx, dy): if (not (dx == 0 and dy == 0)): self.undoStack.append((self.undo_translate, [gfList, -dx, -dy])) # see addUndoTranslate def addUndoRotate(self, gfList, centerX, centerY, angle): self.undoStack.append( (self.undo_rotate, [gfList, centerX, centerY, -angle])) # see addUndoTranslate def addUndoScale(self, gfList, centerX, centerY, factorX, factorY): self.undoStack.append( (self.undo_scale, [gfList, centerX, centerY, 1 / factorX, 1 / factorY])) def addUndoSetCoords(self, gf, oldCoords): self.undoStack.append((self.undo_setCoords, [gf, oldCoords])) def undo(self): method, args = self.undoStack.pop() apply(method, args) ####################### # undo commands def undo_create(self, gfList): for gf in gfList: gf.setCanvas(None) self.removeGF(gf) def undo_delete(self, pairList): # pairList is a list of pairs, where each # pair contains an inactive GF and its position in the canvas. # pairList must be reversed because the index is valid # for the editor's list after the other gfs are deleted. pairList.reverse() for gf, index in pairList: gf.setCanvas(self.canvas) self.insertGF(index, gf) def undo_cut(self, previousClipboard, pairList): # pairList is a list of pairs, where each # pair contains an inactive GF and its position in the canvas. # pairList must be reversed because the index is valid # for the editor's list after the other gfs are deleted. pairList.reverse() for gf, index in pairList: gf.setCanvas(self.canvas) self.insertGF(index, gf) self.clipboardList = previousClipboard def undo_copy(self, previousClipboard): self.clipboardList = previousClipboard def undo_paste(self, pastedList): for gf in pastedList: gf.setCanvas(None) self.removeGF(gf) def undo_bringPush(self, pairList): pairList.reverse() for gf, index in pairList: self.removeGF(gf) self.insertGF(index, gf) def undo_compose(self, compositeGF, indexList): gfList = compositeGF.getComponents() self.removeGF(compositeGF) pairList = map(None, gfList, indexList) pairList.reverse() for gf, index in pairList: gf.setEventHandler(self.mainHandler) self.insertGF(index, gf) def undo_decompose(self, compositeList): for composite in compositeList: gfList = composite.getComponents() for gf in gfList: index = self.removeGF(gf) gf.setEventHandler(composite) self.insertGF(index, composite) def undo_translate(self, gfList, dx, dy): for gf in gfList: gf.translate(dx, dy) def undo_rotate(self, gfList, centerX, centerY, angle): for gf in gfList: gf.rotate(centerX, centerY, angle) def undo_scale(self, gfList, centerX, centerY, factorX, factorY): for gf in gfList: gf.scale(centerX, centerY, factorX, factorY) def undo_setCoords(self, gf, oldCoords): gf.setCoords(oldCoords) def undo_setFillColor(self, undoList): for gf, color in undoList: gf.setFillColor(color) def undo_setOutlineColor(self, undoList): for gf, color in undoList: gf.setOutlineColor(color) def undo_setLineWidth(self, undoList): for gf, width in undoList: gf.setWidth(width) def undo_setOutlineFillOption(self, undoList): for gf, (outlineOption, fillOption) in undoList: gf.setOutlineOption(outlineOption) gf.setFillOption(fillOption) ###################### def appendGF(self, gf): self.GFs.append(gf) def insertGF(self, index, gf): self.GFs.insert(index, gf) for gf in self.GFs: gf.bringToTop() def getIndex(self, gf): return self.GFs.index(gf) def removeGF(self, gf): index = self.GFs.index(gf) self.GFs.remove(gf) return index #return the index the gf had in the list (which was also its relative position on the canvas) # convention: the first gfs in the list are drawn first. # The order in which the GFs are drawn is useful when we want to save. def bringToTopGF(self, gf): gf.bringToTop() index = self.removeGF(gf) self.GFs.append(gf) return index def pushToBottomGF(self, gf): gf.pushToBottom() index = self.removeGF(gf) self.GFs.insert(0, gf) return index def getGFs(self): return self.GFs[:] def setGFs(self, gfList): self.GFs = gfList def getEventHandler(self): return self.mainHandler def getCanvasWidth(self): return self.workspace.getCanvasWidth() def getCanvasHeight(self): return self.workspace.getCanvasHeight() def getCanvas(self): return self.workspace.getCanvas() def getZoom(self): return self.workspace.getZoom() def setZoom(self, zoom, x, y): self.workspace.setZoom(zoom, x, y) # update statusBar and GFs to the new zoom (whether it changed or not) newZoom = self.workspace.getZoom() self.statusBar.setZoom(newZoom) for gf in self.GFs: gf.setZoom(newZoom) def setToolSelector(self, tool): self.toolSelector.set(tool) def getLineWidth(self): return self.lineWidthSelector.get() def getOutlineColor(self): return self.colorSelector.getOutlineColor() def getFillColor(self): return self.colorSelector.getFillColor() def hasOutline(self): option = self.outlineFillOptionSelector.get() return option[0] def hasFill(self): option = self.outlineFillOptionSelector.get() return option[1]