Sun, 09 Sep 2012 14:46:59 +0200
Implemented functions to save the UML graphics to disc.
--- a/Graphics/ApplicationDiagram.py Sat Sep 08 18:20:46 2012 +0200 +++ b/Graphics/ApplicationDiagram.py Sun Sep 09 14:46:59 2012 +0200 @@ -39,7 +39,8 @@ self.project = project self.noModules = noModules - UMLDialog.__init__(self, buildFunction=self.__buildPackages, parent=parent) + UMLDialog.__init__(self, "ApplicationDiagram", buildFunction=self.__buildPackages, + parent=parent) self.setDiagramName( self.trUtf8("Application Diagram {0}").format(project.getProjectName())) @@ -48,6 +49,9 @@ else: self.setObjectName(name) + self.umlView.setPersistenceData( + "project={0}".format(self.project.getProjectFile())) + self.umlView.relayout.connect(self.relayout) def __buildModulesDict(self): @@ -228,6 +232,7 @@ modules.sort() pm = PackageModel(name, modules) pw = PackageItem(pm, x, y, noModules=self.noModules, scene=self.scene) + pw.setId(self.umlView.getItemId()) return pw def __createAssociations(self, shapes):
--- a/Graphics/AssociationItem.py Sat Sep 08 18:20:46 2012 +0200 +++ b/Graphics/AssociationItem.py Sun Sep 09 14:46:59 2012 +0200 @@ -12,10 +12,14 @@ from E5Graphics.E5ArrowItem import E5ArrowItem, NormalArrow, WideArrow +import Utilities + + Normal = 0 Generalisation = 1 Imports = 2 + NoRegion = 0 West = 1 North = 2 @@ -77,6 +81,7 @@ self.itemA = itemA self.itemB = itemB self.assocType = type + self.topToBottom = topToBottom self.regionA = NoRegion self.regionB = NoRegion @@ -504,3 +509,48 @@ """ self.itemA.removeAssociation(self) self.itemB.removeAssociation(self) + + def buildAssociationItemDataString(self): + """ + Public method to build a string to persist the specific item data. + + This string should be built like "attribute=value" with pairs separated + by ", ". value must not contain ", " or newlines. + + @return persistence data (string) + """ + entries = [ + "src={0}".format(self.itemA.getId()), + "dst={0}".format(self.itemB.getId()), + "type={0}".format(self.assocType), + "topToBottom={0}".format(self.topToBottom) + ] + return ", ".join(entries) + + @classmethod + def parseAssociationItemDataString(cls, data): + """ + Class method to parse the given persistence data. + + @param data persisted data to be parsed (string) + @return tuple with the IDs of the source and destination items, + the association type and a flag indicating to associate from top + to bottom (integer, integer, integer, boolean) + """ + src = -1 + dst = -1 + assocType = Normal + topToBottom = False + for entry in data.split(", "): + if "=" in entry: + key, value = entry.split("=", 1) + if key == "src": + src = int(value) + elif key == "dst": + dst = int(value) + elif key == "type": + assocType = value + elif key == "topToBottom": + topToBottom = Utilities.toBool(value) + + return src, dst, assocType, topToBottom
--- a/Graphics/ClassItem.py Sat Sep 08 18:20:46 2012 +0200 +++ b/Graphics/ClassItem.py Sun Sep 09 14:46:59 2012 +0200 @@ -215,3 +215,36 @@ @return external state (boolean) """ return self.external + + def buildItemDataString(self): + """ + Public method to build a string to persist the specific item data. + + This string must start with ", " and should be built like + "attribute=value" with pairs separated by ", ". value must not contain ", " + or newlines. + + @return persistence data (string) + """ + entries = [ + "item_type=class", + "is_external={0}".format(self.external), + "no_attributes={0}".format(self.noAttrs), + "name={0}".format(self.model.getName()), + ] + attributes = self.model.getAttributes() + if attributes: + entries.append("attributes={0}".format("||".join(attributes))) + methods = self.model.getMethods() + if methods: + entries.append("methods={0}".format("||".join(methods))) + + return ", " + ", ".join(entries) + + def parseItemDataString(self, data): + """ + Public method to parse the given persistence data. + + @param data persisted data to be parsed (string) + """ + # TODO: implement this
--- a/Graphics/ImportsDiagram.py Sat Sep 08 18:20:46 2012 +0200 +++ b/Graphics/ImportsDiagram.py Sun Sep 09 14:46:59 2012 +0200 @@ -41,7 +41,8 @@ @keyparam showExternalImports flag indicating to show exports from outside the package (boolean) """ - UMLDialog.__init__(self, buildFunction=self.__buildImports, parent=parent) + UMLDialog.__init__(self, "ImportsDiagram", buildFunction=self.__buildImports, + parent=parent) self.showExternalImports = showExternalImports self.packagePath = Utilities.normabspath(package) @@ -53,6 +54,8 @@ hasInit = len(glob.glob(os.path.join(ppath, '__init__.*'))) > 0 self.shortPackage = self.packagePath.replace(ppath, '').replace(os.sep, '.')[1:] + self.umlView.setPersistenceData("package={0}".format(self.packagePath)) + pname = project.getProjectName() if pname: name = self.trUtf8("Imports Diagramm {0}: {1}").format( @@ -226,6 +229,7 @@ classes.sort() impM = ModuleModel(name, classes) impW = ModuleItem(impM, x, y, scene=self.scene) + impW.setId(self.umlView.getItemId()) return impW def __createAssociations(self, shapes):
--- a/Graphics/ModuleItem.py Sat Sep 08 18:20:46 2012 +0200 +++ b/Graphics/ModuleItem.py Sun Sep 09 14:46:59 2012 +0200 @@ -161,3 +161,31 @@ painter.drawLine(offsetX, offsetY + y, offsetX + w - 1, offsetY + y) self.adjustAssociations() + + def buildItemDataString(self): + """ + Public method to build a string to persist the specific item data. + + This string must start with ", " and should be built like + "attribute=value" with pairs separated by ", ". value must not contain ", " + or newlines. + + @return persistence data (string) + """ + entries = [ + "item_type=module", + "name={0}".format(self.model.getName()), + ] + classes = self.model.getClasses() + if classes: + entries.append("classes={0}".format("||".join(classes))) + + return ", " + ", ".join(entries) + + def parseItemDataString(self, data): + """ + Public method to parse the given persistence data. + + @param data persisted data to be parsed (string) + """ + # TODO: implement this
--- a/Graphics/PackageDiagram.py Sat Sep 08 18:20:46 2012 +0200 +++ b/Graphics/PackageDiagram.py Sun Sep 09 14:46:59 2012 +0200 @@ -37,12 +37,15 @@ @param name name of the view widget (string) @keyparam noAttrs flag indicating, that no attributes should be shown (boolean) """ - UMLDialog.__init__(self, buildFunction=self.__buildClasses, parent=parent) + UMLDialog.__init__(self, "PackageDiagram", buildFunction=self.__buildClasses, + parent=parent) self.package = Utilities.normabspath(package) self.allClasses = {} self.noAttrs = noAttrs + self.umlView.setPersistenceData("package={0}".format(self.package)) + pname = project.getProjectName() if pname: name = self.trUtf8("Package Diagram {0}: {1}").format( @@ -281,6 +284,7 @@ name = "{0} (Module)".format(name) cl = ClassModel(name, meths[:], attrs[:]) cw = ClassItem(cl, False, x, y, noAttrs=self.noAttrs, scene=self.scene) + cw.setId(self.umlView.getItemId()) self.allClasses[className] = cw def __addExternalClass(self, _class, x, y): @@ -296,6 +300,7 @@ """ cl = ClassModel(_class) cw = ClassItem(cl, True, x, y, noAttrs=self.noAttrs, scene=self.scene) + cw.setId(self.umlView.getItemId()) self.allClasses[_class] = cw def __createAssociations(self, routes):
--- a/Graphics/PackageItem.py Sat Sep 08 18:20:46 2012 +0200 +++ b/Graphics/PackageItem.py Sun Sep 09 14:46:59 2012 +0200 @@ -180,3 +180,32 @@ painter.drawLine(offsetX, offsetY + y, offsetX + w - 1, offsetY + y) self.adjustAssociations() + + def buildItemDataString(self): + """ + Public method to build a string to persist the specific item data. + + This string must start with ", " and should be built like + "attribute=value" with pairs separated by ", ". value must not contain ", " + or newlines. + + @return persistence data (string) + """ + entries = [ + "item_type=package", + "no_modules={0}".format(self.noModules), + "name={0}".format(self.model.getName()), + ] + modules = self.model.getModules() + if modules: + entries.append("modules={0}".format("||".join(modules))) + + return ", " + ", ".join(entries) + + def parseItemDataString(self, data): + """ + Public method to parse the given persistence data. + + @param data persisted data to be parsed (string) + """ + # TODO: implement this
--- a/Graphics/UMLClassDiagram.py Sat Sep 08 18:20:46 2012 +0200 +++ b/Graphics/UMLClassDiagram.py Sun Sep 09 14:46:59 2012 +0200 @@ -34,11 +34,14 @@ @param name name of the view widget (string) @keyparam noAttrs flag indicating, that no attributes should be shown (boolean) """ - UMLDialog.__init__(self, buildFunction=self.__buildClasses, parent=parent) + UMLDialog.__init__(self, "UMLClassDiagram", buildFunction=self.__buildClasses, + parent=parent) self.file = file self.noAttrs = noAttrs + self.umlView.setPersistenceData("file={0}".format(file)) + pname = project.getProjectName() if pname and project.isProjectSource(self.file): name = self.trUtf8("Class Diagram {0}: {1}").format( @@ -231,6 +234,7 @@ name = "{0} (Module)".format(name) cl = ClassModel(name, meths[:], attrs[:]) cw = ClassItem(cl, False, x, y, noAttrs=self.noAttrs, scene=self.scene) + cw.setId(self.umlView.getItemId()) self.allClasses[className] = cw if _class.name not in self.allModules[self.file]: self.allModules[self.file].append(_class.name) @@ -248,6 +252,7 @@ """ cl = ClassModel(_class) cw = ClassItem(cl, True, x, y, noAttrs=self.noAttrs, scene=self.scene) + cw.setId(self.umlView.getItemId()) self.allClasses[_class] = cw if _class not in self.allModules[self.file]: self.allModules[self.file].append(_class)
--- a/Graphics/UMLDialog.py Sat Sep 08 18:20:46 2012 +0200 +++ b/Graphics/UMLDialog.py Sun Sep 09 14:46:59 2012 +0200 @@ -20,12 +20,14 @@ """ Class implementing a dialog showing UML like diagrams. """ - def __init__(self, buildFunction=None, diagramName="Unnamed", parent=None, name=None): + def __init__(self, diagramType, diagramName="Unnamed", buildFunction=None, + parent=None, name=""): """ Constructor + @param diagramType type of the diagram (string) + @param diagramName name of the diagram (string) @param buildFunction function to build the diagram contents (function) - @param diagramName name of the diagram (string) @param parent parent widget of the view (QWidget) @param name name of the view widget (string) """ @@ -38,7 +40,8 @@ self.buildFunction = buildFunction self.scene = QGraphicsScene(0.0, 0.0, 800.0, 600.0) - self.umlView = UMLGraphicsView(self.scene, diagramName, self, "umlView") + self.umlView = UMLGraphicsView(self.scene, diagramType, diagramName, + self, "umlView") self.closeAct = \ QAction(UI.PixmapCache.getIcon("close.png"),
--- a/Graphics/UMLGraphicsView.py Sat Sep 08 18:20:46 2012 +0200 +++ b/Graphics/UMLGraphicsView.py Sun Sep 09 14:46:59 2012 +0200 @@ -15,6 +15,7 @@ from E5Gui import E5MessageBox, E5FileDialog from .UMLItem import UMLItem +from .AssociationItem import AssociationItem from .UMLSceneSizeDialog import UMLSceneSizeDialog from .ZoomDialog import ZoomDialog @@ -33,11 +34,13 @@ """ relayout = pyqtSignal() - def __init__(self, scene, diagramName="Unnamed", parent=None, name=None): + def __init__(self, scene, diagramType, diagramName="Unnamed", parent=None, + name=None): """ Constructor @param scene reference to the scene object (QGraphicsScene) + @param diagramType type of the diagram (string) @param diagramName name of the diagram (string) @param parent parent widget of the view (QWidget) @param name name of the view widget (string) @@ -48,6 +51,11 @@ self.setViewportUpdateMode(QGraphicsView.FullViewportUpdate) self.diagramName = diagramName + self.diagramType = diagramType + + self.persistenceData = "" + self.__fileName = "" + self.__itemId = -1 self.border = 10 self.deltaSize = 100.0 @@ -72,8 +80,18 @@ self.saveAct = \ QAction(UI.PixmapCache.getIcon("fileSave.png"), + self.trUtf8("Save"), self) + self.saveAct.triggered[()].connect(self.__save) + + self.saveAsAct = \ + QAction(UI.PixmapCache.getIcon("fileSaveAs.png"), + self.trUtf8("Save As..."), self) + self.saveAsAct.triggered[()].connect(self.__saveAs) + + self.saveImageAct = \ + QAction(UI.PixmapCache.getIcon("fileSavePixmap.png"), self.trUtf8("Save as PNG"), self) - self.saveAct.triggered[()].connect(self.__saveImage) + self.saveImageAct.triggered[()].connect(self.__saveImage) self.printAct = \ QAction(UI.PixmapCache.getIcon("print.png"), @@ -225,6 +243,8 @@ toolBar.addAction(self.deleteShapeAct) toolBar.addSeparator() toolBar.addAction(self.saveAct) + toolBar.addAction(self.saveAsAct) + toolBar.addAction(self.saveImageAct) toolBar.addSeparator() toolBar.addAction(self.printPreviewAct) toolBar.addAction(self.printAct) @@ -251,15 +271,16 @@ return toolBar - def filteredItems(self, items): + def filteredItems(self, items, itemType=UMLItem): """ Public method to filter a list of items. @param items list of items as returned by the scene object (QGraphicsItem) + @param itemType type to be filtered (class) @return list of interesting collision items (QGraphicsItem) """ - return [itm for itm in items if isinstance(itm, UMLItem)] + return [itm for itm in items if isinstance(itm, itemType)] def selectItems(self, items): """ @@ -383,6 +404,7 @@ """ Private method to handle the re-layout context menu entry. """ + self.__itemId = -1 self.scene().clear() self.relayout.emit() @@ -569,3 +591,109 @@ else: self.setZoom(pinch.scaleFactor()) evt.accept() + + def setPersistenceData(self, data): + """ + Public method to set additional persistence data. + + @param data string of additional data to be made persistent (string) + """ + self.persistenceData = data + + def getPersistenceData(self): + """ + Public method to get the additional persistence data. + + @return additional persistence data (string) + """ + return self.persistenceData + + def getItemId(self): + """ + Public method to get the ID to be assigned to an item. + + @return item ID (integer) + """ + self.__itemId += 1 + return self.__itemId + + def findItem(self, id): + """ + Public method to find an UML item based on the ID. + + @param id of the item to search for (integer) + @return item found (UMLItem) or None + """ + for item in self.scene().items(): + try: + itemID = item.getId() + except AttributeError: + continue + + if itemID == id: + return item + + return None + + def __save(self): + """ + Private slot to save the diagram with the current name. + """ + self.__saveAs(self.__fileName) + + def __saveAs(self, filename=""): + """ + Private slot to save the diagram. + + @param filename name of the file to write to (string) + """ + if not filename: + fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter( + self, + self.trUtf8("Save Diagram"), + "", + self.trUtf8("Eric5 Graphics File (*.e5g);;All Files (*)"), + "", + E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite)) + if not fname: + return + ext = QFileInfo(fname).suffix() + if not ext: + ex = selectedFilter.split("(*")[1].split(")")[0] + if ex: + fname += ex + if QFileInfo(fname).exists(): + res = E5MessageBox.yesNo(self, + self.trUtf8("Save Diagram"), + self.trUtf8("<p>The file <b>{0}</b> already exists." + " Overwrite it?</p>").format(fname), + icon=E5MessageBox.Warning) + if not res: + return + filename = fname + + lines = [ + "version: 1.0", + "diagram_type: {0}".format(self.diagramType), + "diagram_name: {0}".format(self.diagramName), + "scene_size: {0};{1}".format(self.scene().width(), self.scene().height()), + ] + if self.persistenceData: + lines.append("diagram_data: {0}".format(self.persistenceData)) + for item in self.filteredItems(self.scene().items(), UMLItem): + lines.append("item: id={0}, x={1}, y={2}{3}".format( + item.getId(), item.x(), item.y(), item.buildItemDataString())) + for item in self.filteredItems(self.scene().items(), AssociationItem): + lines.append("association: {0}".format(item.buildAssociationItemDataString())) + + try: + f = open(filename, "w", encoding="utf-8") + f.write("\n".join(lines)) + f.close() + except (IOError, OSError) as err: + E5MessageBox.critical(self, + self.trUtf8("Save Diagram"), + self.trUtf8("""<p>The file <b>{0}</b> could not be saved.</p>""" + """<p>Reason: {1}</p>""").format(fname, str(err))) + + self.__fileName = filename
--- a/Graphics/UMLItem.py Sat Sep 08 18:20:46 2012 +0200 +++ b/Graphics/UMLItem.py Sun Sep 09 14:46:59 2012 +0200 @@ -31,6 +31,7 @@ self.margin = 5 self.associations = [] self.shouldAdjustAssociations = False + self.__id = -1 self.setRect(x, y, 60, 30) @@ -144,3 +145,39 @@ painter.setBrush(self.brush()) painter.drawRect(self.rect()) self.adjustAssociations() + + def setId(self, id): + """ + Public method to assign an ID to the item. + + @param id assigned ID (integer) + """ + self.__id = id + + def getId(self): + """ + Public method to get the item ID. + + @return ID of the item (integer) + """ + return self.__id + + def buildItemDataString(self): + """ + Public method to build a string to persist the specific item data. + + This string must start with ", " and should be built like + "attribute=value" with pairs separated by ", ". value must not contain ", " + or newlines. + + @return persistence data (string) + """ + return "" + + def parseItemDataString(self, data): + """ + Public method to parse the given persistence data. + + @param data persisted data to be parsed (string) + """ + pass
--- a/Utilities/__init__.py Sat Sep 08 18:20:46 2012 +0200 +++ b/Utilities/__init__.py Sun Sep 09 14:46:59 2012 +0200 @@ -1678,6 +1678,21 @@ return True + +def toBool(dataStr): + """ + Module function to convert a string to a boolean value. + + @param dataStr string to be converted (string) + @return converted boolean value (boolean) + """ + if dataStr in [ "True", "true", "1", "Yes", "yes"]: + return True + elif dataStr in ["False", "false", "0", "No", "no"]: + return False + else: + return bool(dataStr) + ################################################################################ # posix compatibility functions below ################################################################################