eric7/Graphics/UMLGraphicsView.py

Sat, 15 May 2021 18:45:04 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sat, 15 May 2021 18:45:04 +0200
branch
eric7
changeset 8312
800c432b34c8
parent 8295
eric6/Graphics/UMLGraphicsView.py@3f5e8b0a338e
child 8318
962bce857696
permissions
-rw-r--r--

Started to rename eric6 to eric7.

# -*- coding: utf-8 -*-

# Copyright (c) 2007 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing a subclass of E5GraphicsView for our diagrams.
"""

from PyQt5.QtCore import (
    pyqtSignal, Qt, QSignalMapper, QFileInfo, QEvent, QRectF
)
from PyQt5.QtWidgets import QGraphicsView, QAction, QToolBar, QDialog
from PyQt5.QtPrintSupport import QPrinter, QPrintDialog

from E5Graphics.E5GraphicsView import E5GraphicsView

from E5Gui import E5MessageBox, E5FileDialog
from E5Gui.E5ZoomWidget import E5ZoomWidget

from .UMLItem import UMLItem

import UI.Config
import UI.PixmapCache

import Preferences


class UMLGraphicsView(E5GraphicsView):
    """
    Class implementing a specialized E5GraphicsView for our diagrams.
    
    @signal relayout() emitted to indicate a relayout of the diagram
        is requested
    """
    relayout = pyqtSignal()
    
    def __init__(self, scene, parent=None):
        """
        Constructor
        
        @param scene reference to the scene object
        @type QGraphicsScene
        @param parent parent widget of the view
        @type QWidget
        """
        E5GraphicsView.__init__(self, scene, parent)
        self.setObjectName("UMLGraphicsView")
        self.setViewportUpdateMode(
            QGraphicsView.ViewportUpdateMode.FullViewportUpdate)
        
        self.diagramName = "Unnamed"
        self.__itemId = -1
        
        self.border = 10
        self.deltaSize = 100.0
        
        self.__zoomWidget = E5ZoomWidget(
            UI.PixmapCache.getPixmap("zoomOut"),
            UI.PixmapCache.getPixmap("zoomIn"),
            UI.PixmapCache.getPixmap("zoomReset"), self)
        parent.statusBar().addPermanentWidget(self.__zoomWidget)
        self.__zoomWidget.setMapping(
            E5GraphicsView.ZoomLevels, E5GraphicsView.ZoomLevelDefault)
        self.__zoomWidget.valueChanged.connect(self.setZoom)
        self.zoomValueChanged.connect(self.__zoomWidget.setValue)
        
        self.__initActions()
        
        scene.changed.connect(self.__sceneChanged)
        
        self.grabGesture(Qt.GestureType.PinchGesture)
        
    def __initActions(self):
        """
        Private method to initialize the view actions.
        """
        self.alignMapper = QSignalMapper(self)
        try:
            self.alignMapper.mappedInt.connect(self.__alignShapes)
        except AttributeError:
            # pre Qt 5.15
            self.alignMapper.mapped[int].connect(self.__alignShapes)
        
        self.deleteShapeAct = QAction(
            UI.PixmapCache.getIcon("deleteShape"),
            self.tr("Delete shapes"), self)
        self.deleteShapeAct.triggered.connect(self.__deleteShape)
        
        self.incWidthAct = QAction(
            UI.PixmapCache.getIcon("sceneWidthInc"),
            self.tr("Increase width by {0} points").format(
                self.deltaSize),
            self)
        self.incWidthAct.triggered.connect(self.__incWidth)
        
        self.incHeightAct = QAction(
            UI.PixmapCache.getIcon("sceneHeightInc"),
            self.tr("Increase height by {0} points").format(
                self.deltaSize),
            self)
        self.incHeightAct.triggered.connect(self.__incHeight)
        
        self.decWidthAct = QAction(
            UI.PixmapCache.getIcon("sceneWidthDec"),
            self.tr("Decrease width by {0} points").format(
                self.deltaSize),
            self)
        self.decWidthAct.triggered.connect(self.__decWidth)
        
        self.decHeightAct = QAction(
            UI.PixmapCache.getIcon("sceneHeightDec"),
            self.tr("Decrease height by {0} points").format(
                self.deltaSize),
            self)
        self.decHeightAct.triggered.connect(self.__decHeight)
        
        self.setSizeAct = QAction(
            UI.PixmapCache.getIcon("sceneSize"),
            self.tr("Set size"), self)
        self.setSizeAct.triggered.connect(self.__setSize)
        
        self.rescanAct = QAction(
            UI.PixmapCache.getIcon("rescan"),
            self.tr("Re-Scan"), self)
        self.rescanAct.triggered.connect(self.__rescan)
        
        self.relayoutAct = QAction(
            UI.PixmapCache.getIcon("relayout"),
            self.tr("Re-Layout"), self)
        self.relayoutAct.triggered.connect(self.__relayout)
        
        self.alignLeftAct = QAction(
            UI.PixmapCache.getIcon("shapesAlignLeft"),
            self.tr("Align Left"), self)
        self.alignMapper.setMapping(
            self.alignLeftAct, Qt.AlignmentFlag.AlignLeft)
        self.alignLeftAct.triggered.connect(self.alignMapper.map)
        
        self.alignHCenterAct = QAction(
            UI.PixmapCache.getIcon("shapesAlignHCenter"),
            self.tr("Align Center Horizontal"), self)
        self.alignMapper.setMapping(
            self.alignHCenterAct, Qt.AlignmentFlag.AlignHCenter)
        self.alignHCenterAct.triggered.connect(self.alignMapper.map)
        
        self.alignRightAct = QAction(
            UI.PixmapCache.getIcon("shapesAlignRight"),
            self.tr("Align Right"), self)
        self.alignMapper.setMapping(
            self.alignRightAct, Qt.AlignmentFlag.AlignRight)
        self.alignRightAct.triggered.connect(self.alignMapper.map)
        
        self.alignTopAct = QAction(
            UI.PixmapCache.getIcon("shapesAlignTop"),
            self.tr("Align Top"), self)
        self.alignMapper.setMapping(
            self.alignTopAct, Qt.AlignmentFlag.AlignTop)
        self.alignTopAct.triggered.connect(self.alignMapper.map)
        
        self.alignVCenterAct = QAction(
            UI.PixmapCache.getIcon("shapesAlignVCenter"),
            self.tr("Align Center Vertical"), self)
        self.alignMapper.setMapping(
            self.alignVCenterAct, Qt.AlignmentFlag.AlignVCenter)
        self.alignVCenterAct.triggered.connect(self.alignMapper.map)
        
        self.alignBottomAct = QAction(
            UI.PixmapCache.getIcon("shapesAlignBottom"),
            self.tr("Align Bottom"), self)
        self.alignMapper.setMapping(
            self.alignBottomAct, Qt.AlignmentFlag.AlignBottom)
        self.alignBottomAct.triggered.connect(self.alignMapper.map)
    
    def setLayoutActionsEnabled(self, enable):
        """
        Public method to enable or disable the layout related actions.
        
        @param enable flag indicating the desired enable state
        @type bool
        """
        self.rescanAct.setEnabled(enable)
        self.relayoutAct.setEnabled(enable)
    
    def __checkSizeActions(self):
        """
        Private slot to set the enabled state of the size actions.
        """
        diagramSize = self._getDiagramSize(10)
        sceneRect = self.scene().sceneRect()
        if (sceneRect.width() - self.deltaSize) < diagramSize.width():
            self.decWidthAct.setEnabled(False)
        else:
            self.decWidthAct.setEnabled(True)
        if (sceneRect.height() - self.deltaSize) < diagramSize.height():
            self.decHeightAct.setEnabled(False)
        else:
            self.decHeightAct.setEnabled(True)
        
    def __sceneChanged(self, areas):
        """
        Private slot called when the scene changes.
        
        @param areas list of rectangles that contain changes
        @type list of QRectF
        """
        if len(self.scene().selectedItems()) > 0:
            self.deleteShapeAct.setEnabled(True)
        else:
            self.deleteShapeAct.setEnabled(False)
        
        sceneRect = self.scene().sceneRect()
        newWidth = width = sceneRect.width()
        newHeight = height = sceneRect.height()
        rect = self.scene().itemsBoundingRect()
        # calculate with 10 pixel border on each side
        if sceneRect.right() - 10 < rect.right():
            newWidth = rect.right() + 10
        if sceneRect.bottom() - 10 < rect.bottom():
            newHeight = rect.bottom() + 10
        
        if newHeight != height or newWidth != width:
            self.setSceneSize(newWidth, newHeight)
            self.__checkSizeActions()
        
    def initToolBar(self):
        """
        Public method to populate a toolbar with our actions.
        
        @return the populated toolBar
        @rtype QToolBar
        """
        toolBar = QToolBar(self.tr("Graphics"), self)
        toolBar.setIconSize(UI.Config.ToolBarIconSize)
        toolBar.addAction(self.deleteShapeAct)
        toolBar.addSeparator()
        toolBar.addAction(self.alignLeftAct)
        toolBar.addAction(self.alignHCenterAct)
        toolBar.addAction(self.alignRightAct)
        toolBar.addAction(self.alignTopAct)
        toolBar.addAction(self.alignVCenterAct)
        toolBar.addAction(self.alignBottomAct)
        toolBar.addSeparator()
        toolBar.addAction(self.incWidthAct)
        toolBar.addAction(self.incHeightAct)
        toolBar.addAction(self.decWidthAct)
        toolBar.addAction(self.decHeightAct)
        toolBar.addAction(self.setSizeAct)
        toolBar.addSeparator()
        toolBar.addAction(self.rescanAct)
        toolBar.addAction(self.relayoutAct)
        
        return toolBar
        
    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
        @type QGraphicsItem
        @param itemType type to be filtered
        @type class
        @return list of interesting collision items
        @rtype QGraphicsItem
        """
        return [itm for itm in items if isinstance(itm, itemType)]
        
    def selectItems(self, items):
        """
        Public method to select the given items.
        
        @param items list of items to be selected
        @type list of QGraphicsItemItem
        """
        # step 1: deselect all items
        self.unselectItems()
        
        # step 2: select all given items
        for itm in items:
            if isinstance(itm, UMLItem):
                itm.setSelected(True)
        
    def selectItem(self, item):
        """
        Public method to select an item.
        
        @param item item to be selected
        @type QGraphicsItemItem
        """
        if isinstance(item, UMLItem):
            item.setSelected(not item.isSelected())
        
    def __deleteShape(self):
        """
        Private method to delete the selected shapes from the display.
        """
        for item in self.scene().selectedItems():
            item.removeAssociations()
            item.setSelected(False)
            self.scene().removeItem(item)
            del item
        
    def __incWidth(self):
        """
        Private method to handle the increase width context menu entry.
        """
        self.resizeScene(self.deltaSize, True)
        self.__checkSizeActions()
        
    def __incHeight(self):
        """
        Private method to handle the increase height context menu entry.
        """
        self.resizeScene(self.deltaSize, False)
        self.__checkSizeActions()
        
    def __decWidth(self):
        """
        Private method to handle the decrease width context menu entry.
        """
        self.resizeScene(-self.deltaSize, True)
        self.__checkSizeActions()
        
    def __decHeight(self):
        """
        Private method to handle the decrease height context menu entry.
        """
        self.resizeScene(-self.deltaSize, False)
        self.__checkSizeActions()
        
    def __setSize(self):
        """
        Private method to handle the set size context menu entry.
        """
        from .UMLSceneSizeDialog import UMLSceneSizeDialog
        rect = self._getDiagramRect(10)
        sceneRect = self.scene().sceneRect()
        dlg = UMLSceneSizeDialog(sceneRect.width(), sceneRect.height(),
                                 rect.width(), rect.height(), self)
        if dlg.exec() == QDialog.DialogCode.Accepted:
            width, height = dlg.getData()
            self.setSceneSize(width, height)
        self.__checkSizeActions()
        
    def autoAdjustSceneSize(self, limit=False):
        """
        Public method to adjust the scene size to the diagram size.
        
        @param limit flag indicating to limit the scene to the
            initial size
        @type bool
        """
        super().autoAdjustSceneSize(limit=limit)
        self.__checkSizeActions()
        
    def saveImage(self):
        """
        Public method to handle the save context menu entry.
        """
        fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
            self,
            self.tr("Save Diagram"),
            "",
            self.tr("Portable Network Graphics (*.png);;"
                    "Scalable Vector Graphics (*.svg)"),
            "",
            E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite))
        if fname:
            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.tr("Save Diagram"),
                    self.tr("<p>The file <b>{0}</b> already exists."
                            " Overwrite it?</p>").format(fname),
                    icon=E5MessageBox.Warning)
                if not res:
                    return
            
            success = super().saveImage(
                fname, QFileInfo(fname).suffix().upper())
            if not success:
                E5MessageBox.critical(
                    self,
                    self.tr("Save Diagram"),
                    self.tr(
                        """<p>The file <b>{0}</b> could not be saved.</p>""")
                    .format(fname))
        
    def __relayout(self):
        """
        Private slot to handle the re-layout context menu entry.
        """
        self.__itemId = -1
        self.scene().clear()
        self.relayout.emit()
        
    def __rescan(self):
        """
        Private slot to handle the re-scan context menu entry.
        """
        # 1. save positions of all items and names of selected items
        itemPositions = {}
        selectedItems = []
        for item in self.filteredItems(self.scene().items(), UMLItem):
            name = item.getName()
            if name:
                itemPositions[name] = (item.x(), item.y())
                if item.isSelected():
                    selectedItems.append(name)
        
        # 2. save
        
        # 2. re-layout the diagram
        self.__relayout()
        
        # 3. move known items to the saved positions
        for item in self.filteredItems(self.scene().items(), UMLItem):
            name = item.getName()
            if name in itemPositions:
                item.setPos(*itemPositions[name])
            if name in selectedItems:
                item.setSelected(True)
        
    def printDiagram(self):
        """
        Public slot called to print the diagram.
        """
        printer = QPrinter(mode=QPrinter.PrinterMode.PrinterResolution)
        printer.setFullPage(True)
        if Preferences.getPrinter("ColorMode"):
            printer.setColorMode(QPrinter.ColorMode.Color)
        else:
            printer.setColorMode(QPrinter.ColorMode.GrayScale)
        if Preferences.getPrinter("FirstPageFirst"):
            printer.setPageOrder(QPrinter.PageOrder.FirstPageFirst)
        else:
            printer.setPageOrder(QPrinter.PageOrder.LastPageFirst)
        printer.setPageMargins(
            Preferences.getPrinter("LeftMargin") * 10,
            Preferences.getPrinter("TopMargin") * 10,
            Preferences.getPrinter("RightMargin") * 10,
            Preferences.getPrinter("BottomMargin") * 10,
            QPrinter.Unit.Millimeter
        )
        printerName = Preferences.getPrinter("PrinterName")
        if printerName:
            printer.setPrinterName(printerName)
        
        printDialog = QPrintDialog(printer, self)
        if printDialog.exec():
            super().printDiagram(
                printer, self.diagramName)
        
    def printPreviewDiagram(self):
        """
        Public slot called to show a print preview of the diagram.
        """
        from PyQt5.QtPrintSupport import QPrintPreviewDialog
        
        printer = QPrinter(mode=QPrinter.PrinterMode.PrinterResolution)
        printer.setFullPage(True)
        if Preferences.getPrinter("ColorMode"):
            printer.setColorMode(QPrinter.ColorMode.Color)
        else:
            printer.setColorMode(QPrinter.ColorMode.GrayScale)
        if Preferences.getPrinter("FirstPageFirst"):
            printer.setPageOrder(QPrinter.PageOrder.FirstPageFirst)
        else:
            printer.setPageOrder(QPrinter.PageOrder.LastPageFirst)
        printer.setPageMargins(
            Preferences.getPrinter("LeftMargin") * 10,
            Preferences.getPrinter("TopMargin") * 10,
            Preferences.getPrinter("RightMargin") * 10,
            Preferences.getPrinter("BottomMargin") * 10,
            QPrinter.Unit.Millimeter
        )
        printerName = Preferences.getPrinter("PrinterName")
        if printerName:
            printer.setPrinterName(printerName)
        
        preview = QPrintPreviewDialog(printer, self)
        preview.paintRequested[QPrinter].connect(self.__printPreviewPrint)
        preview.exec()
        
    def __printPreviewPrint(self, printer):
        """
        Private slot to generate a print preview.
        
        @param printer reference to the printer object
        @type QPrinter
        """
        super().printDiagram(printer, self.diagramName)
        
    def setDiagramName(self, name):
        """
        Public slot to set the diagram name.
        
        @param name diagram name
        @type str
        """
        self.diagramName = name
        
    def __alignShapes(self, alignment):
        """
        Private slot to align the selected shapes.
        
        @param alignment alignment type
        @type Qt.AlignmentFlag
        """
        # step 1: get all selected items
        items = self.scene().selectedItems()
        if len(items) <= 1:
            return
        
        # step 2: find the index of the item to align in relation to
        amount = None
        for i, item in enumerate(items):
            rect = item.sceneBoundingRect()
            if alignment == Qt.AlignmentFlag.AlignLeft:
                if amount is None or rect.x() < amount:
                    amount = rect.x()
                    index = i
            elif alignment == Qt.AlignmentFlag.AlignRight:
                if amount is None or rect.x() + rect.width() > amount:
                    amount = rect.x() + rect.width()
                    index = i
            elif alignment == Qt.AlignmentFlag.AlignHCenter:
                if amount is None or rect.width() > amount:
                    amount = rect.width()
                    index = i
            elif alignment == Qt.AlignmentFlag.AlignTop:
                if amount is None or rect.y() < amount:
                    amount = rect.y()
                    index = i
            elif alignment == Qt.AlignmentFlag.AlignBottom:
                if amount is None or rect.y() + rect.height() > amount:
                    amount = rect.y() + rect.height()
                    index = i
            elif alignment == Qt.AlignmentFlag.AlignVCenter:
                # __IGNORE_WARNING_Y102__
                if amount is None or rect.height() > amount:
                    amount = rect.height()
                    index = i
        rect = items[index].sceneBoundingRect()
        
        # step 3: move the other items
        for i, item in enumerate(items):
            if i == index:
                continue
            itemrect = item.sceneBoundingRect()
            xOffset = yOffset = 0
            if alignment == Qt.AlignmentFlag.AlignLeft:
                xOffset = rect.x() - itemrect.x()
            elif alignment == Qt.AlignmentFlag.AlignRight:
                xOffset = (
                    (rect.x() + rect.width()) -
                    (itemrect.x() + itemrect.width())
                )
            elif alignment == Qt.AlignmentFlag.AlignHCenter:
                xOffset = (
                    (rect.x() + rect.width() // 2) -
                    (itemrect.x() + itemrect.width() // 2)
                )
            elif alignment == Qt.AlignmentFlag.AlignTop:
                yOffset = rect.y() - itemrect.y()
            elif alignment == Qt.AlignmentFlag.AlignBottom:
                yOffset = (
                    (rect.y() + rect.height()) -
                    (itemrect.y() + itemrect.height())
                )
            elif alignment == Qt.AlignmentFlag.AlignVCenter:
                yOffset = (
                    (rect.y() + rect.height() // 2) -
                    (itemrect.y() + itemrect.height() // 2)
                )
            item.moveBy(xOffset, yOffset)
        
        self.scene().update()
    
    def __itemsBoundingRect(self, items):
        """
        Private method to calculate the bounding rectangle of the given items.
        
        @param items list of items to operate on
        @type list of UMLItem
        @return bounding rectangle
        @rtype QRectF
        """
        rect = self.scene().sceneRect()
        right = rect.left()
        bottom = rect.top()
        left = rect.right()
        top = rect.bottom()
        for item in items:
            rect = item.sceneBoundingRect()
            left = min(rect.left(), left)
            right = max(rect.right(), right)
            top = min(rect.top(), top)
            bottom = max(rect.bottom(), bottom)
        return QRectF(left, top, right - left, bottom - top)
    
    def keyPressEvent(self, evt):
        """
        Protected method handling key press events.
        
        @param evt reference to the key event
        @type QKeyEvent
        """
        key = evt.key()
        if key in [Qt.Key.Key_Up, Qt.Key.Key_Down, Qt.Key.Key_Left,
                   Qt.Key.Key_Right]:
            items = self.filteredItems(self.scene().selectedItems())
            if items:
                if evt.modifiers() & Qt.KeyboardModifier.ControlModifier:
                    stepSize = 50
                else:
                    stepSize = 5
                if key == Qt.Key.Key_Up:
                    dx = 0
                    dy = -stepSize
                elif key == Qt.Key.Key_Down:
                    dx = 0
                    dy = stepSize
                elif key == Qt.Key.Key_Left:
                    dx = -stepSize
                    dy = 0
                else:
                    dx = stepSize
                    dy = 0
                for item in items:
                    item.moveBy(dx, dy)
                evt.accept()
                return
        
        super().keyPressEvent(evt)
    
    def wheelEvent(self, evt):
        """
        Protected method to handle wheel events.
        
        @param evt reference to the wheel event
        @type QWheelEvent
        """
        if evt.modifiers() & Qt.KeyboardModifier.ControlModifier:
            delta = evt.angleDelta().y()
            if delta < 0:
                self.zoomOut()
            elif delta > 0:
                self.zoomIn()
            evt.accept()
            return
        
        super().wheelEvent(evt)
    
    def event(self, evt):
        """
        Public method handling events.
        
        @param evt reference to the event
        @type QEvent
        @return flag indicating, if the event was handled
        @rtype bool
        """
        if evt.type() == QEvent.Type.Gesture:
            self.gestureEvent(evt)
            return True
        
        return super().event(evt)
    
    def gestureEvent(self, evt):
        """
        Protected method handling gesture events.
        
        @param evt reference to the gesture event
        @type QGestureEvent
        """
        pinch = evt.gesture(Qt.GestureType.PinchGesture)
        if pinch:
            if pinch.state() == Qt.GestureState.GestureStarted:
                pinch.setTotalScaleFactor(self.zoom() / 100.0)
            elif pinch.state() == Qt.GestureState.GestureUpdated:
                self.setZoom(int(pinch.totalScaleFactor() * 100))
            evt.accept()
    
    def getItemId(self):
        """
        Public method to get the ID to be assigned to an item.
        
        @return item ID
        @rtype int
        """
        self.__itemId += 1
        return self.__itemId

    def findItem(self, itemId):
        """
        Public method to find an UML item based on the ID.
        
        @param itemId of the item to search for
        @type int
        @return item found or None
        @rtype UMLItem
        """
        for item in self.scene().items():
            try:
                if item.getId() == itemId:
                    return item
            except AttributeError:
                continue
        
        return None
    
    def findItemByName(self, name):
        """
        Public method to find an UML item based on its name.
        
        @param name name to look for
        @type str
        @return item found or None
        @rtype UMLItem
        """
        for item in self.scene().items():
            try:
                if item.getName() == name:
                    return item
            except AttributeError:
                continue
        
        return None
    
    def getPersistenceData(self):
        """
        Public method to get a list of data to be persisted.
        
        @return list of data to be persisted
        @rtype list of str
        """
        lines = [
            "diagram_name: {0}".format(self.diagramName),
        ]
        
        for item in self.filteredItems(self.scene().items(), UMLItem):
            lines.append("item: id={0}, x={1}, y={2}, item_type={3}{4}".format(
                item.getId(), item.x(), item.y(), item.getItemType(),
                item.buildItemDataString()))
        
        from .AssociationItem import AssociationItem
        for item in self.filteredItems(self.scene().items(), AssociationItem):
            lines.append("association: {0}".format(
                item.buildAssociationItemDataString()))
        
        return lines
    
    def parsePersistenceData(self, version, data):
        """
        Public method to parse persisted data.
        
        @param version version of the data
        @type str
        @param data persisted data to be parsed
        @type list of str
        @return tuple of flag indicating success (boolean) and faulty line
            number
        @rtype int
        """
        umlItems = {}
        
        if not data[0].startswith("diagram_name:"):
            return False, 0
        self.diagramName = data[0].split(": ", 1)[1].strip()
        
        from .ClassItem import ClassItem
        from .ModuleItem import ModuleItem
        from .PackageItem import PackageItem
        from .AssociationItem import AssociationItem
        
        for linenum, line in enumerate(data[1:], start=1):
            if not line.startswith(("item:", "association:")):
                return False, linenum
            
            key, value = line.split(": ", 1)
            if key == "item":
                itemId, x, y, itemType, itemData = value.split(", ", 4)
                try:
                    itemId = int(itemId.split("=", 1)[1].strip())
                    x = float(x.split("=", 1)[1].strip())
                    y = float(y.split("=", 1)[1].strip())
                    itemType = itemType.split("=", 1)[1].strip()
                    if itemType == ClassItem.ItemType:
                        itm = ClassItem(x=0, y=0, scene=self.scene(),
                                        colors=self.getDrawingColors())
                    elif itemType == ModuleItem.ItemType:
                        itm = ModuleItem(x=0, y=0, scene=self.scene(),
                                         colors=self.getDrawingColors())
                    elif itemType == PackageItem.ItemType:
                        itm = PackageItem(x=0, y=0, scene=self.scene(),
                                          colors=self.getDrawingColors())
                    itm.setPos(x, y)
                    itm.setId(itemId)
                    umlItems[itemId] = itm
                    if not itm.parseItemDataString(version, itemData):
                        return False, linenum
                except ValueError:
                    return False, linenum
            elif key == "association":
                srcId, dstId, assocType, topToBottom = (
                    AssociationItem.parseAssociationItemDataString(
                        value.strip())
                )
                assoc = AssociationItem(umlItems[srcId], umlItems[dstId],
                                        assocType, topToBottom)
                self.scene().addItem(assoc)
        
        return True, -1
    
    def toDict(self):
        """
        Public method to collect data to be persisted.
        
        @return dictionary containing data to be persisted
        @rtype dict
        """
        items = [
            item.toDict()
            for item in self.filteredItems(self.scene().items(), UMLItem)
        ]
        
        from .AssociationItem import AssociationItem
        associations = [
            assoc.toDict()
            for assoc in self.filteredItems(self.scene().items(),
                                            AssociationItem)
        ]
        
        data = {
            "diagram_name": self.diagramName,
            "items": items,
            "associations": associations,
        }
        
        return data
    
    def fromDict(self, version, data):
        """
        Public method to populate the class with data persisted by 'toDict()'.
        
        @param version version of the data
        @type str
        @param data dictionary containing the persisted data
        @type dict
        @return flag indicating success
        @rtype bool
        """
        from .UMLItem import UMLItem
        from .ClassItem import ClassItem
        from .ModuleItem import ModuleItem
        from .PackageItem import PackageItem
        from .AssociationItem import AssociationItem
        
        umlItems = {}
        
        try:
            self.diagramName = data["diagram_name"]
            for itemData in data["items"]:
                if itemData["type"] == UMLItem.ItemType:
                    itm = UMLItem.fromDict(
                        itemData, colors=self.getDrawingColors())
                elif itemData["type"] == ClassItem.ItemType:
                    itm = ClassItem.fromDict(
                        itemData, colors=self.getDrawingColors())
                elif itemData["type"] == ModuleItem.ItemType:
                    itm = ModuleItem.fromDict(
                        itemData, colors=self.getDrawingColors())
                elif itemData["type"] == PackageItem.ItemType:
                    itm = PackageItem.fromDict(
                        itemData, colors=self.getDrawingColors())
                if itm is not None:
                    umlItems[itm.getId()] = itm
                    self.scene().addItem(itm)
            
            for assocData in data["associations"]:
                assoc = AssociationItem.fromDict(
                    assocData, umlItems, colors=self.getDrawingColors())
                self.scene().addItem(assoc)
            
            return True
        except KeyError:
            return False

eric ide

mercurial