UML Diagrams

Sat, 01 May 2021 18:48:35 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sat, 01 May 2021 18:48:35 +0200
changeset 8277
ea734702ae94
parent 8276
1436fd09d1e1
child 8278
e647b71b393f

UML Diagrams
- extended the class items to show class attributes

docs/changelog file | annotate | diff | comparison | revisions
eric6/E5Graphics/E5GraphicsView.py file | annotate | diff | comparison | revisions
eric6/Graphics/ClassItem.py file | annotate | diff | comparison | revisions
eric6/Graphics/ImportsDiagramBuilder.py file | annotate | diff | comparison | revisions
eric6/Graphics/PackageDiagramBuilder.py file | annotate | diff | comparison | revisions
eric6/Graphics/UMLClassDiagramBuilder.py file | annotate | diff | comparison | revisions
eric6/Project/ProjectSourcesBrowser.py file | annotate | diff | comparison | revisions
eric6/QScintilla/Editor.py file | annotate | diff | comparison | revisions
--- a/docs/changelog	Sat May 01 16:01:43 2021 +0200
+++ b/docs/changelog	Sat May 01 18:48:35 2021 +0200
@@ -2,6 +2,8 @@
 ----------
 Version 21.6:
 - bug fixes
+- UML Diagrams
+  -- extended the class items to show class attributes
 
 Version 21.5:
 - bug fixes
--- a/eric6/E5Graphics/E5GraphicsView.py	Sat May 01 16:01:43 2021 +0200
+++ b/eric6/E5Graphics/E5GraphicsView.py	Sat May 01 18:48:35 2021 +0200
@@ -283,10 +283,14 @@
         Private method to retrieve the diagram from the scene fitting it
         in the minimum rectangle.
         
-        @param rect minimum rectangle fitting the diagram (QRectF)
-        @param imageFormat format for the image file (string)
-        @param filename name of the file for non pixmaps (string)
-        @return diagram pixmap to receive the diagram (QPixmap)
+        @param rect minimum rectangle fitting the diagram
+        @type QRectF
+        @param imageFormat format for the image file
+        @type str
+        @param filename name of the file for non pixmaps
+        str
+        @return paint device containing the diagram
+        @rtype QPixmap or QSvgGenerator
         """
         selectedItems = self.scene().selectedItems()
         
--- a/eric6/Graphics/ClassItem.py	Sat May 01 16:01:43 2021 +0200
+++ b/eric6/Graphics/ClassItem.py	Sat May 01 18:48:35 2021 +0200
@@ -7,6 +7,7 @@
 Module implementing an UML like class item.
 """
 
+from PyQt5.QtCore import QCoreApplication
 from PyQt5.QtGui import QFont
 from PyQt5.QtWidgets import QGraphicsSimpleTextItem, QStyle
 
@@ -19,53 +20,88 @@
     """
     Class implementing the class model.
     """
-    def __init__(self, name, methods=None, attributes=None):
+    def __init__(self, name, methods=None, instanceAttributes=None,
+                 classAttributes=None):
         """
         Constructor
         
-        @param name the class name (string)
+        @param name the class name
+        @type str
         @param methods list of method names of the class
-            (list of strings)
-        @param attributes list of attribute names of the class
-            (list of strings)
+        @type list of str
+        @param instanceAttributes list of instance attribute names of the class
+        @type list of str
+        @param classAttributes list of class attribute names of the class
+        @type list of str
         """
         super().__init__(name)
         
         self.methods = [] if methods is None else methods[:]
-        self.attributes = [] if attributes is None else attributes[:]
+        self.instanceAttributes = (
+            []
+            if instanceAttributes is None else
+            instanceAttributes[:]
+        )
+        self.classAttributes = (
+            []
+            if classAttributes is None else
+            classAttributes[:]
+        )
     
     def addMethod(self, method):
         """
         Public method to add a method to the class model.
         
-        @param method method name to be added (string)
+        @param method method name to be added
+        @type str
         """
         self.methods.append(method)
     
-    def addAttribute(self, attribute):
+    def addInstanceAttribute(self, attribute):
         """
-        Public method to add an attribute to the class model.
+        Public method to add an instance attribute to the class model.
         
-        @param attribute attribute name to be added (string)
+        @param attribute instance attribute name to be added
+        @type str
+        """
+        self.instanceAttributes.append(attribute)
+    
+    def addClassAttribute(self, attribute):
         """
-        self.attributes.append(attribute)
+        Public method to add a class attribute to the class model.
+        
+        @param attribute class attribute name to be added
+        @type str
+        """
+        self.classAttributes.append(attribute)
     
     def getMethods(self):
         """
         Public method to retrieve the methods of the class.
         
-        @return list of class methods (list of strings)
+        @return list of class methods
+        @type list of str
         """
         return self.methods[:]
     
-    def getAttributes(self):
+    def getInstanceAttributes(self):
         """
         Public method to retrieve the attributes of the class.
         
-        @return list of class attributes (list of strings)
+        @return list of instance attributes
+        @type list of str
+        """
+        return self.instanceAttributes[:]
+    
+    def getClassAttributes(self):
         """
-        return self.attributes[:]
-    
+        Public method to retrieve the global attributes of the class.
+        
+        @return list of class attributes
+        @type list of str
+        """
+        return self.classAttributes[:]
+
 
 class ClassItem(UMLItem):
     """
@@ -118,8 +154,10 @@
         
         boldFont = QFont(self.font)
         boldFont.setBold(True)
+        boldFont.setUnderline(True)
         
-        attrs = self.model.getAttributes()
+        classAttributes = self.model.getClassAttributes()
+        attrs = self.model.getInstanceAttributes()
         meths = self.model.getMethods()
         
         x = self.margin + self.rect().x()
@@ -130,13 +168,32 @@
         self.header.setText(self.model.getName())
         self.header.setPos(x, y)
         y += self.header.boundingRect().height() + self.margin
+        
+        if self.external:
+            self.classAttributes = None
+        else:
+            txt = QCoreApplication.translate(
+                "ClassItem", "Class Attributes:\n  ")
+            txt += (
+                "\n  ".join(classAttributes)
+                if globals else
+                "  " + QCoreApplication.translate("ClassItem", "none")
+            )
+            self.classAttributes = QGraphicsSimpleTextItem(self)
+            self.classAttributes.setBrush(self._colors[0])
+            self.classAttributes.setFont(self.font)
+            self.classAttributes.setText(txt)
+            self.classAttributes.setPos(x, y)
+            y += self.classAttributes.boundingRect().height() + self.margin
+        
         if not self.noAttrs and not self.external:
-            # TODO: add class attributes next to the instance attributes
-            #       separate class and instance attributes by dashed line
-            if attrs:
-                txt = "\n".join(attrs)
-            else:
-                txt = " "
+            txt = QCoreApplication.translate(
+                "ClassItem", "Instance Attributes:\n  ")
+            txt += (
+                "\n  ".join(attrs)
+                if attrs else
+                "  " + QCoreApplication.translate("ClassItem", "none")
+            )
             self.attrs = QGraphicsSimpleTextItem(self)
             self.attrs.setBrush(self._colors[0])
             self.attrs.setFont(self.font)
@@ -145,7 +202,16 @@
             y += self.attrs.boundingRect().height() + self.margin
         else:
             self.attrs = None
-        txt = "\n".join(meths) if meths else " "
+        
+        if self.external:
+            txt = " "
+        else:
+            txt = QCoreApplication.translate("ClassItem", "Methods:\n  ")
+            txt += (
+                "\n  ".join(meths)
+                if meths else
+                "  " + QCoreApplication.translate("ClassItem", "none")
+            )
         self.meths = QGraphicsSimpleTextItem(self)
         self.meths.setBrush(self._colors[0])
         self.meths.setFont(self.font)
@@ -161,12 +227,16 @@
         
         width = self.header.boundingRect().width()
         height = self.header.boundingRect().height()
+        if self.classAttributes:
+            width = max(width, self.classAttributes.boundingRect().width())
+            height += self.classAttributes.boundingRect().height() + self.margin
         if self.attrs:
             width = max(width, self.attrs.boundingRect().width())
             height = height + self.attrs.boundingRect().height() + self.margin
         if self.meths:
             width = max(width, self.meths.boundingRect().width())
             height += self.meths.boundingRect().height()
+        
         self.setSize(width + 2 * self.margin, height + 2 * self.margin)
         
     def setModel(self, model):
@@ -177,6 +247,9 @@
         """
         self.scene().removeItem(self.header)
         self.header = None
+        if self.classAttributes:
+            self.scene().removeItem(self.classAttributes)
+            self.classAttributes = None
         if self.attrs:
             self.scene().removeItem(self.attrs)
             self.attrs = None
@@ -216,6 +289,10 @@
         painter.drawRect(offsetX, offsetY, w, h)
         y = self.margin + self.header.boundingRect().height()
         painter.drawLine(offsetX, offsetY + y, offsetX + w - 1, offsetY + y)
+        if self.classAttributes:
+            y += self.margin + self.classAttributes.boundingRect().height()
+            painter.drawLine(offsetX, offsetY + y,
+                             offsetX + w - 1, offsetY + y)
         if self.attrs:
             y += self.margin + self.attrs.boundingRect().height()
             painter.drawLine(offsetX, offsetY + y,
@@ -246,12 +323,18 @@
             "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)))
+        instanceAttributes = self.model.getInstanceAttributes()
+        if instanceAttributes:
+            entries.append("attributes={0}".format(
+                "||".join(instanceAttributes)))
         methods = self.model.getMethods()
         if methods:
-            entries.append("methods={0}".format("||".join(methods)))
+            entries.append("methods={0}".format(
+                "||".join(methods)))
+        classAttributes = self.model.getClassAttributes()
+        if classAttributes:
+            entries.append("class_attributes={0}".format(
+                "||".join(classAttributes)))
         
         return ", " + ", ".join(entries)
     
@@ -268,8 +351,9 @@
             return False
         
         name = ""
-        attributes = []
+        instanceAttributes = []
         methods = []
+        classAttributes = []
         
         for part in parts:
             key, value = part.split("=", 1)
@@ -280,13 +364,16 @@
             elif key == "name":
                 name = value.strip()
             elif key == "attributes":
-                attributes = value.strip().split("||")
+                instanceAttributes = value.strip().split("||")
             elif key == "methods":
                 methods = value.strip().split("||")
+            elif key == "class_attributes":
+                classAttributes = value.strip().split("||")
             else:
                 return False
         
-        self.model = ClassModel(name, methods, attributes)
+        self.model = ClassModel(name, methods, instanceAttributes,
+                                classAttributes)
         self.__createTexts()
         self.__calculateSize()
         
--- a/eric6/Graphics/ImportsDiagramBuilder.py	Sat May 01 16:01:43 2021 +0200
+++ b/eric6/Graphics/ImportsDiagramBuilder.py	Sat May 01 18:48:35 2021 +0200
@@ -135,39 +135,41 @@
         sceneRect = self.umlView.sceneRect()
         
         modules = self.__buildModulesDict()
-        sortedkeys = sorted(modules.keys())
         externalMods = []
         packageList = self.shortPackage.split('.')
         packageListLen = len(packageList)
-        for module in sortedkeys:
+        for module in sorted(modules.keys()):
             impLst = []
-            for i in modules[module].imports:
-                n = (i[len(self.package) + 1:]
-                     if i.startswith(self.package) else i)
-                if i in modules:
+            for importName in modules[module].imports:
+                n = (
+                    importName[len(self.package) + 1:]
+                    if importName.startswith(self.package) else
+                    importName
+                )
+                if importName in modules:
                     impLst.append(n)
                 elif self.showExternalImports:
                     impLst.append(n)
                     if n not in externalMods:
                         externalMods.append(n)
-            for i in list(modules[module].from_imports.keys()):
-                if i.startswith('.'):
-                    dots = len(i) - len(i.lstrip('.'))
+            for importName in list(modules[module].from_imports.keys()):
+                if importName.startswith('.'):
+                    dots = len(importName) - len(importName.lstrip('.'))
                     if dots == 1:
-                        n = i[1:]
-                        i = n
+                        n = importName[1:]
+                        importName = n
                     else:
                         if self.showExternalImports:
                             n = '.'.join(
                                 packageList[:packageListLen - dots + 1] +
-                                [i[dots:]])
+                                [importName[dots:]])
                         else:
-                            n = i
-                elif i.startswith(self.package):
-                    n = i[len(self.package) + 1:]
+                            n = importName
+                elif importName.startswith(self.package):
+                    n = importName[len(self.package) + 1:]
                 else:
-                    n = i
-                if i in modules:
+                    n = importName
+                if importName in modules:
                     impLst.append(n)
                 elif self.showExternalImports:
                     impLst.append(n)
--- a/eric6/Graphics/PackageDiagramBuilder.py	Sat May 01 16:01:43 2021 +0200
+++ b/eric6/Graphics/PackageDiagramBuilder.py	Sat May 01 18:48:35 2021 +0200
@@ -378,12 +378,15 @@
         @param isRbModule flag indicating a Ruby module (boolean)
         """
         from .ClassItem import ClassItem, ClassModel
-        meths = sorted(_class.methods.keys())
-        attrs = sorted(_class.attributes.keys())
         name = _class.name
         if isRbModule:
             name = "{0} (Module)".format(name)
-        cl = ClassModel(name, meths[:], attrs[:])
+        cl = ClassModel(
+            name,
+            sorted(_class.methods.keys())[:],
+            sorted(_class.attributes.keys())[:],
+            sorted(_class.globals.keys())[:]
+        )
         cw = ClassItem(cl, False, x, y, noAttrs=self.noAttrs, scene=self.scene,
                        colors=self.umlView.getDrawingColors())
         cw.setId(self.umlView.getItemId())
--- a/eric6/Graphics/UMLClassDiagramBuilder.py	Sat May 01 16:01:43 2021 +0200
+++ b/eric6/Graphics/UMLClassDiagramBuilder.py	Sat May 01 18:48:35 2021 +0200
@@ -234,12 +234,15 @@
         @param isRbModule flag indicating a Ruby module (boolean)
         """
         from .ClassItem import ClassItem, ClassModel
-        meths = sorted(_class.methods.keys())
-        attrs = sorted(_class.attributes.keys())
         name = _class.name
         if isRbModule:
             name = "{0} (Module)".format(name)
-        cl = ClassModel(name, meths[:], attrs[:])
+        cl = ClassModel(
+            name,
+            sorted(_class.methods.keys())[:],
+            sorted(_class.attributes.keys())[:],
+            sorted(_class.globals.keys())[:]
+        )
         cw = ClassItem(cl, False, x, y, noAttrs=self.noAttrs, scene=self.scene,
                        colors=self.umlView.getDrawingColors())
         cw.setId(self.umlView.getItemId())
--- a/eric6/Project/ProjectSourcesBrowser.py	Sat May 01 16:01:43 2021 +0200
+++ b/eric6/Project/ProjectSourcesBrowser.py	Sat May 01 18:48:35 2021 +0200
@@ -123,7 +123,6 @@
             self.tr('Profile data...'), self.__showProfileData)
         self.menuShow.aboutToShow.connect(self.__showContextMenuShow)
         
-        # TODO: differentiate between file and directory/package
         self.graphicsMenu = QMenu(self.tr('Diagrams'))
         self.classDiagramAction = self.graphicsMenu.addAction(
             self.tr("Class Diagram..."), self.__showClassDiagram)
@@ -1094,7 +1093,7 @@
             fn = itm.fileName()
         except AttributeError:
             fn = itm.dirName()
-        package = os.path.isdir(fn) and fn or os.path.dirname(fn)
+        package = fn if os.path.isdir(fn) else os.path.dirname(fn)
         res = E5MessageBox.yesNo(
             self,
             self.tr("Imports Diagram"),
@@ -1115,7 +1114,7 @@
             fn = itm.fileName()
         except AttributeError:
             fn = itm.dirName()
-        package = os.path.isdir(fn) and fn or os.path.dirname(fn)
+        package = fn if os.path.isdir(fn) else os.path.dirname(fn)
         res = E5MessageBox.yesNo(
             self,
             self.tr("Package Diagram"),
--- a/eric6/QScintilla/Editor.py	Sat May 01 16:01:43 2021 +0200
+++ b/eric6/QScintilla/Editor.py	Sat May 01 18:48:35 2021 +0200
@@ -7595,10 +7595,7 @@
         if not self.checkDirty():
             return
         
-        package = (
-            os.path.isdir(self.fileName) and
-            self.fileName or os.path.dirname(self.fileName)
-        )
+        package = os.path.dirname(self.fileName)
         res = E5MessageBox.yesNo(
             self,
             self.tr("Imports Diagram"),

eric ide

mercurial