Ejemplo n.º 1
0
class ModFileTreeModel_QUndo(ModFileTreeModel):
    """
    An extension of the ModFileTreeModel that only handles undo events;
    everything else is delegated back to the base class
    """
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self._stack = QUndoStack(self)

    @property
    def undostack(self):
        return self._stack

    @property
    def has_unsaved_changes(self):
        return not self._stack.isClean()

    def setMod(self, mod_entry):
        if mod_entry is self.mod: return

        self.save()

        self._stack.clear()

        super().setMod(mod_entry)

        # todo: show dialog box asking if the user would like to save
        # unsaved changes; have checkbox to allow 'remembering' the answer

    def setData(self, index, value, role=Qt_CheckStateRole):
        """Simply a wrapper around the base setData() that only pushes
        a qundocommand if setData would have returned True"""

        # see if the setData() command should succeed
        if super().setData(index, value, role):
            # if it does, the model should have created and queued a
            # command; try to to grab it and push it to the undostack
            try:
                self._stack.push(self.dequeue_command())
            except IndexError as e:
                # if there was no command in the queue...well, what happened?
                self.LOGGER.error("No command queued")
                self.LOGGER.exception(e)
                # pass

            return True
        return False

    def save(self):

        # if we have any changes to apply:
        if not self._stack.isClean():
            super().save()
            self._stack.setClean()

    def revert_changes(self):

        # revert to 'clean' state
        self._stack.setIndex(self._stack.cleanIndex())
Ejemplo n.º 2
0
class MapDocument(QObject):
    fileNameChanged = pyqtSignal(str, str)
    modifiedChanged = pyqtSignal()
    saved = pyqtSignal()
    ##
    # Emitted when the selected tile region changes. Sends the currently
    # selected region and the previously selected region.
    ##
    selectedAreaChanged = pyqtSignal(QRegion, QRegion)
    ##
    # Emitted when the list of selected objects changes.
    ##
    selectedObjectsChanged = pyqtSignal()
    ##
    # Emitted when the list of selected tiles from the dock changes.
    ##
    selectedTilesChanged = pyqtSignal()
    currentObjectChanged = pyqtSignal(list)
    ##
    # Emitted when the map size or its tile size changes.
    ##
    mapChanged = pyqtSignal()
    layerAdded = pyqtSignal(int)
    layerAboutToBeRemoved = pyqtSignal(int)
    layerRenamed = pyqtSignal(int)
    layerRemoved = pyqtSignal(int)
    layerChanged = pyqtSignal(int)
    ##
    # Emitted after a new layer was added and the name should be edited.
    # Applies to the current layer.
    ##
    editLayerNameRequested = pyqtSignal()
    editCurrentObject = pyqtSignal()
    ##
    # Emitted when the current layer index changes.
    ##
    currentLayerIndexChanged = pyqtSignal(int)
    ##
    # Emitted when a certain region of the map changes. The region is given in
    # tile coordinates.
    ##
    regionChanged = pyqtSignal(QRegion, Layer)
    ##
    # Emitted when a certain region of the map was edited by user input.
    # The region is given in tile coordinates.
    # If multiple layers have been edited, multiple signals will be emitted.
    ##
    regionEdited = pyqtSignal(QRegion, Layer)
    tileLayerDrawMarginsChanged = pyqtSignal(TileLayer)
    tileTerrainChanged = pyqtSignal(QList)
    tileProbabilityChanged = pyqtSignal(Tile)
    tileObjectGroupChanged = pyqtSignal(Tile)
    tileAnimationChanged = pyqtSignal(Tile)
    objectGroupChanged = pyqtSignal(ObjectGroup)
    imageLayerChanged = pyqtSignal(ImageLayer)
    tilesetAboutToBeAdded = pyqtSignal(int)
    tilesetAdded = pyqtSignal(int, Tileset)
    tilesetAboutToBeRemoved = pyqtSignal(int)
    tilesetRemoved = pyqtSignal(Tileset)
    tilesetMoved = pyqtSignal(int, int)
    tilesetFileNameChanged = pyqtSignal(Tileset)
    tilesetNameChanged = pyqtSignal(Tileset)
    tilesetTileOffsetChanged = pyqtSignal(Tileset)
    tilesetChanged = pyqtSignal(Tileset)
    objectsAdded = pyqtSignal(QList)
    objectsInserted = pyqtSignal(ObjectGroup, int, int)
    objectsRemoved = pyqtSignal(QList)
    objectsChanged = pyqtSignal(QList)
    objectsIndexChanged = pyqtSignal(ObjectGroup, int, int)
    propertyAdded = pyqtSignal(Object, str)
    propertyRemoved = pyqtSignal(Object, str)
    propertyChanged = pyqtSignal(Object, str)
    propertiesChanged = pyqtSignal(Object)

    ##
    # Constructs a map document around the given map. The map document takes
    # ownership of the map.
    ##
    def __init__(self, map, fileName=QString()):
        super().__init__()

        ##
        # The filename of a plugin is unique. So it can be used to determine
        # the right plugin to be used for saving or reloading the map.
        # The nameFilter of a plugin can not be used, since it's translatable.
        # The filename of a plugin must not change while maps are open using this
        # plugin.
        ##
        self.mReaderFormat = None
        self.mWriterFormat = None
        self.mExportFormat = None
        self.mSelectedArea = QRegion()
        self.mSelectedObjects = QList()
        self.mSelectedTiles = QList()
        self.mCurrentLayerIndex = 0
        self.mLastSaved = QDateTime()
        self.mLastExportFileName = ''

        self.mFileName = fileName
        self.mMap = map
        self.mLayerModel = LayerModel(self)
        self.mCurrentObject = map  ## Current properties object. ##
        self.mRenderer = None
        self.mMapObjectModel = MapObjectModel(self)
        self.mTerrainModel = TerrainModel(self, self)
        self.mUndoStack = QUndoStack(self)
        self.createRenderer()
        if (map.layerCount() == 0):
            _x = -1
        else:
            _x = 0
        self.mCurrentLayerIndex = _x
        self.mLayerModel.setMapDocument(self)
        # Forward signals emitted from the layer model
        self.mLayerModel.layerAdded.connect(self.onLayerAdded)
        self.mLayerModel.layerAboutToBeRemoved.connect(
            self.onLayerAboutToBeRemoved)
        self.mLayerModel.layerRemoved.connect(self.onLayerRemoved)
        self.mLayerModel.layerChanged.connect(self.layerChanged)
        # Forward signals emitted from the map object model
        self.mMapObjectModel.setMapDocument(self)
        self.mMapObjectModel.objectsAdded.connect(self.objectsAdded)
        self.mMapObjectModel.objectsChanged.connect(self.objectsChanged)
        self.mMapObjectModel.objectsRemoved.connect(self.onObjectsRemoved)
        self.mMapObjectModel.rowsInserted.connect(
            self.onMapObjectModelRowsInserted)
        self.mMapObjectModel.rowsRemoved.connect(
            self.onMapObjectModelRowsInsertedOrRemoved)
        self.mMapObjectModel.rowsMoved.connect(self.onObjectsMoved)
        self.mTerrainModel.terrainRemoved.connect(self.onTerrainRemoved)
        self.mUndoStack.cleanChanged.connect(self.modifiedChanged)
        # Register tileset references
        tilesetManager = TilesetManager.instance()
        tilesetManager.addReferences(self.mMap.tilesets())

    ##
    # Destructor.
    ##
    def __del__(self):
        # Unregister tileset references
        tilesetManager = TilesetManager.instance()
        tilesetManager.removeReferences(self.mMap.tilesets())
        del self.mRenderer
        del self.mMap

    ##
    # Saves the map to its current file name. Returns whether or not the file
    # was saved successfully. If not, <i>error</i> will be set to the error
    # message if it is not 0.
    ##
    def save(self, *args):
        l = len(args)
        if l == 0:
            args = ('')
        if l == 1:
            arg = args[0]
            file = QFileInfo(arg)
            if not file.isFile():
                fileName = self.fileName()
                error = args[0]
            else:
                fileName = arg
                error = ''
            return self.save(fileName, error)
        if l == 2:
            ##
            # Saves the map to the file at \a fileName. Returns whether or not the
            # file was saved successfully. If not, <i>error</i> will be set to the
            # error message if it is not 0.
            #
            # If the save was successful, the file name of this document will be set
            # to \a fileName.
            #
            # The map format will be the same as this map was opened with.
            ##
            fileName, error = args
            mapFormat = self.mWriterFormat

            tmxMapFormat = TmxMapFormat()
            if (not mapFormat):
                mapFormat = tmxMapFormat
            if (not mapFormat.write(self.map(), fileName)):
                if (error):
                    error = mapFormat.errorString()
                return False

            self.undoStack().setClean()
            self.setFileName(fileName)
            self.mLastSaved = QFileInfo(fileName).lastModified()
            self.saved.emit()
            return True

    ##
    # Loads a map and returns a MapDocument instance on success. Returns 0
    # on error and sets the \a error message.
    ##
    def load(fileName, mapFormat=None):
        error = ''
        tmxMapFormat = TmxMapFormat()

        if (not mapFormat and not tmxMapFormat.supportsFile(fileName)):
            # Try to find a plugin that implements support for this format
            formats = PluginManager.objects()
            for format in formats:
                if (format.supportsFile(fileName)):
                    mapFormat = format
                    break

        map = None
        errorString = ''

        if mapFormat:
            map = mapFormat.read(fileName)
            errorString = mapFormat.errorString()
        else:
            map = tmxMapFormat.read(fileName)
            errorString = tmxMapFormat.errorString()

        if (not map):
            error = errorString
            return None, error

        mapDocument = MapDocument(map, fileName)
        if mapFormat:
            mapDocument.setReaderFormat(mapFormat)
            if mapFormat.hasCapabilities(MapFormat.Write):
                mapDocument.setWriterFormat(mapFormat)

        return mapDocument, error

    def fileName(self):
        return self.mFileName

    def lastExportFileName(self):
        return self.mLastExportFileName

    def setLastExportFileName(self, fileName):
        self.mLastExportFileName = fileName

    def readerFormat(self):
        return self.mReaderFormat

    def setReaderFormat(self, format):
        self.mReaderFormat = format

    def writerFormat(self):
        return self.mWriterFormat

    def setWriterFormat(self, format):
        self.mWriterFormat = format

    def exportFormat(self):
        return self.mExportFormat

    def setExportFormat(self, format):
        self.mExportFormat = format

    ##
    # Returns the name with which to display this map. It is the file name without
    # its path, or 'untitled.tmx' when the map has no file name.
    ##
    def displayName(self):
        displayName = QFileInfo(self.mFileName).fileName()
        if len(displayName) == 0:
            displayName = self.tr("untitled.tmx")
        return displayName

    ##
    # Returns whether the map has unsaved changes.
    ##
    def isModified(self):
        return not self.mUndoStack.isClean()

    def lastSaved(self):
        return self.mLastSaved

    ##
    # Returns the map instance. Be aware that directly modifying the map will
    # not allow the GUI to update itself appropriately.
    ##
    def map(self):
        return self.mMap

    ##
    # Sets the current layer to the given index.
    ##
    def setCurrentLayerIndex(self, index):
        changed = self.mCurrentLayerIndex != index
        self.mCurrentLayerIndex = index
        ## This function always sends the following signal, even if the index
        # didn't actually change. This is because the selected index in the layer
        # table view might be out of date anyway, and would otherwise not be
        # properly updated.
        #
        # This problem happens due to the selection model not sending signals
        # about changes to its current index when it is due to insertion/removal
        # of other items. The selected item doesn't change in that case, but our
        # layer index does.
        ##
        self.currentLayerIndexChanged.emit(self.mCurrentLayerIndex)
        if (changed and self.mCurrentLayerIndex != -1):
            self.setCurrentObject(self.currentLayer())

    ##
    # Returns the index of the currently selected layer. Returns -1 if no
    # layer is currently selected.
    ##
    def currentLayerIndex(self):
        return self.mCurrentLayerIndex

    ##
    # Returns the currently selected layer, or 0 if no layer is currently
    # selected.
    ##
    def currentLayer(self):
        if (self.mCurrentLayerIndex == -1):
            return None
        return self.mMap.layerAt(self.mCurrentLayerIndex)

    ##
    # Resize this map to the given \a size, while at the same time shifting
    # the contents by \a offset.
    ##
    def resizeMap(self, size, offset):
        movedSelection = self.mSelectedArea.translated(offset)
        newArea = QRect(-offset, size)
        visibleArea = self.mRenderer.boundingRect(newArea)
        origin = self.mRenderer.tileToPixelCoords_(QPointF())
        newOrigin = self.mRenderer.tileToPixelCoords_(-offset)
        pixelOffset = origin - newOrigin
        # Resize the map and each layer
        self.mUndoStack.beginMacro(self.tr("Resize Map"))
        for i in range(self.mMap.layerCount()):
            layer = self.mMap.layerAt(i)
            x = layer.layerType()
            if x == Layer.TileLayerType:
                tileLayer = layer
                self.mUndoStack.push(
                    ResizeTileLayer(self, tileLayer, size, offset))
            elif x == Layer.ObjectGroupType:
                objectGroup = layer
                # Remove objects that will fall outside of the map
                for o in objectGroup.objects():
                    if (not visibleIn(visibleArea, o, self.mRenderer)):
                        self.mUndoStack.push(RemoveMapObject(self, o))
                    else:
                        oldPos = o.position()
                        newPos = oldPos + pixelOffset
                        self.mUndoStack.push(
                            MoveMapObject(self, newPos, oldPos))
            elif x == Layer.ImageLayerType:
                # Currently not adjusted when resizing the map
                break

        self.mUndoStack.push(ResizeMap(self, size))
        self.mUndoStack.push(ChangeSelectedArea(self, movedSelection))
        self.mUndoStack.endMacro()
        # TODO: Handle layers that don't match the map size correctly

    ##
    # Offsets the layers at \a layerIndexes by \a offset, within \a bounds,
    # and optionally wraps on the X or Y axis.
    ##
    def offsetMap(self, layerIndexes, offset, bounds, wrapX, wrapY):
        if (layerIndexes.empty()):
            return
        if (layerIndexes.size() == 1):
            self.mUndoStack.push(
                OffsetLayer(self, layerIndexes.first(), offset, bounds, wrapX,
                            wrapY))
        else:
            self.mUndoStack.beginMacro(self.tr("Offset Map"))
            for layerIndex in layerIndexes:
                self.mUndoStack.push(
                    OffsetLayer(self, layerIndex, offset, bounds, wrapX,
                                wrapY))

            self.mUndoStack.endMacro()

    ##
    # Flips the selected objects in the given \a direction.
    ##
    def flipSelectedObjects(self, direction):
        if (self.mSelectedObjects.isEmpty()):
            return
        self.mUndoStack.push(
            FlipMapObjects(self, self.mSelectedObjects, direction))

    ##
    # Rotates the selected objects.
    ##
    def rotateSelectedObjects(self, direction):
        if (self.mSelectedObjects.isEmpty()):
            return
        self.mUndoStack.beginMacro(
            self.tr("Rotate %n Object(s)", "", self.mSelectedObjects.size()))
        # TODO: Rotate them properly as a group
        for mapObject in self.mSelectedObjects:
            oldRotation = mapObject.rotation()
            newRotation = oldRotation
            if (direction == RotateDirection.RotateLeft):
                newRotation -= 90
                if (newRotation < -180):
                    newRotation += 360
            else:
                newRotation += 90
                if (newRotation > 180):
                    newRotation -= 360

            self.mUndoStack.push(
                RotateMapObject(self, mapObject, newRotation, oldRotation))

        self.mUndoStack.endMacro()

    ##
    # Adds a layer of the given type to the top of the layer stack. After adding
    # the new layer, emits editLayerNameRequested().
    ##
    def addLayer(self, layerType):
        layer = None
        name = QString()
        x = layerType
        if x == Layer.TileLayerType:
            name = self.tr("Tile Layer %d" % (self.mMap.tileLayerCount() + 1))
            layer = TileLayer(name, 0, 0, self.mMap.width(),
                              self.mMap.height())
        elif x == Layer.ObjectGroupType:
            name = self.tr("Object Layer %d" %
                           (self.mMap.objectGroupCount() + 1))
            layer = ObjectGroup(name, 0, 0, self.mMap.width(),
                                self.mMap.height())
        elif x == Layer.ImageLayerType:
            name = self.tr("Image Layer %d" %
                           (self.mMap.imageLayerCount() + 1))
            layer = ImageLayer(name, 0, 0, self.mMap.width(),
                               self.mMap.height())

        index = self.mMap.layerCount()
        self.mUndoStack.push(AddLayer(self, index, layer))
        self.setCurrentLayerIndex(index)
        self.editLayerNameRequested.emit()

    ##
    # Duplicates the currently selected layer.
    ##
    def duplicateLayer(self):
        if (self.mCurrentLayerIndex == -1):
            return
        duplicate = self.mMap.layerAt(self.mCurrentLayerIndex).clone()
        duplicate.setName(self.tr("Copy of %s" % duplicate.name()))
        index = self.mCurrentLayerIndex + 1
        cmd = AddLayer(self, index, duplicate)
        cmd.setText(self.tr("Duplicate Layer"))
        self.mUndoStack.push(cmd)
        self.setCurrentLayerIndex(index)

    ##
    # Merges the currently selected layer with the layer below. This only works
    # when the layers can be merged.
    #
    # \see Layer.canMergeWith
    ##
    def mergeLayerDown(self):
        if (self.mCurrentLayerIndex < 1):
            return
        upperLayer = self.mMap.layerAt(self.mCurrentLayerIndex)
        lowerLayer = self.mMap.layerAt(self.mCurrentLayerIndex - 1)
        if (not lowerLayer.canMergeWith(upperLayer)):
            return
        merged = lowerLayer.mergedWith(upperLayer)
        self.mUndoStack.beginMacro(self.tr("Merge Layer Down"))
        self.mUndoStack.push(
            AddLayer(self, self.mCurrentLayerIndex - 1, merged))
        self.mUndoStack.push(RemoveLayer(self, self.mCurrentLayerIndex))
        self.mUndoStack.push(RemoveLayer(self, self.mCurrentLayerIndex))
        self.mUndoStack.endMacro()

    ##
    # Moves the given layer up. Does nothing when no valid layer index is
    # given.
    ##
    def moveLayerUp(self, index):
        if index < 0 or index >= self.mMap.layerCount() - 1:
            return
        self.mUndoStack.push(MoveLayer(self, index, MoveLayer.Up))

    ##
    # Moves the given layer down. Does nothing when no valid layer index is
    # given.
    ##
    def moveLayerDown(self, index):
        if index < 1 or index >= self.mMap.layerCount():
            return
        self.mUndoStack.push(MoveLayer(self, index, MoveLayer.Down))

    ##
    # Removes the given layer.
    ##
    def removeLayer(self, index):
        if index < 0 or index >= self.mMap.layerCount():
            return
        self.mUndoStack.push(RemoveLayer(self, index))

    ##
    # Show or hide all other layers except the layer at the given index.
    # If any other layer is visible then all layers will be hidden, otherwise
    # the layers will be shown.
    ##
    def toggleOtherLayers(self, index):
        self.mLayerModel.toggleOtherLayers(index)

    ##
    # Adds a tileset to this map at the given \a index. Emits the appropriate
    # signal.
    ##
    def insertTileset(self, index, tileset):
        self.tilesetAboutToBeAdded.emit(index)
        self.mMap.insertTileset(index, tileset)
        tilesetManager = TilesetManager.instance()
        tilesetManager.addReference(tileset)
        self.tilesetAdded.emit(index, tileset)

    ##
    # Removes the tileset at the given \a index from this map. Emits the
    # appropriate signal.
    #
    # \warning Does not make sure that any references to tiles in the removed
    #          tileset are cleared.
    ##
    def removeTilesetAt(self, index):
        self.tilesetAboutToBeRemoved.emit(index)
        tileset = self.mMap.tilesets().at(index)
        if (tileset == self.mCurrentObject
                or isFromTileset(self.mCurrentObject, tileset)):
            self.setCurrentObject(None)
        self.mMap.removeTilesetAt(index)
        self.tilesetRemoved.emit(tileset)
        tilesetManager = TilesetManager.instance()
        tilesetManager.removeReference(tileset)

    def moveTileset(self, _from, to):
        if (_from == to):
            return
        tileset = self.mMap.tilesets().at(_from)
        self.mMap.removeTilesetAt(_from)
        self.mMap.insertTileset(to, tileset)
        self.tilesetMoved.emit(_from, to)

    def setTilesetFileName(self, tileset, fileName):
        tileset.setFileName(fileName)
        self.tilesetFileNameChanged.emit(tileset)

    def setTilesetName(self, tileset, name):
        tileset.setName(name)
        self.tilesetNameChanged.emit(tileset)

    def setTilesetTileOffset(self, tileset, tileOffset):
        tileset.setTileOffset(tileOffset)
        self.mMap.recomputeDrawMargins()
        self.tilesetTileOffsetChanged.emit(tileset)

    def duplicateObjects(self, objects):
        if (objects.isEmpty()):
            return
        self.mUndoStack.beginMacro(
            self.tr("Duplicate %n Object(s)", "", objects.size()))
        clones = QList()
        for mapObject in objects:
            clone = mapObject.clone()
            clones.append(clone)
            self.mUndoStack.push(
                AddMapObject(self, mapObject.objectGroup(), clone))

        self.mUndoStack.endMacro()
        self.setSelectedObjects(clones)

    def removeObjects(self, objects):
        if (objects.isEmpty()):
            return
        self.mUndoStack.beginMacro(
            self.tr("Remove %n Object(s)", "", objects.size()))
        for mapObject in objects:
            self.mUndoStack.push(RemoveMapObject(self, mapObject))
        self.mUndoStack.endMacro()

    def moveObjectsToGroup(self, objects, objectGroup):
        if (objects.isEmpty()):
            return
        self.mUndoStack.beginMacro(
            self.tr("Move %n Object(s) to Layer", "", objects.size()))
        for mapObject in objects:
            if (mapObject.objectGroup() == objectGroup):
                continue
            self.mUndoStack.push(
                MoveMapObjectToGroup(self, mapObject, objectGroup))

        self.mUndoStack.endMacro()

    def setProperty(self, object, name, value):
        hadProperty = object.hasProperty(name)
        object.setProperty(name, value)
        if (hadProperty):
            self.propertyChanged.emit(object, name)
        else:
            self.propertyAdded.emit(object, name)

    def setProperties(self, object, properties):
        object.setProperties(properties)
        self.propertiesChanged.emit(object)

    def removeProperty(self, object, name):
        object.removeProperty(name)
        self.propertyRemoved.emit(object, name)

    ##
    # Returns the layer model. Can be used to modify the layer stack of the
    # map, and to display the layer stack in a view.
    ##
    def layerModel(self):
        return self.mLayerModel

    def mapObjectModel(self):
        return self.mMapObjectModel

    def terrainModel(self):
        return self.mTerrainModel

    ##
    # Returns the map renderer.
    ##
    def renderer(self):
        return self.mRenderer

    ##
    # Creates the map renderer. Should be called after changing the map
    # orientation.
    ##
    def createRenderer(self):
        if (self.mRenderer):
            del self.mRenderer
        x = self.mMap.orientation()
        if x == Map.Orientation.Isometric:
            self.mRenderer = IsometricRenderer(self.mMap)
        elif x == Map.Orientation.Staggered:
            self.mRenderer = StaggeredRenderer(self.mMap)
        elif x == Map.Orientation.Hexagonal:
            self.mRenderer = HexagonalRenderer(self.mMap)
        else:
            self.mRenderer = OrthogonalRenderer(self.mMap)

    ##
    # Returns the undo stack of this map document. Should be used to push any
    # commands on that modify the map.
    ##
    def undoStack(self):
        return self.mUndoStack

    ##
    # Returns the selected area of tiles.
    ##
    def selectedArea(self):
        return QRegion(self.mSelectedArea)

    ##
    # Sets the selected area of tiles.
    ##
    def setSelectedArea(self, selection):
        if (self.mSelectedArea != selection):
            oldSelectedArea = self.mSelectedArea
            self.mSelectedArea = selection
            self.selectedAreaChanged.emit(self.mSelectedArea, oldSelectedArea)

    ##
    # Returns the list of selected objects.
    ##
    def selectedObjects(self):
        return self.mSelectedObjects

    ##
    # Sets the list of selected objects, emitting the selectedObjectsChanged
    # signal.
    ##
    def setSelectedObjects(self, selectedObjects):
        if selectedObjects.nequal(self.mSelectedObjects):
            self.mSelectedObjects = selectedObjects
            self.selectedObjectsChanged.emit()
            if (selectedObjects.size() == 1):
                self.setCurrentObject(selectedObjects.first())

    ##
    # Returns the list of selected tiles.
    ##
    def selectedTiles(self):
        return self.mSelectedTiles

    def setSelectedTiles(self, selectedTiles):
        self.mSelectedTiles = selectedTiles
        self.selectedTilesChanged.emit()

    def currentObject(self):
        return self.mCurrentObject

    def setCurrentObject(self, object):
        if (object == self.mCurrentObject):
            return
        self.mCurrentObject = object
        self.currentObjectChanged.emit([object])

    def currentObjects(self):
        objects = QList()
        if (self.mCurrentObject):
            if (self.mCurrentObject.typeId() == Object.MapObjectType
                    and not self.mSelectedObjects.isEmpty()):
                for mapObj in self.mSelectedObjects:
                    objects.append(mapObj)
            elif (self.mCurrentObject.typeId() == Object.TileType
                  and not self.mSelectedTiles.isEmpty()):
                for tile in self.mSelectedTiles:
                    objects.append(tile)

            else:
                objects.append(self.mCurrentObject)

        return objects

    def unifyTilesets(self, *args):
        l = len(args)
        if l == 1:
            ##
            # Makes sure the all tilesets which are used at the given \a map will be
            # present in the map document.
            #
            # To reach the aim, all similar tilesets will be replaced by the version
            # in the current map document and all missing tilesets will be added to
            # the current map document.
            #
            # \warning This method assumes that the tilesets in \a map are managed by
            #          the TilesetManager!
            ##
            map = args[0]
            undoCommands = QList()
            existingTilesets = self.mMap.tilesets()
            tilesetManager = TilesetManager.instance()
            # Add tilesets that are not yet part of this map
            for tileset in map.tilesets():
                if (existingTilesets.contains(tileset)):
                    continue
                replacement = tileset.findSimilarTileset(existingTilesets)
                if (not replacement):
                    undoCommands.append(AddTileset(self, tileset))
                    continue

                # Merge the tile properties
                sharedTileCount = min(tileset.tileCount(),
                                      replacement.tileCount())
                for i in range(sharedTileCount):
                    replacementTile = replacement.tileAt(i)
                    properties = replacementTile.properties()
                    properties.merge(tileset.tileAt(i).properties())
                    undoCommands.append(
                        ChangeProperties(self, self.tr("Tile"),
                                         replacementTile, properties))

                map.replaceTileset(tileset, replacement)
                tilesetManager.addReference(replacement)
                tilesetManager.removeReference(tileset)

            if (not undoCommands.isEmpty()):
                self.mUndoStack.beginMacro(self.tr("Tileset Changes"))
                for command in undoCommands:
                    self.mUndoStack.push(command)
                self.mUndoStack.endMacro()
        elif l == 2:
            map, missingTilesets = args

            existingTilesets = self.mMap.tilesets()
            tilesetManager = TilesetManager.instance()

            for tileset in map.tilesets():
                # tileset already added
                if existingTilesets.contains(tileset):
                    continue

                replacement = tileset.findSimilarTileset(existingTilesets)

                # tileset not present and no replacement tileset found
                if not replacement:
                    if not missingTilesets.contains(tileset):
                        missingTilesets.append(tileset)
                    continue

                # replacement tileset found, change given map
                map.replaceTileset(tileset, replacement)

                tilesetManager.addReference(replacement)
                tilesetManager.removeReference(tileset)

    ##
    # Emits the map changed signal. This signal should be emitted after changing
    # the map size or its tile size.
    ##
    def emitMapChanged(self):
        self.mapChanged.emit()

    ##
    # Emits the region changed signal for the specified region. The region
    # should be in tile coordinates. This method is used by the TilePainter.
    ##
    def emitRegionChanged(self, region, layer):
        self.regionChanged.emit(region, layer)

    ##
    # Emits the region edited signal for the specified region and tile layer.
    # The region should be in tile coordinates. This should be called from
    # all map document changing classes which are triggered by user input.
    ##
    def emitRegionEdited(self, region, layer):
        self.regionEdited.emit(region, layer)

    def emitTileLayerDrawMarginsChanged(self, layer):
        self.tileLayerDrawMarginsChanged.emit(layer)

    ##
    # Emits the tileset changed signal. This signal is currently used when adding
    # or removing tiles from a tileset.
    #
    # @todo Emit more specific signals.
    ##
    def emitTilesetChanged(self, tileset):
        self.tilesetChanged.emit(tileset)

    ##
    # Emits the signal notifying about the terrain probability of a tile changing.
    ##
    def emitTileProbabilityChanged(self, tile):
        self.tileProbabilityChanged.emit(tile)

    ##
    # Emits the signal notifying tileset models about changes to tile terrain
    # information. All the \a tiles need to be from the same tileset.
    ##
    def emitTileTerrainChanged(self, tiles):
        if (not tiles.isEmpty()):
            self.tileTerrainChanged.emit(tiles)

    ##
    # Emits the signal notifying the TileCollisionEditor about the object group
    # of a tile changing.
    ##
    def emitTileObjectGroupChanged(self, tile):
        self.tileObjectGroupChanged.emit(tile)

    ##
    # Emits the signal notifying about the animation of a tile changing.
    ##
    def emitTileAnimationChanged(self, tile):
        self.tileAnimationChanged.emit(tile)

    ##
    # Emits the objectGroupChanged signal, should be called when changing the
    # color or drawing order of an object group.
    ##
    def emitObjectGroupChanged(self, objectGroup):
        self.objectGroupChanged.emit(objectGroup)

    ##
    # Emits the imageLayerChanged signal, should be called when changing the
    # image or the transparent color of an image layer.
    ##
    def emitImageLayerChanged(self, imageLayer):
        self.imageLayerChanged.emit(imageLayer)

    ##
    # Emits the editLayerNameRequested signal, to get renamed.
    ##
    def emitEditLayerNameRequested(self):
        self.editLayerNameRequested.emit()

    ##
    # Emits the editCurrentObject signal, which makes the Properties window become
    # visible and take focus.
    ##
    def emitEditCurrentObject(self):
        self.editCurrentObject.emit()

    ##
    # Before forwarding the signal, the objects are removed from the list of
    # selected objects, triggering a selectedObjectsChanged signal when
    # appropriate.
    ##
    def onObjectsRemoved(self, objects):
        self.deselectObjects(objects)
        self.objectsRemoved.emit(objects)

    def onMapObjectModelRowsInserted(self, parent, first, last):
        objectGroup = self.mMapObjectModel.toObjectGroup(parent)
        if (not objectGroup):  # we're not dealing with insertion of objects
            return
        self.objectsInserted.emit(objectGroup, first, last)
        self.onMapObjectModelRowsInsertedOrRemoved(parent, first, last)

    def onMapObjectModelRowsInsertedOrRemoved(self, parent, first, last):
        objectGroup = self.mMapObjectModel.toObjectGroup(parent)
        if (not objectGroup):
            return
        # Inserting or removing objects changes the index of any that come after
        lastIndex = objectGroup.objectCount() - 1
        if (last < lastIndex):
            self.objectsIndexChanged.emit(objectGroup, last + 1, lastIndex)

    def onObjectsMoved(self, parent, start, end, destination, row):
        if (parent != destination):
            return
        objectGroup = self.mMapObjectModel.toObjectGroup(parent)
        # Determine the full range over which object indexes changed
        first = min(start, row)
        last = max(end, row - 1)
        self.objectsIndexChanged.emit(objectGroup, first, last)

    def onLayerAdded(self, index):
        self.layerAdded.emit(index)
        # Select the first layer that gets added to the map
        if (self.mMap.layerCount() == 1):
            self.setCurrentLayerIndex(0)

    def onLayerAboutToBeRemoved(self, index):
        layer = self.mMap.layerAt(index)
        if (layer == self.mCurrentObject):
            self.setCurrentObject(None)
        # Deselect any objects on this layer when necessary
        og = layer
        if type(og) == ObjectGroup:
            self.deselectObjects(og.objects())
        self.layerAboutToBeRemoved.emit(index)

    def onLayerRemoved(self, index):
        # Bring the current layer index to safety
        currentLayerRemoved = self.mCurrentLayerIndex == self.mMap.layerCount()
        if (currentLayerRemoved):
            self.mCurrentLayerIndex = self.mCurrentLayerIndex - 1
        self.layerRemoved.emit(index)
        # Emitted after the layerRemoved signal so that the MapScene has a chance
        # of synchronizing before adapting to the newly selected index
        if (currentLayerRemoved):
            self.currentLayerIndexChanged.emit(self.mCurrentLayerIndex)

    def onTerrainRemoved(self, terrain):
        if (terrain == self.mCurrentObject):
            self.setCurrentObject(None)

    def setFileName(self, fileName):
        if (self.mFileName == fileName):
            return
        oldFileName = self.mFileName
        self.mFileName = fileName
        self.fileNameChanged.emit(fileName, oldFileName)

    def deselectObjects(self, objects):
        # Unset the current object when it was part of this list of objects
        if (self.mCurrentObject
                and self.mCurrentObject.typeId() == Object.MapObjectType):
            if (objects.contains(self.mCurrentObject)):
                self.setCurrentObject(None)
        removedCount = 0
        for object in objects:
            removedCount += self.mSelectedObjects.removeAll(object)
        if (removedCount > 0):
            self.selectedObjectsChanged.emit()

    def disconnect(self):
        try:
            super().disconnect()
        except:
            pass
Ejemplo n.º 3
0
class IconEditorGrid(QWidget):
    """
    Class implementing the icon editor grid.
    
    @signal canRedoChanged(bool) emitted after the redo status has changed
    @signal canUndoChanged(bool) emitted after the undo status has changed
    @signal clipboardImageAvailable(bool) emitted to signal the availability
        of an image to be pasted
    @signal colorChanged(QColor) emitted after the drawing color was changed
    @signal imageChanged(bool) emitted after the image was modified
    @signal positionChanged(int, int) emitted after the cursor poition was
        changed
    @signal previewChanged(QPixmap) emitted to signal a new preview pixmap
    @signal selectionAvailable(bool) emitted to signal a change of the
        selection
    @signal sizeChanged(int, int) emitted after the size has been changed
    @signal zoomChanged(int) emitted to signal a change of the zoom value
    """
    canRedoChanged = pyqtSignal(bool)
    canUndoChanged = pyqtSignal(bool)
    clipboardImageAvailable = pyqtSignal(bool)
    colorChanged = pyqtSignal(QColor)
    imageChanged = pyqtSignal(bool)
    positionChanged = pyqtSignal(int, int)
    previewChanged = pyqtSignal(QPixmap)
    selectionAvailable = pyqtSignal(bool)
    sizeChanged = pyqtSignal(int, int)
    zoomChanged = pyqtSignal(int)

    Pencil = 1
    Rubber = 2
    Line = 3
    Rectangle = 4
    FilledRectangle = 5
    Circle = 6
    FilledCircle = 7
    Ellipse = 8
    FilledEllipse = 9
    Fill = 10
    ColorPicker = 11

    RectangleSelection = 20
    CircleSelection = 21

    MarkColor = QColor(255, 255, 255, 255)
    NoMarkColor = QColor(0, 0, 0, 0)

    ZoomMinimum = 100
    ZoomMaximum = 10000
    ZoomStep = 100
    ZoomDefault = 1200
    ZoomPercent = True

    def __init__(self, parent=None):
        """
        Constructor
        
        @param parent reference to the parent widget (QWidget)
        """
        super(IconEditorGrid, self).__init__(parent)

        self.setAttribute(Qt.WA_StaticContents)
        self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)

        self.__curColor = Qt.black
        self.__zoom = 12
        self.__curTool = self.Pencil
        self.__startPos = QPoint()
        self.__endPos = QPoint()
        self.__dirty = False
        self.__selecting = False
        self.__selRect = QRect()
        self.__isPasting = False
        self.__clipboardSize = QSize()
        self.__pasteRect = QRect()

        self.__undoStack = QUndoStack(self)
        self.__currentUndoCmd = None

        self.__image = QImage(32, 32, QImage.Format_ARGB32)
        self.__image.fill(qRgba(0, 0, 0, 0))
        self.__markImage = QImage(self.__image)
        self.__markImage.fill(self.NoMarkColor.rgba())

        self.__compositingMode = QPainter.CompositionMode_SourceOver
        self.__lastPos = (-1, -1)

        self.__gridEnabled = True
        self.__selectionAvailable = False

        self.__initCursors()
        self.__initUndoTexts()

        self.setMouseTracking(True)

        self.__undoStack.canRedoChanged.connect(self.canRedoChanged)
        self.__undoStack.canUndoChanged.connect(self.canUndoChanged)
        self.__undoStack.cleanChanged.connect(self.__cleanChanged)

        self.imageChanged.connect(self.__updatePreviewPixmap)
        QApplication.clipboard().dataChanged.connect(self.__checkClipboard)

        self.__checkClipboard()

    def __initCursors(self):
        """
        Private method to initialize the various cursors.
        """
        self.__normalCursor = QCursor(Qt.ArrowCursor)

        pix = QPixmap(":colorpicker-cursor.xpm")
        mask = pix.createHeuristicMask()
        pix.setMask(mask)
        self.__colorPickerCursor = QCursor(pix, 1, 21)

        pix = QPixmap(":paintbrush-cursor.xpm")
        mask = pix.createHeuristicMask()
        pix.setMask(mask)
        self.__paintCursor = QCursor(pix, 0, 19)

        pix = QPixmap(":fill-cursor.xpm")
        mask = pix.createHeuristicMask()
        pix.setMask(mask)
        self.__fillCursor = QCursor(pix, 3, 20)

        pix = QPixmap(":aim-cursor.xpm")
        mask = pix.createHeuristicMask()
        pix.setMask(mask)
        self.__aimCursor = QCursor(pix, 10, 10)

        pix = QPixmap(":eraser-cursor.xpm")
        mask = pix.createHeuristicMask()
        pix.setMask(mask)
        self.__rubberCursor = QCursor(pix, 1, 16)

    def __initUndoTexts(self):
        """
        Private method to initialize texts to be associated with undo commands
        for the various drawing tools.
        """
        self.__undoTexts = {
            self.Pencil: self.tr("Set Pixel"),
            self.Rubber: self.tr("Erase Pixel"),
            self.Line: self.tr("Draw Line"),
            self.Rectangle: self.tr("Draw Rectangle"),
            self.FilledRectangle: self.tr("Draw Filled Rectangle"),
            self.Circle: self.tr("Draw Circle"),
            self.FilledCircle: self.tr("Draw Filled Circle"),
            self.Ellipse: self.tr("Draw Ellipse"),
            self.FilledEllipse: self.tr("Draw Filled Ellipse"),
            self.Fill: self.tr("Fill Region"),
        }

    def isDirty(self):
        """
        Public method to check the dirty status.
        
        @return flag indicating a modified status (boolean)
        """
        return self.__dirty

    def setDirty(self, dirty, setCleanState=False):
        """
        Public slot to set the dirty flag.
        
        @param dirty flag indicating the new modification status (boolean)
        @param setCleanState flag indicating to set the undo stack to clean
            (boolean)
        """
        self.__dirty = dirty
        self.imageChanged.emit(dirty)

        if not dirty and setCleanState:
            self.__undoStack.setClean()

    def sizeHint(self):
        """
        Public method to report the size hint.
        
        @return size hint (QSize)
        """
        size = self.__zoom * self.__image.size()
        if self.__zoom >= 3 and self.__gridEnabled:
            size += QSize(1, 1)
        return size

    def setPenColor(self, newColor):
        """
        Public method to set the drawing color.
        
        @param newColor reference to the new color (QColor)
        """
        self.__curColor = QColor(newColor)
        self.colorChanged.emit(QColor(newColor))

    def penColor(self):
        """
        Public method to get the current drawing color.
        
        @return current drawing color (QColor)
        """
        return QColor(self.__curColor)

    def setCompositingMode(self, mode):
        """
        Public method to set the compositing mode.
        
        @param mode compositing mode to set (QPainter.CompositionMode)
        """
        self.__compositingMode = mode

    def compositingMode(self):
        """
        Public method to get the compositing mode.
        
        @return compositing mode (QPainter.CompositionMode)
        """
        return self.__compositingMode

    def setTool(self, tool):
        """
        Public method to set the current drawing tool.
        
        @param tool drawing tool to be used
            (IconEditorGrid.Pencil ... IconEditorGrid.CircleSelection)
        """
        self.__curTool = tool
        self.__lastPos = (-1, -1)

        if self.__curTool in [self.RectangleSelection, self.CircleSelection]:
            self.__selecting = True
        else:
            self.__selecting = False

        if self.__curTool in [
                self.RectangleSelection, self.CircleSelection, self.Line,
                self.Rectangle, self.FilledRectangle, self.Circle,
                self.FilledCircle, self.Ellipse, self.FilledEllipse
        ]:
            self.setCursor(self.__aimCursor)
        elif self.__curTool == self.Fill:
            self.setCursor(self.__fillCursor)
        elif self.__curTool == self.ColorPicker:
            self.setCursor(self.__colorPickerCursor)
        elif self.__curTool == self.Pencil:
            self.setCursor(self.__paintCursor)
        elif self.__curTool == self.Rubber:
            self.setCursor(self.__rubberCursor)
        else:
            self.setCursor(self.__normalCursor)

    def tool(self):
        """
        Public method to get the current drawing tool.
        
        @return current drawing tool
            (IconEditorGrid.Pencil ... IconEditorGrid.CircleSelection)
        """
        return self.__curTool

    def setIconImage(self, newImage, undoRedo=False, clearUndo=False):
        """
        Public method to set a new icon image.
        
        @param newImage reference to the new image (QImage)
        @keyparam undoRedo flag indicating an undo or redo operation (boolean)
        @keyparam clearUndo flag indicating to clear the undo stack (boolean)
        """
        if newImage != self.__image:
            self.__image = newImage.convertToFormat(QImage.Format_ARGB32)
            self.update()
            self.updateGeometry()
            self.resize(self.sizeHint())

            self.__markImage = QImage(self.__image)
            self.__markImage.fill(self.NoMarkColor.rgba())

            if undoRedo:
                self.setDirty(not self.__undoStack.isClean())
            else:
                self.setDirty(False)

            if clearUndo:
                self.__undoStack.clear()

            self.sizeChanged.emit(*self.iconSize())

    def iconImage(self):
        """
        Public method to get a copy of the icon image.
        
        @return copy of the icon image (QImage)
        """
        return QImage(self.__image)

    def iconSize(self):
        """
        Public method to get the size of the icon.
        
        @return width and height of the image as a tuple (integer, integer)
        """
        return self.__image.width(), self.__image.height()

    def setZoomFactor(self, newZoom):
        """
        Public method to set the zoom factor in percent.
        
        @param newZoom zoom factor (integer >= 100)
        """
        newZoom = max(100, newZoom)  # must not be less than 100
        if newZoom != self.__zoom:
            self.__zoom = newZoom // 100
            self.update()
            self.updateGeometry()
            self.resize(self.sizeHint())
            self.zoomChanged.emit(int(self.__zoom * 100))

    def zoomFactor(self):
        """
        Public method to get the current zoom factor in percent.
        
        @return zoom factor (integer)
        """
        return self.__zoom * 100

    def setGridEnabled(self, enable):
        """
        Public method to enable the display of grid lines.
        
        @param enable enabled status of the grid lines (boolean)
        """
        if enable != self.__gridEnabled:
            self.__gridEnabled = enable
            self.update()

    def isGridEnabled(self):
        """
        Public method to get the grid lines status.
        
        @return enabled status of the grid lines (boolean)
        """
        return self.__gridEnabled

    def paintEvent(self, evt):
        """
        Protected method called to repaint some of the widget.
        
        @param evt reference to the paint event object (QPaintEvent)
        """
        painter = QPainter(self)

        if self.__zoom >= 3 and self.__gridEnabled:
            painter.setPen(self.palette().windowText().color())
            i = 0
            while i <= self.__image.width():
                painter.drawLine(self.__zoom * i, 0, self.__zoom * i,
                                 self.__zoom * self.__image.height())
                i += 1
            j = 0
            while j <= self.__image.height():
                painter.drawLine(0, self.__zoom * j,
                                 self.__zoom * self.__image.width(),
                                 self.__zoom * j)
                j += 1

        col = QColor("#aaa")
        painter.setPen(Qt.DashLine)
        for i in range(0, self.__image.width()):
            for j in range(0, self.__image.height()):
                rect = self.__pixelRect(i, j)
                if evt.region().intersects(rect):
                    color = QColor.fromRgba(self.__image.pixel(i, j))
                    painter.fillRect(rect, QBrush(Qt.white))
                    painter.fillRect(QRect(rect.topLeft(), rect.center()), col)
                    painter.fillRect(QRect(rect.center(), rect.bottomRight()),
                                     col)
                    painter.fillRect(rect, QBrush(color))

                    if self.__isMarked(i, j):
                        painter.drawRect(rect.adjusted(0, 0, -1, -1))

        painter.end()

    def __pixelRect(self, i, j):
        """
        Private method to determine the rectangle for a given pixel coordinate.
        
        @param i x-coordinate of the pixel in the image (integer)
        @param j y-coordinate of the pixel in the image (integer)
        @return rectangle for the given pixel coordinates (QRect)
        """
        if self.__zoom >= 3 and self.__gridEnabled:
            return QRect(self.__zoom * i + 1, self.__zoom * j + 1,
                         self.__zoom - 1, self.__zoom - 1)
        else:
            return QRect(self.__zoom * i, self.__zoom * j, self.__zoom,
                         self.__zoom)

    def mousePressEvent(self, evt):
        """
        Protected method to handle mouse button press events.
        
        @param evt reference to the mouse event object (QMouseEvent)
        """
        if evt.button() == Qt.LeftButton:
            if self.__isPasting:
                self.__isPasting = False
                self.editPaste(True)
                self.__markImage.fill(self.NoMarkColor.rgba())
                self.update(self.__pasteRect)
                self.__pasteRect = QRect()
                return

            if self.__curTool == self.Pencil:
                cmd = IconEditCommand(self, self.__undoTexts[self.__curTool],
                                      self.__image)
                self.__setImagePixel(evt.pos(), True)
                self.setDirty(True)
                self.__undoStack.push(cmd)
                self.__currentUndoCmd = cmd
            elif self.__curTool == self.Rubber:
                cmd = IconEditCommand(self, self.__undoTexts[self.__curTool],
                                      self.__image)
                self.__setImagePixel(evt.pos(), False)
                self.setDirty(True)
                self.__undoStack.push(cmd)
                self.__currentUndoCmd = cmd
            elif self.__curTool == self.Fill:
                i, j = self.__imageCoordinates(evt.pos())
                col = QColor()
                col.setRgba(self.__image.pixel(i, j))
                cmd = IconEditCommand(self, self.__undoTexts[self.__curTool],
                                      self.__image)
                self.__drawFlood(i, j, col)
                self.setDirty(True)
                self.__undoStack.push(cmd)
                cmd.setAfterImage(self.__image)
            elif self.__curTool == self.ColorPicker:
                i, j = self.__imageCoordinates(evt.pos())
                col = QColor()
                col.setRgba(self.__image.pixel(i, j))
                self.setPenColor(col)
            else:
                self.__unMark()
                self.__startPos = evt.pos()
                self.__endPos = evt.pos()

    def mouseMoveEvent(self, evt):
        """
        Protected method to handle mouse move events.
        
        @param evt reference to the mouse event object (QMouseEvent)
        """
        self.positionChanged.emit(*self.__imageCoordinates(evt.pos()))

        if self.__isPasting and not (evt.buttons() & Qt.LeftButton):
            self.__drawPasteRect(evt.pos())
            return

        if evt.buttons() & Qt.LeftButton:
            if self.__curTool == self.Pencil:
                self.__setImagePixel(evt.pos(), True)
                self.setDirty(True)
            elif self.__curTool == self.Rubber:
                self.__setImagePixel(evt.pos(), False)
                self.setDirty(True)
            elif self.__curTool in [self.Fill, self.ColorPicker]:
                pass  # do nothing
            else:
                self.__drawTool(evt.pos(), True)

    def mouseReleaseEvent(self, evt):
        """
        Protected method to handle mouse button release events.
        
        @param evt reference to the mouse event object (QMouseEvent)
        """
        if evt.button() == Qt.LeftButton:
            if self.__curTool in [self.Pencil, self.Rubber]:
                if self.__currentUndoCmd:
                    self.__currentUndoCmd.setAfterImage(self.__image)
                    self.__currentUndoCmd = None

            if self.__curTool not in [
                    self.Pencil, self.Rubber, self.Fill, self.ColorPicker,
                    self.RectangleSelection, self.CircleSelection
            ]:
                cmd = IconEditCommand(self, self.__undoTexts[self.__curTool],
                                      self.__image)
                if self.__drawTool(evt.pos(), False):
                    self.__undoStack.push(cmd)
                    cmd.setAfterImage(self.__image)
                    self.setDirty(True)

    def __setImagePixel(self, pos, opaque):
        """
        Private slot to set or erase a pixel.
        
        @param pos position of the pixel in the widget (QPoint)
        @param opaque flag indicating a set operation (boolean)
        """
        i, j = self.__imageCoordinates(pos)

        if self.__image.rect().contains(i, j) and (i, j) != self.__lastPos:
            if opaque:
                painter = QPainter(self.__image)
                painter.setPen(self.penColor())
                painter.setCompositionMode(self.__compositingMode)
                painter.drawPoint(i, j)
            else:
                self.__image.setPixel(i, j, qRgba(0, 0, 0, 0))
            self.__lastPos = (i, j)

            self.update(self.__pixelRect(i, j))

    def __imageCoordinates(self, pos):
        """
        Private method to convert from widget to image coordinates.
        
        @param pos widget coordinate (QPoint)
        @return tuple with the image coordinates (tuple of two integers)
        """
        i = pos.x() // self.__zoom
        j = pos.y() // self.__zoom
        return i, j

    def __drawPasteRect(self, pos):
        """
        Private slot to draw a rectangle for signaling a paste operation.
        
        @param pos widget position of the paste rectangle (QPoint)
        """
        self.__markImage.fill(self.NoMarkColor.rgba())
        if self.__pasteRect.isValid():
            self.__updateImageRect(
                self.__pasteRect.topLeft(),
                self.__pasteRect.bottomRight() + QPoint(1, 1))

        x, y = self.__imageCoordinates(pos)
        isize = self.__image.size()
        if x + self.__clipboardSize.width() <= isize.width():
            sx = self.__clipboardSize.width()
        else:
            sx = isize.width() - x
        if y + self.__clipboardSize.height() <= isize.height():
            sy = self.__clipboardSize.height()
        else:
            sy = isize.height() - y

        self.__pasteRect = QRect(QPoint(x, y), QSize(sx - 1, sy - 1))

        painter = QPainter(self.__markImage)
        painter.setPen(self.MarkColor)
        painter.drawRect(self.__pasteRect)
        painter.end()

        self.__updateImageRect(self.__pasteRect.topLeft(),
                               self.__pasteRect.bottomRight() + QPoint(1, 1))

    def __drawTool(self, pos, mark):
        """
        Private method to perform a draw operation depending of the current
        tool.
        
        @param pos widget coordinate to perform the draw operation at (QPoint)
        @param mark flag indicating a mark operation (boolean)
        @return flag indicating a successful draw (boolean)
        """
        self.__unMark()

        if mark:
            self.__endPos = QPoint(pos)
            drawColor = self.MarkColor
            img = self.__markImage
        else:
            drawColor = self.penColor()
            img = self.__image

        start = QPoint(*self.__imageCoordinates(self.__startPos))
        end = QPoint(*self.__imageCoordinates(pos))

        painter = QPainter(img)
        painter.setPen(drawColor)
        painter.setCompositionMode(self.__compositingMode)

        if self.__curTool == self.Line:
            painter.drawLine(start, end)

        elif self.__curTool in [
                self.Rectangle, self.FilledRectangle, self.RectangleSelection
        ]:
            left = min(start.x(), end.x())
            top = min(start.y(), end.y())
            right = max(start.x(), end.x())
            bottom = max(start.y(), end.y())
            if self.__curTool == self.RectangleSelection:
                painter.setBrush(QBrush(drawColor))
            if self.__curTool == self.FilledRectangle:
                for y in range(top, bottom + 1):
                    painter.drawLine(left, y, right, y)
            else:
                painter.drawRect(left, top, right - left, bottom - top)
            if self.__selecting:
                self.__selRect = QRect(left, top, right - left + 1,
                                       bottom - top + 1)
                self.__selectionAvailable = True
                self.selectionAvailable.emit(True)

        elif self.__curTool in [
                self.Circle, self.FilledCircle, self.CircleSelection
        ]:
            r = max(abs(start.x() - end.x()), abs(start.y() - end.y()))
            if self.__curTool in [self.FilledCircle, self.CircleSelection]:
                painter.setBrush(QBrush(drawColor))
            painter.drawEllipse(start, r, r)
            if self.__selecting:
                self.__selRect = QRect(start.x() - r,
                                       start.y() - r, 2 * r + 1, 2 * r + 1)
                self.__selectionAvailable = True
                self.selectionAvailable.emit(True)

        elif self.__curTool in [self.Ellipse, self.FilledEllipse]:
            r1 = abs(start.x() - end.x())
            r2 = abs(start.y() - end.y())
            if r1 == 0 or r2 == 0:
                return False
            if self.__curTool == self.FilledEllipse:
                painter.setBrush(QBrush(drawColor))
            painter.drawEllipse(start, r1, r2)

        painter.end()

        if self.__curTool in [
                self.Circle, self.FilledCircle, self.Ellipse,
                self.FilledEllipse
        ]:
            self.update()
        else:
            self.__updateRect(self.__startPos, pos)

        return True

    def __drawFlood(self, i, j, oldColor, doUpdate=True):
        """
        Private method to perform a flood fill operation.
        
        @param i x-value in image coordinates (integer)
        @param j y-value in image coordinates (integer)
        @param oldColor reference to the color at position i, j (QColor)
        @param doUpdate flag indicating an update is requested (boolean)
            (used for speed optimizations)
        """
        if not self.__image.rect().contains(i, j) or \
           self.__image.pixel(i, j) != oldColor.rgba() or \
           self.__image.pixel(i, j) == self.penColor().rgba():
            return

        self.__image.setPixel(i, j, self.penColor().rgba())

        self.__drawFlood(i, j - 1, oldColor, False)
        self.__drawFlood(i, j + 1, oldColor, False)
        self.__drawFlood(i - 1, j, oldColor, False)
        self.__drawFlood(i + 1, j, oldColor, False)

        if doUpdate:
            self.update()

    def __updateRect(self, pos1, pos2):
        """
        Private slot to update parts of the widget.
        
        @param pos1 top, left position for the update in widget coordinates
            (QPoint)
        @param pos2 bottom, right position for the update in widget
            coordinates (QPoint)
        """
        self.__updateImageRect(QPoint(*self.__imageCoordinates(pos1)),
                               QPoint(*self.__imageCoordinates(pos2)))

    def __updateImageRect(self, ipos1, ipos2):
        """
        Private slot to update parts of the widget.
        
        @param ipos1 top, left position for the update in image coordinates
            (QPoint)
        @param ipos2 bottom, right position for the update in image
            coordinates (QPoint)
        """
        r1 = self.__pixelRect(ipos1.x(), ipos1.y())
        r2 = self.__pixelRect(ipos2.x(), ipos2.y())

        left = min(r1.x(), r2.x())
        top = min(r1.y(), r2.y())
        right = max(r1.x() + r1.width(), r2.x() + r2.width())
        bottom = max(r1.y() + r1.height(), r2.y() + r2.height())
        self.update(left, top, right - left + 1, bottom - top + 1)

    def __unMark(self):
        """
        Private slot to remove the mark indicator.
        """
        self.__markImage.fill(self.NoMarkColor.rgba())
        if self.__curTool in [
                self.Circle, self.FilledCircle, self.Ellipse,
                self.FilledEllipse, self.CircleSelection
        ]:
            self.update()
        else:
            self.__updateRect(self.__startPos, self.__endPos)

        if self.__selecting:
            self.__selRect = QRect()
            self.__selectionAvailable = False
            self.selectionAvailable.emit(False)

    def __isMarked(self, i, j):
        """
        Private method to check, if a pixel is marked.
        
        @param i x-value in image coordinates (integer)
        @param j y-value in image coordinates (integer)
        @return flag indicating a marked pixel (boolean)
        """
        return self.__markImage.pixel(i, j) == self.MarkColor.rgba()

    def __updatePreviewPixmap(self):
        """
        Private slot to generate and signal an updated preview pixmap.
        """
        p = QPixmap.fromImage(self.__image)
        self.previewChanged.emit(p)

    def previewPixmap(self):
        """
        Public method to generate a preview pixmap.
        
        @return preview pixmap (QPixmap)
        """
        p = QPixmap.fromImage(self.__image)
        return p

    def __checkClipboard(self):
        """
        Private slot to check, if the clipboard contains a valid image, and
        signal the result.
        """
        ok = self.__clipboardImage()[1]
        self.__clipboardImageAvailable = ok
        self.clipboardImageAvailable.emit(ok)

    def canPaste(self):
        """
        Public slot to check the availability of the paste operation.
        
        @return flag indicating availability of paste (boolean)
        """
        return self.__clipboardImageAvailable

    def __clipboardImage(self):
        """
        Private method to get an image from the clipboard.
        
        @return tuple with the image (QImage) and a flag indicating a
            valid image (boolean)
        """
        img = QApplication.clipboard().image()
        ok = not img.isNull()
        if ok:
            img = img.convertToFormat(QImage.Format_ARGB32)

        return img, ok

    def __getSelectionImage(self, cut):
        """
        Private method to get an image from the selection.
        
        @param cut flag indicating to cut the selection (boolean)
        @return image of the selection (QImage)
        """
        if cut:
            cmd = IconEditCommand(self, self.tr("Cut Selection"), self.__image)

        img = QImage(self.__selRect.size(), QImage.Format_ARGB32)
        img.fill(qRgba(0, 0, 0, 0))
        for i in range(0, self.__selRect.width()):
            for j in range(0, self.__selRect.height()):
                if self.__image.rect().contains(self.__selRect.x() + i,
                                                self.__selRect.y() + j):
                    if self.__isMarked(self.__selRect.x() + i,
                                       self.__selRect.y() + j):
                        img.setPixel(
                            i, j,
                            self.__image.pixel(self.__selRect.x() + i,
                                               self.__selRect.y() + j))
                        if cut:
                            self.__image.setPixel(self.__selRect.x() + i,
                                                  self.__selRect.y() + j,
                                                  qRgba(0, 0, 0, 0))

        if cut:
            self.__undoStack.push(cmd)
            cmd.setAfterImage(self.__image)

        self.__unMark()

        if cut:
            self.update(self.__selRect)

        return img

    def editCopy(self):
        """
        Public slot to copy the selection.
        """
        if self.__selRect.isValid():
            img = self.__getSelectionImage(False)
            QApplication.clipboard().setImage(img)

    def editCut(self):
        """
        Public slot to cut the selection.
        """
        if self.__selRect.isValid():
            img = self.__getSelectionImage(True)
            QApplication.clipboard().setImage(img)

    @pyqtSlot()
    def editPaste(self, pasting=False):
        """
        Public slot to paste an image from the clipboard.
        
        @param pasting flag indicating part two of the paste operation
            (boolean)
        """
        img, ok = self.__clipboardImage()
        if ok:
            if img.width() > self.__image.width() or \
                    img.height() > self.__image.height():
                res = E5MessageBox.yesNo(
                    self, self.tr("Paste"),
                    self.tr("""<p>The clipboard image is larger than the"""
                            """ current image.<br/>Paste as new image?</p>"""))
                if res:
                    self.editPasteAsNew()
                return
            elif not pasting:
                self.__isPasting = True
                self.__clipboardSize = img.size()
            else:
                cmd = IconEditCommand(self, self.tr("Paste Clipboard"),
                                      self.__image)
                self.__markImage.fill(self.NoMarkColor.rgba())
                painter = QPainter(self.__image)
                painter.setPen(self.penColor())
                painter.setCompositionMode(self.__compositingMode)
                painter.drawImage(self.__pasteRect.x(), self.__pasteRect.y(),
                                  img, 0, 0,
                                  self.__pasteRect.width() + 1,
                                  self.__pasteRect.height() + 1)

                self.__undoStack.push(cmd)
                cmd.setAfterImage(self.__image)

                self.__updateImageRect(
                    self.__pasteRect.topLeft(),
                    self.__pasteRect.bottomRight() + QPoint(1, 1))
        else:
            E5MessageBox.warning(
                self, self.tr("Pasting Image"),
                self.tr("""Invalid image data in clipboard."""))

    def editPasteAsNew(self):
        """
        Public slot to paste the clipboard as a new image.
        """
        img, ok = self.__clipboardImage()
        if ok:
            cmd = IconEditCommand(self,
                                  self.tr("Paste Clipboard as New Image"),
                                  self.__image)
            self.setIconImage(img)
            self.setDirty(True)
            self.__undoStack.push(cmd)
            cmd.setAfterImage(self.__image)

    def editSelectAll(self):
        """
        Public slot to select the complete image.
        """
        self.__unMark()

        self.__startPos = QPoint(0, 0)
        self.__endPos = QPoint(self.rect().bottomRight())
        self.__markImage.fill(self.MarkColor.rgba())
        self.__selRect = self.__image.rect()
        self.__selectionAvailable = True
        self.selectionAvailable.emit(True)

        self.update()

    def editClear(self):
        """
        Public slot to clear the image.
        """
        self.__unMark()

        cmd = IconEditCommand(self, self.tr("Clear Image"), self.__image)
        self.__image.fill(qRgba(0, 0, 0, 0))
        self.update()
        self.setDirty(True)
        self.__undoStack.push(cmd)
        cmd.setAfterImage(self.__image)

    def editResize(self):
        """
        Public slot to resize the image.
        """
        from .IconSizeDialog import IconSizeDialog
        dlg = IconSizeDialog(self.__image.width(), self.__image.height())
        res = dlg.exec_()
        if res == QDialog.Accepted:
            newWidth, newHeight = dlg.getData()
            if newWidth != self.__image.width() or \
                    newHeight != self.__image.height():
                cmd = IconEditCommand(self, self.tr("Resize Image"),
                                      self.__image)
                img = self.__image.scaled(newWidth, newHeight,
                                          Qt.IgnoreAspectRatio,
                                          Qt.SmoothTransformation)
                self.setIconImage(img)
                self.setDirty(True)
                self.__undoStack.push(cmd)
                cmd.setAfterImage(self.__image)

    def editNew(self):
        """
        Public slot to generate a new, empty image.
        """
        from .IconSizeDialog import IconSizeDialog
        dlg = IconSizeDialog(self.__image.width(), self.__image.height())
        res = dlg.exec_()
        if res == QDialog.Accepted:
            width, height = dlg.getData()
            img = QImage(width, height, QImage.Format_ARGB32)
            img.fill(qRgba(0, 0, 0, 0))
            self.setIconImage(img)

    def grayScale(self):
        """
        Public slot to convert the image to gray preserving transparency.
        """
        cmd = IconEditCommand(self, self.tr("Convert to Grayscale"),
                              self.__image)
        for x in range(self.__image.width()):
            for y in range(self.__image.height()):
                col = self.__image.pixel(x, y)
                if col != qRgba(0, 0, 0, 0):
                    gray = qGray(col)
                    self.__image.setPixel(x, y,
                                          qRgba(gray, gray, gray, qAlpha(col)))
        self.update()
        self.setDirty(True)
        self.__undoStack.push(cmd)
        cmd.setAfterImage(self.__image)

    def editUndo(self):
        """
        Public slot to perform an undo operation.
        """
        if self.__undoStack.canUndo():
            self.__undoStack.undo()

    def editRedo(self):
        """
        Public slot to perform a redo operation.
        """
        if self.__undoStack.canRedo():
            self.__undoStack.redo()

    def canUndo(self):
        """
        Public method to return the undo status.
        
        @return flag indicating the availability of undo (boolean)
        """
        return self.__undoStack.canUndo()

    def canRedo(self):
        """
        Public method to return the redo status.
        
        @return flag indicating the availability of redo (boolean)
        """
        return self.__undoStack.canRedo()

    def __cleanChanged(self, clean):
        """
        Private slot to handle the undo stack clean state change.
        
        @param clean flag indicating the clean state (boolean)
        """
        self.setDirty(not clean)

    def shutdown(self):
        """
        Public slot to perform some shutdown actions.
        """
        self.__undoStack.canRedoChanged.disconnect(self.canRedoChanged)
        self.__undoStack.canUndoChanged.disconnect(self.canUndoChanged)
        self.__undoStack.cleanChanged.disconnect(self.__cleanChanged)

    def isSelectionAvailable(self):
        """
        Public method to check the availability of a selection.
        
        @return flag indicating the availability of a selection (boolean)
        """
        return self.__selectionAvailable
Ejemplo n.º 4
0
class PFSNet(QWidget):
    changed = pyqtSignal()

    def __init__(self, id: str, window, tempName=None):
        super(QWidget, self).__init__()
        self._filename = None
        self._filepath = None
        self._tempName = tempName
        self._id = id
        layout = QHBoxLayout()
        self._tab = QTabWidget()
        self._tab.currentChanged.connect(self.changeTab)
        self._tab.setTabsClosable(True)
        self._tab.tabCloseRequested.connect(self.closeTab)
        layout.addWidget(self._tab)
        self.setLayout(layout)
        self._prop = QTableWidget(20, 2)
        self._prop.itemChanged.connect(self.propertiesItemChanged)
        self._prop.verticalHeader().hide()
        self._prop.setColumnWidth(1, 180)
        self._prop.setMaximumWidth(300)
        lv = QVBoxLayout()
        lv.addWidget(self._prop)
        self._tree = QTreeWidget()
        self._tree.itemClicked.connect(self.treeItemClicked)
        self._tree.setMaximumWidth(300)
        lv.addWidget(self._tree)
        layout.addLayout(lv)
        self._pages = []
        self._idPage = 0
        self._sm = window._sm
        self._window = window
        self._distributorId = 0
        self._activityId = 0
        self._relationId = 0
        self._otherId = 0
        self._pageId = 0
        self._page = None
        self._elements = {}
        self.undoStack = QUndoStack(self)
        self.undoAction = self.undoStack.createUndoAction(self, "Desfazer")
        self.undoAction.setShortcuts(QKeySequence.Undo)
        self.undoAction.setIcon(
            QIcon.fromTheme("edit-undo", QIcon("icons/edit-undo.svg")))
        self.redoAction = self.undoStack.createRedoAction(self, "Refazer")
        self.redoAction.setShortcuts(QKeySequence.Redo)
        self.redoAction.setIcon(
            QIcon.fromTheme("edit-redo", QIcon("icons/edit-redo.svg")))
        self._pasteList = []

    def tree(self):
        tree = QTreeWidgetItem(self._tree, ["Net " + self._id], 0)
        child = self._page.tree(tree)
        self._tree.expandAll()
        return tree

    def prepareTree(self):
        self._tree.clear()
        self.tree()

    def showPage(self, widget):
        if widget in self._pages:
            self._tab.setCurrentWidget(widget)
        else:
            self._tab.addTab(widget, widget.name())
            self._pages.append(widget)
            self._tab.setCurrentWidget(widget)

    def removeTabWidget(self, widget):
        for i in range(self._tab.count()):
            if self._tab.widget(i) == widget:
                self._tab.removeTab(i)
                self._pages.remove(widget)

    def propertiesItemChanged(self, item: PFSTableValueText):
        if item.comparePrevious():
            item.edited.emit(item)

    def getAllPages(self):
        ans = []
        ans.append(self._page)
        aux = self._page.getAllSubPages()
        if len(aux) > 0:
            ans = ans + aux
        return ans

    def generateXml(self, xml: QXmlStreamWriter):
        xml.writeStartDocument()
        xml.writeStartElement("PetriNetDoc")
        xml.writeStartElement("net")
        xml.writeAttribute("id", self._id)
        pages = self.getAllPages()
        for p in pages:
            p.generateXml(xml)
        xml.writeEndElement()
        xml.writeEndElement()
        xml.writeEndDocument()

    def treeItemClicked(self, item, col):
        if isinstance(item, PFSTreeItem):
            item.clicked.emit()

    def createFromXml(doc: QDomDocument, window):
        el = doc.documentElement()
        nodes = el.childNodes()
        nets = []
        for i in range(nodes.count()):
            node = nodes.at(i)
            if node.nodeName() != "net":
                continue
            if not (node.hasAttributes() and node.attributes().contains("id")):
                continue
            id = node.attributes().namedItem("id").nodeValue()
            net = PFSNet(id, window)
            nodesPage = node.childNodes()
            pages = []
            for j in range(nodesPage.count()):
                nodePage = nodesPage.at(j)
                if nodePage.nodeName() != "page":
                    continue
                page = PFSPage.createFromXml(nodePage)
                if page is not None:
                    pages.append(page)
            if len(pages) == 0:
                continue
            aux = {}
            for page in pages:
                p = PFSPage.createFromContent(page, window._sm, net)
                if p is not None:
                    i = page._ref
                    if i is None or not i:
                        i = "main"
                    aux[i] = p
            if "main" not in aux.keys():
                continue
            for indice, page in aux.items():
                ids = page.getMaxIds()
                if net._activityId < ids[0] + 1:
                    net._activityId = ids[0] + 1
                if net._distributorId < ids[1] + 1:
                    net._distributorId = ids[1] + 1
                if net._relationId < ids[2] + 1:
                    net._relationId = ids[2] + 1
                if net._otherId < ids[3] + 1:
                    net._otherId = ids[3] + 1
                if net._pageId < int(page._id[1:]) + 1:
                    net._pageId = int(page._id[1:]) + 1
                if indice == "main":
                    net._tab.blockSignals(True)
                    net._tab.addTab(page, page.name())
                    net._tab.blockSignals(False)
                for indice2, page2 in aux.items():
                    elem = page2.getElementById(indice)
                    if elem is not None:
                        page._subRef = elem
                        elem.setSubPage(page)
                        page.setName("Ref_" + elem._id)
                        break
            net._page = aux["main"]
            net._pages.append(aux["main"])
            net.tree()
            nets.append(net)
        return nets

    def getTabName(self) -> str:
        if self._filename is None and self._tempName is None:
            ans = "New model"
        elif self._filename is None:
            ans = self._tempName
        else:
            ans = self._filename
        if self.undoStack.isClean():
            return ans
        return ans + "*"

    def newNet(id, window, tempName="newmodel.xml"):
        ans = PFSNet(id, window, tempName)
        page = PFSPage.newPage(ans.requestId(PFSPage), window._sm, ans)
        ans._page = page
        ans._pages.append(page)
        ans._tab.addTab(page, page.name())
        return ans

    def openPage(self, element):
        if isinstance(element, PFSPage):
            page = element
        elif isinstance(element, PFSActivity):
            page = element.subPage()
        else:
            return
        if page not in self._pages:
            self._tab.addTab(page, page.name())
            self._pages.append(page)
        self._tab.setCurrentWidget(page)

    def createPage(self, element=None):
        page = PFSPage.newPage(self.requestId(PFSPage), self._sm, self, 600,
                               120)
        if element is not None and element.setSubPage(page):
            page.setName("Ref_" + element._id)
            page._subRef = element
            openac = PFSOpenActivity(self.requestId(PFSOpenActivity), 20, 10,
                                     100)
            self.addItemNoUndo(openac, page)
            closeac = PFSCloseActivity(self.requestId(PFSCloseActivity),
                                       page._scene.sceneRect().width() - 20,
                                       10, 100)
            self.addItemNoUndo(closeac, page)
            self._idPage = self._idPage + 1
            self._sm.fixTransitions(page._scene)
            return page
        return None

    def deleteElements(self):
        if len(self._pages) == 0:
            return
        scene = self._tab.currentWidget()._scene
        itemsSeleted = scene.selectedItems()
        if len(itemsSeleted) == 0:
            if self._tab.currentWidget() == self._page:
                return
            x = PFSUndoDeletePage(self._tab.currentWidget())
            self.undoStack.push(x)
            return
        itemsDeleted = []
        for item in itemsSeleted:
            if not item.canDelete():
                continue
            if isinstance(item, PFSNode):
                item.deleted.emit()
            itemsDeleted.append(item)
        if len(itemsDeleted) > 0:
            x = PFSUndoDelete(itemsDeleted)
            self.undoStack.push(x)

    def pasteElements(self, elements):
        self._pasteList = elements

    def pasteItems(self, pos):
        ans = []
        aux = {}
        for elem in self._pasteList:
            if isinstance(elem, PFSRelationContent) or isinstance(
                    elem, PFSSecondaryFlowContent):
                continue
            oldId = elem._id
            id = self.requestId(elem)
            if isinstance(elem, PFSActivityContent):
                e = PFSActivity.paste(elem, id, pos.x(), pos.y())
                ans.append(e)
                aux[oldId] = e
            elif isinstance(elem, PFSDistributorContent):
                e = PFSDistributor.paste(elem, id, pos.x(), pos.y())
                ans.append(e)
                aux[oldId] = e
        for elem in self._pasteList:
            if isinstance(elem, PFSRelationContent):
                oldId = elem._id
                id = self.requestId(elem)
                e = PFSRelation.paste(elem, id, pos.x(), pos.y(), aux)
                ans.append(e)
            elif isinstance(elem, PFSSecondaryFlowContent):
                oldId = elem._id
                id = self.requestId(elem)
                e = PFSSecondaryFlow.paste(elem, id, pos.x(), pos.y(), aux)
                ans.append(e)
        x = PFSUndoAdd(ans, self._tab.currentWidget()._scene)
        self.undoStack.push(x)

    def export(self, filename):
        if len(self._pages) > 1:
            scene = self._tab.currentWidget()._scene
        elif len(self._pages) == 1:
            scene = self._pages[0]._scene
        else:
            return
        if filename.endswith(".png"):
            PFSImage.gravaPng(scene, filename)
        else:
            PFSImage.gravaSvg(scene, filename)

    def addItem(self, element, page: PFSPage):
        if isinstance(element, PFSRelation):
            if isinstance(element._source, PFSActive) and isinstance(
                    element._target, PFSActive):
                return False
            if isinstance(element._source, PFSPassive) and isinstance(
                    element._target, PFSPassive):
                return False
        x = PFSUndoAdd([element], page._scene)
        self.undoStack.push(x)
        return True

    def addItemNoUndo(self, element, page: PFSPage):
        if isinstance(element, PFSRelation):
            if isinstance(element._source, PFSActive) and isinstance(
                    element._target, PFSActive):
                return False
            if isinstance(element._source, PFSPassive) and isinstance(
                    element._target, PFSPassive):
                return False
        page._scene.addItem(element)
        page._scene.update()
        return True

    def requestId(self, element):
        if element == PFSActivity or isinstance(element, PFSActivityContent):
            ans = "A" + str(self._activityId)
            self._activityId = self._activityId + 1
        elif element == PFSDistributor or isinstance(element,
                                                     PFSDistributorContent):
            ans = "D" + str(self._distributorId)
            self._distributorId = self._distributorId + 1
        elif element == PFSRelation:
            ans = "R" + str(self._relationId)
            self._relationId = self._relationId + 1
        elif element == PFSPage:
            ans = "P" + str(self._pageId)
            self._pageId = self._pageId + 1
        else:
            ans = "O" + str(self._otherId)
            self._otherId = self._otherId + 1
        return ans

    def changeTab(self, index: int):
        self._prop.clear()
        self._tree.clear()
        self.tree()
        if index < 0:
            return
        self._tab.widget(index)._scene.clearSelection()
        self._window._main.tabChanged.emit()

    def fillProperties(self, props):
        if len(props) > 0:
            self._prop.setRowCount(0)
            self._prop.setRowCount(len(props))
            i = 0
            for line in props:
                if isinstance(line[0], QTableWidgetItem):
                    self._prop.setItem(i, 0, line[0])
                else:
                    self._prop.setCellWidget(i, 0, line[0])
                if isinstance(line[1], QTableWidgetItem):
                    self._prop.setItem(i, 1, line[1])
                else:
                    self._prop.setCellWidget(i, 1, line[1])
                if isinstance(line[0], PFSTableLabelTags):
                    self._prop.setRowHeight(i, 100)
                i = i + 1

    def closeTab(self, ind):
        w = self._tab.widget(ind)
        self._pages.remove(w)
        self._tab.removeTab(ind)
Ejemplo n.º 5
0
class MapDocument(QObject):
    fileNameChanged = pyqtSignal(str, str)
    modifiedChanged = pyqtSignal()
    saved = pyqtSignal()
    ##
    # Emitted when the selected tile region changes. Sends the currently
    # selected region and the previously selected region.
    ##
    selectedAreaChanged = pyqtSignal(QRegion, QRegion)
    ##
    # Emitted when the list of selected objects changes.
    ##
    selectedObjectsChanged = pyqtSignal()
    ##
    # Emitted when the list of selected tiles from the dock changes.
    ##
    selectedTilesChanged = pyqtSignal()
    currentObjectChanged = pyqtSignal(list)
    ##
    # Emitted when the map size or its tile size changes.
    ##
    mapChanged = pyqtSignal()
    layerAdded = pyqtSignal(int)
    layerAboutToBeRemoved = pyqtSignal(int)
    layerRenamed = pyqtSignal(int)
    layerRemoved = pyqtSignal(int)
    layerChanged = pyqtSignal(int)
    ##
    # Emitted after a new layer was added and the name should be edited.
    # Applies to the current layer.
    ##
    editLayerNameRequested = pyqtSignal()
    editCurrentObject = pyqtSignal()
    ##
    # Emitted when the current layer index changes.
    ##
    currentLayerIndexChanged = pyqtSignal(int)
    ##
    # Emitted when a certain region of the map changes. The region is given in
    # tile coordinates.
    ##
    regionChanged = pyqtSignal(QRegion, Layer)
    ##
    # Emitted when a certain region of the map was edited by user input.
    # The region is given in tile coordinates.
    # If multiple layers have been edited, multiple signals will be emitted.
    ##
    regionEdited = pyqtSignal(QRegion, Layer)
    tileLayerDrawMarginsChanged = pyqtSignal(TileLayer)
    tileTerrainChanged = pyqtSignal(QList)
    tileProbabilityChanged = pyqtSignal(Tile)
    tileObjectGroupChanged = pyqtSignal(Tile)
    tileAnimationChanged = pyqtSignal(Tile)
    objectGroupChanged = pyqtSignal(ObjectGroup)
    imageLayerChanged = pyqtSignal(ImageLayer)
    tilesetAboutToBeAdded = pyqtSignal(int)
    tilesetAdded = pyqtSignal(int, Tileset)
    tilesetAboutToBeRemoved = pyqtSignal(int)
    tilesetRemoved = pyqtSignal(Tileset)
    tilesetMoved = pyqtSignal(int, int)
    tilesetFileNameChanged = pyqtSignal(Tileset)
    tilesetNameChanged = pyqtSignal(Tileset)
    tilesetTileOffsetChanged = pyqtSignal(Tileset)
    tilesetChanged = pyqtSignal(Tileset)
    objectsAdded = pyqtSignal(QList)
    objectsInserted = pyqtSignal(ObjectGroup, int, int)
    objectsRemoved = pyqtSignal(QList)
    objectsChanged = pyqtSignal(QList)
    objectsIndexChanged = pyqtSignal(ObjectGroup, int, int)
    propertyAdded = pyqtSignal(Object, str)
    propertyRemoved = pyqtSignal(Object, str)
    propertyChanged = pyqtSignal(Object, str)
    propertiesChanged = pyqtSignal(Object)

    ##
    # Constructs a map document around the given map. The map document takes
    # ownership of the map.
    ##
    def __init__(self, map, fileName = QString()):
        super().__init__()

        ##
        # The filename of a plugin is unique. So it can be used to determine
        # the right plugin to be used for saving or reloading the map.
        # The nameFilter of a plugin can not be used, since it's translatable.
        # The filename of a plugin must not change while maps are open using this
        # plugin.
        ##
        self.mReaderFormat = None
        self.mWriterFormat = None
        self.mExportFormat = None
        self.mSelectedArea = QRegion()
        self.mSelectedObjects = QList()
        self.mSelectedTiles = QList()
        self.mCurrentLayerIndex = 0
        self.mLastSaved = QDateTime()
        self.mLastExportFileName = ''

        self.mFileName = fileName
        self.mMap = map
        self.mLayerModel = LayerModel(self)
        self.mCurrentObject = map ## Current properties object. ##
        self.mRenderer = None
        self.mMapObjectModel = MapObjectModel(self)
        self.mTerrainModel = TerrainModel(self, self)
        self.mUndoStack = QUndoStack(self)
        self.createRenderer()
        if (map.layerCount() == 0):
            _x = -1
        else:
            _x = 0
        self.mCurrentLayerIndex = _x
        self.mLayerModel.setMapDocument(self)
        # Forward signals emitted from the layer model
        self.mLayerModel.layerAdded.connect(self.onLayerAdded)
        self.mLayerModel.layerAboutToBeRemoved.connect(self.onLayerAboutToBeRemoved)
        self.mLayerModel.layerRemoved.connect(self.onLayerRemoved)
        self.mLayerModel.layerChanged.connect(self.layerChanged)
        # Forward signals emitted from the map object model
        self.mMapObjectModel.setMapDocument(self)
        self.mMapObjectModel.objectsAdded.connect(self.objectsAdded)
        self.mMapObjectModel.objectsChanged.connect(self.objectsChanged)
        self.mMapObjectModel.objectsRemoved.connect(self.onObjectsRemoved)
        self.mMapObjectModel.rowsInserted.connect(self.onMapObjectModelRowsInserted)
        self.mMapObjectModel.rowsRemoved.connect(self.onMapObjectModelRowsInsertedOrRemoved)
        self.mMapObjectModel.rowsMoved.connect(self.onObjectsMoved)
        self.mTerrainModel.terrainRemoved.connect(self.onTerrainRemoved)
        self.mUndoStack.cleanChanged.connect(self.modifiedChanged)
        # Register tileset references
        tilesetManager = TilesetManager.instance()
        tilesetManager.addReferences(self.mMap.tilesets())

    ##
    # Destructor.
    ##
    def __del__(self):
        # Unregister tileset references
        tilesetManager = TilesetManager.instance()
        tilesetManager.removeReferences(self.mMap.tilesets())
        del self.mRenderer
        del self.mMap

    ##
    # Saves the map to its current file name. Returns whether or not the file
    # was saved successfully. If not, <i>error</i> will be set to the error
    # message if it is not 0.
    ##
    def save(self, *args):
        l = len(args)
        if l==0:
            args = ('')
        if l==1:
            arg = args[0]
            file = QFileInfo(arg)
            if not file.isFile():
                fileName = self.fileName()
                error = args[0]
            else:
                fileName = arg
                error = ''
            return self.save(fileName, error)
        if l==2:
            ##
            # Saves the map to the file at \a fileName. Returns whether or not the
            # file was saved successfully. If not, <i>error</i> will be set to the
            # error message if it is not 0.
            #
            # If the save was successful, the file name of this document will be set
            # to \a fileName.
            #
            # The map format will be the same as this map was opened with.
            ##
            fileName, error = args
            mapFormat = self.mWriterFormat
            
            tmxMapFormat = TmxMapFormat()
            if (not mapFormat):
                mapFormat = tmxMapFormat
            if (not mapFormat.write(self.map(), fileName)):
                if (error):
                   error = mapFormat.errorString()
                return False

            self.undoStack().setClean()
            self.setFileName(fileName)
            self.mLastSaved = QFileInfo(fileName).lastModified()
            self.saved.emit()
            return True

    ##
    # Loads a map and returns a MapDocument instance on success. Returns 0
    # on error and sets the \a error message.
    ##
    def load(fileName, mapFormat = None):
        error = ''
        tmxMapFormat = TmxMapFormat()
        
        if (not mapFormat and not tmxMapFormat.supportsFile(fileName)):
            # Try to find a plugin that implements support for this format
            formats = PluginManager.objects()
            for format in formats:
                if (format.supportsFile(fileName)):
                    mapFormat = format
                    break

        map = None
        errorString = ''

        if mapFormat:
            map = mapFormat.read(fileName)
            errorString = mapFormat.errorString()
        else:
            map = tmxMapFormat.read(fileName)
            errorString = tmxMapFormat.errorString()

        if (not map):
            error = errorString
            return None, error

        mapDocument = MapDocument(map, fileName)
        if mapFormat:
            mapDocument.setReaderFormat(mapFormat)
            if mapFormat.hasCapabilities(MapFormat.Write):
                mapDocument.setWriterFormat(mapFormat)

        return mapDocument, error

    def fileName(self):
        return self.mFileName

    def lastExportFileName(self):
        return self.mLastExportFileName

    def setLastExportFileName(self, fileName):
        self.mLastExportFileName = fileName

    def readerFormat(self):
        return self.mReaderFormat

    def setReaderFormat(self, format):
        self.mReaderFormat = format

    def writerFormat(self):
        return self.mWriterFormat

    def setWriterFormat(self, format):
        self.mWriterFormat = format

    def exportFormat(self):
        return self.mExportFormat

    def setExportFormat(self, format):
        self.mExportFormat = format
        
    ##
    # Returns the name with which to display this map. It is the file name without
    # its path, or 'untitled.tmx' when the map has no file name.
    ##
    def displayName(self):
        displayName = QFileInfo(self.mFileName).fileName()
        if len(displayName)==0:
            displayName = self.tr("untitled.tmx")
        return displayName

    ##
    # Returns whether the map has unsaved changes.
    ##
    def isModified(self):
        return not self.mUndoStack.isClean()

    def lastSaved(self):
        return self.mLastSaved
        
    ##
    # Returns the map instance. Be aware that directly modifying the map will
    # not allow the GUI to update itself appropriately.
    ##
    def map(self):
        return self.mMap

    ##
    # Sets the current layer to the given index.
    ##
    def setCurrentLayerIndex(self, index):
        changed = self.mCurrentLayerIndex != index
        self.mCurrentLayerIndex = index
        ## This function always sends the following signal, even if the index
        # didn't actually change. This is because the selected index in the layer
        # table view might be out of date anyway, and would otherwise not be
        # properly updated.
        #
        # This problem happens due to the selection model not sending signals
        # about changes to its current index when it is due to insertion/removal
        # of other items. The selected item doesn't change in that case, but our
        # layer index does.
        ##
        self.currentLayerIndexChanged.emit(self.mCurrentLayerIndex)
        if (changed and self.mCurrentLayerIndex != -1):
            self.setCurrentObject(self.currentLayer())

    ##
    # Returns the index of the currently selected layer. Returns -1 if no
    # layer is currently selected.
    ##
    def currentLayerIndex(self):
        return self.mCurrentLayerIndex

    ##
    # Returns the currently selected layer, or 0 if no layer is currently
    # selected.
    ##
    def currentLayer(self):
        if (self.mCurrentLayerIndex == -1):
            return None
        return self.mMap.layerAt(self.mCurrentLayerIndex)

    ##
    # Resize this map to the given \a size, while at the same time shifting
    # the contents by \a offset.
    ##
    def resizeMap(self, size, offset):
        movedSelection = self.mSelectedArea.translated(offset)
        newArea = QRect(-offset, size)
        visibleArea = self.mRenderer.boundingRect(newArea)
        origin = self.mRenderer.tileToPixelCoords_(QPointF())
        newOrigin = self.mRenderer.tileToPixelCoords_(-offset)
        pixelOffset = origin - newOrigin
        # Resize the map and each layer
        self.mUndoStack.beginMacro(self.tr("Resize Map"))
        for i in range(self.mMap.layerCount()):
            layer = self.mMap.layerAt(i)
            x = layer.layerType()
            if x==Layer.TileLayerType:
                tileLayer = layer
                self.mUndoStack.push(ResizeTileLayer(self, tileLayer, size, offset))
            elif x==Layer.ObjectGroupType:
                objectGroup = layer
                # Remove objects that will fall outside of the map
                for o in objectGroup.objects():
                    if (not visibleIn(visibleArea, o, self.mRenderer)):
                        self.mUndoStack.push(RemoveMapObject(self, o))
                    else:
                        oldPos = o.position()
                        newPos = oldPos + pixelOffset
                        self.mUndoStack.push(MoveMapObject(self, newPos, oldPos))
            elif x==Layer.ImageLayerType:
                # Currently not adjusted when resizing the map
                break

        self.mUndoStack.push(ResizeMap(self, size))
        self.mUndoStack.push(ChangeSelectedArea(self, movedSelection))
        self.mUndoStack.endMacro()
        # TODO: Handle layers that don't match the map size correctly

    ##
    # Offsets the layers at \a layerIndexes by \a offset, within \a bounds,
    # and optionally wraps on the X or Y axis.
    ##
    def offsetMap(self, layerIndexes, offset, bounds, wrapX, wrapY):
        if (layerIndexes.empty()):
            return
        if (layerIndexes.size() == 1):
            self.mUndoStack.push(OffsetLayer(self, layerIndexes.first(), offset,
                                             bounds, wrapX, wrapY))
        else:
            self.mUndoStack.beginMacro(self.tr("Offset Map"))
            for layerIndex in layerIndexes:
                self.mUndoStack.push(OffsetLayer(self, layerIndex, offset,
                                                 bounds, wrapX, wrapY))

            self.mUndoStack.endMacro()

    ##
    # Flips the selected objects in the given \a direction.
    ##
    def flipSelectedObjects(self, direction):
        if (self.mSelectedObjects.isEmpty()):
            return
        self.mUndoStack.push(FlipMapObjects(self, self.mSelectedObjects, direction))

    ##
    # Rotates the selected objects.
    ##
    def rotateSelectedObjects(self, direction):
        if (self.mSelectedObjects.isEmpty()):
            return
        self.mUndoStack.beginMacro(self.tr("Rotate %n Object(s)", "", self.mSelectedObjects.size()))
        # TODO: Rotate them properly as a group
        for mapObject in self.mSelectedObjects:
            oldRotation = mapObject.rotation()
            newRotation = oldRotation
            if (direction == RotateDirection.RotateLeft):
                newRotation -= 90
                if (newRotation < -180):
                    newRotation += 360
            else:
                newRotation += 90
                if (newRotation > 180):
                    newRotation -= 360

            self.mUndoStack.push(RotateMapObject(self, mapObject, newRotation, oldRotation))

        self.mUndoStack.endMacro()

    ##
    # Adds a layer of the given type to the top of the layer stack. After adding
    # the new layer, emits editLayerNameRequested().
    ##
    def addLayer(self, layerType):
        layer = None
        name = QString()
        x = layerType
        if x==Layer.TileLayerType:
            name = self.tr("Tile Layer %d"%(self.mMap.tileLayerCount() + 1))
            layer = TileLayer(name, 0, 0, self.mMap.width(), self.mMap.height())
        elif x==Layer.ObjectGroupType:
            name = self.tr("Object Layer %d"%(self.mMap.objectGroupCount() + 1))
            layer = ObjectGroup(name, 0, 0, self.mMap.width(), self.mMap.height())
        elif x==Layer.ImageLayerType:
            name = self.tr("Image Layer %d"%(self.mMap.imageLayerCount() + 1))
            layer = ImageLayer(name, 0, 0, self.mMap.width(), self.mMap.height())

        index = self.mMap.layerCount()
        self.mUndoStack.push(AddLayer(self, index, layer))
        self.setCurrentLayerIndex(index)
        self.editLayerNameRequested.emit()

    ##
    # Duplicates the currently selected layer.
    ##
    def duplicateLayer(self):
        if (self.mCurrentLayerIndex == -1):
            return
        duplicate = self.mMap.layerAt(self.mCurrentLayerIndex).clone()
        duplicate.setName(self.tr("Copy of %s"%duplicate.name()))
        index = self.mCurrentLayerIndex + 1
        cmd = AddLayer(self, index, duplicate)
        cmd.setText(self.tr("Duplicate Layer"))
        self.mUndoStack.push(cmd)
        self.setCurrentLayerIndex(index)

    ##
    # Merges the currently selected layer with the layer below. This only works
    # when the layers can be merged.
    #
    # \see Layer.canMergeWith
    ##
    def mergeLayerDown(self):
        if (self.mCurrentLayerIndex < 1):
            return
        upperLayer = self.mMap.layerAt(self.mCurrentLayerIndex)
        lowerLayer = self.mMap.layerAt(self.mCurrentLayerIndex - 1)
        if (not lowerLayer.canMergeWith(upperLayer)):
            return
        merged = lowerLayer.mergedWith(upperLayer)
        self.mUndoStack.beginMacro(self.tr("Merge Layer Down"))
        self.mUndoStack.push(AddLayer(self, self.mCurrentLayerIndex - 1, merged))
        self.mUndoStack.push(RemoveLayer(self, self.mCurrentLayerIndex))
        self.mUndoStack.push(RemoveLayer(self, self.mCurrentLayerIndex))
        self.mUndoStack.endMacro()

    ##
    # Moves the given layer up. Does nothing when no valid layer index is
    # given.
    ##
    def moveLayerUp(self, index):
        if index<0 or index>=self.mMap.layerCount() - 1:
            return
        self.mUndoStack.push(MoveLayer(self, index, MoveLayer.Up))

    ##
    # Moves the given layer down. Does nothing when no valid layer index is
    # given.
    ##
    def moveLayerDown(self, index):
        if index<1 or index>=self.mMap.layerCount():
            return
        self.mUndoStack.push(MoveLayer(self, index, MoveLayer.Down))

    ##
    # Removes the given layer.
    ##
    def removeLayer(self, index):
        if index<0 or index>=self.mMap.layerCount():
            return
        self.mUndoStack.push(RemoveLayer(self, index))

    ##
    # Show or hide all other layers except the layer at the given index.
    # If any other layer is visible then all layers will be hidden, otherwise
    # the layers will be shown.
    ##
    def toggleOtherLayers(self, index):
        self.mLayerModel.toggleOtherLayers(index)

    ##
    # Adds a tileset to this map at the given \a index. Emits the appropriate
    # signal.
    ##
    def insertTileset(self, index, tileset):
        self.tilesetAboutToBeAdded.emit(index)
        self.mMap.insertTileset(index, tileset)
        tilesetManager = TilesetManager.instance()
        tilesetManager.addReference(tileset)
        self.tilesetAdded.emit(index, tileset)

    ##
    # Removes the tileset at the given \a index from this map. Emits the
    # appropriate signal.
    #
    # \warning Does not make sure that any references to tiles in the removed
    #          tileset are cleared.
    ##
    def removeTilesetAt(self, index):
        self.tilesetAboutToBeRemoved.emit(index)
        tileset = self.mMap.tilesets().at(index)
        if (tileset == self.mCurrentObject or isFromTileset(self.mCurrentObject, tileset)):
            self.setCurrentObject(None)
        self.mMap.removeTilesetAt(index)
        self.tilesetRemoved.emit(tileset)
        tilesetManager = TilesetManager.instance()
        tilesetManager.removeReference(tileset)

    def moveTileset(self, _from, to):
        if (_from == to):
            return
        tileset = self.mMap.tilesets().at(_from)
        self.mMap.removeTilesetAt(_from)
        self.mMap.insertTileset(to, tileset)
        self.tilesetMoved.emit(_from, to)

    def setTilesetFileName(self, tileset, fileName):
        tileset.setFileName(fileName)
        self.tilesetFileNameChanged.emit(tileset)

    def setTilesetName(self, tileset, name):
        tileset.setName(name)
        self.tilesetNameChanged.emit(tileset)

    def setTilesetTileOffset(self, tileset, tileOffset):
        tileset.setTileOffset(tileOffset)
        self.mMap.recomputeDrawMargins()
        self.tilesetTileOffsetChanged.emit(tileset)

    def duplicateObjects(self, objects):
        if (objects.isEmpty()):
            return
        self.mUndoStack.beginMacro(self.tr("Duplicate %n Object(s)", "", objects.size()))
        clones = QList()
        for mapObject in objects:
            clone = mapObject.clone()
            clones.append(clone)
            self.mUndoStack.push(AddMapObject(self,
                                              mapObject.objectGroup(),
                                              clone))

        self.mUndoStack.endMacro()
        self.setSelectedObjects(clones)

    def removeObjects(self, objects):
        if (objects.isEmpty()):
            return
        self.mUndoStack.beginMacro(self.tr("Remove %n Object(s)", "", objects.size()))
        for mapObject in objects:
            self.mUndoStack.push(RemoveMapObject(self, mapObject))
        self.mUndoStack.endMacro()

    def moveObjectsToGroup(self, objects, objectGroup):
        if (objects.isEmpty()):
            return
        self.mUndoStack.beginMacro(self.tr("Move %n Object(s) to Layer", "",
                                  objects.size()))
        for mapObject in objects:
            if (mapObject.objectGroup() == objectGroup):
                continue
            self.mUndoStack.push(MoveMapObjectToGroup(self,
                                                      mapObject,
                                                      objectGroup))

        self.mUndoStack.endMacro()

    def setProperty(self, object, name, value):
        hadProperty = object.hasProperty(name)
        object.setProperty(name, value)
        if (hadProperty):
            self.propertyChanged.emit(object, name)
        else:
            self.propertyAdded.emit(object, name)

    def setProperties(self, object, properties):
        object.setProperties(properties)
        self.propertiesChanged.emit(object)

    def removeProperty(self, object, name):
        object.removeProperty(name)
        self.propertyRemoved.emit(object, name)

    ##
    # Returns the layer model. Can be used to modify the layer stack of the
    # map, and to display the layer stack in a view.
    ##
    def layerModel(self):
        return self.mLayerModel

    def mapObjectModel(self):
        return self.mMapObjectModel

    def terrainModel(self):
        return self.mTerrainModel

    ##
    # Returns the map renderer.
    ##
    def renderer(self):
        return self.mRenderer

    ##
    # Creates the map renderer. Should be called after changing the map
    # orientation.
    ##
    def createRenderer(self):
        if (self.mRenderer):
            del self.mRenderer
        x = self.mMap.orientation()
        if x==Map.Orientation.Isometric:
            self.mRenderer = IsometricRenderer(self.mMap)
        elif x==Map.Orientation.Staggered:
            self.mRenderer = StaggeredRenderer(self.mMap)
        elif x==Map.Orientation.Hexagonal:
            self.mRenderer = HexagonalRenderer(self.mMap)
        else:
            self.mRenderer = OrthogonalRenderer(self.mMap)

    ##
    # Returns the undo stack of this map document. Should be used to push any
    # commands on that modify the map.
    ##
    def undoStack(self):
        return self.mUndoStack

    ##
    # Returns the selected area of tiles.
    ##
    def selectedArea(self):
        return QRegion(self.mSelectedArea)

    ##
    # Sets the selected area of tiles.
    ##
    def setSelectedArea(self, selection):
        if (self.mSelectedArea != selection):
            oldSelectedArea = self.mSelectedArea
            self.mSelectedArea = selection
            self.selectedAreaChanged.emit(self.mSelectedArea, oldSelectedArea)

    ##
    # Returns the list of selected objects.
    ##
    def selectedObjects(self):
        return self.mSelectedObjects

    ##
    # Sets the list of selected objects, emitting the selectedObjectsChanged
    # signal.
    ##
    def setSelectedObjects(self, selectedObjects):
        if selectedObjects.nequal(self.mSelectedObjects):
            self.mSelectedObjects = selectedObjects
            self.selectedObjectsChanged.emit()
            if (selectedObjects.size() == 1):
                self.setCurrentObject(selectedObjects.first())

    ##
    # Returns the list of selected tiles.
    ##
    def selectedTiles(self):
        return self.mSelectedTiles

    def setSelectedTiles(self, selectedTiles):
        self.mSelectedTiles = selectedTiles
        self.selectedTilesChanged.emit()

    def currentObject(self):
        return self.mCurrentObject

    def setCurrentObject(self, object):
        if (object == self.mCurrentObject):
            return
        self.mCurrentObject = object
        self.currentObjectChanged.emit([object])

    def currentObjects(self):
        objects = QList()
        if (self.mCurrentObject):
            if (self.mCurrentObject.typeId() == Object.MapObjectType and not self.mSelectedObjects.isEmpty()):
                for mapObj in self.mSelectedObjects:
                    objects.append(mapObj)
            elif (self.mCurrentObject.typeId() == Object.TileType and not self.mSelectedTiles.isEmpty()):
                for tile in self.mSelectedTiles:
                    objects.append(tile)

            else:
                objects.append(self.mCurrentObject)

        return objects

    def unifyTilesets(self, *args):
        l = len(args)
        if l==1:
            ##
            # Makes sure the all tilesets which are used at the given \a map will be
            # present in the map document.
            #
            # To reach the aim, all similar tilesets will be replaced by the version
            # in the current map document and all missing tilesets will be added to
            # the current map document.
            #
            # \warning This method assumes that the tilesets in \a map are managed by
            #          the TilesetManager!
            ##
            map = args[0]
            undoCommands = QList()
            existingTilesets = self.mMap.tilesets()
            tilesetManager = TilesetManager.instance()
            # Add tilesets that are not yet part of this map
            for tileset in map.tilesets():
                if (existingTilesets.contains(tileset)):
                    continue
                replacement = tileset.findSimilarTileset(existingTilesets)
                if (not replacement):
                    undoCommands.append(AddTileset(self, tileset))
                    continue

                # Merge the tile properties
                sharedTileCount = min(tileset.tileCount(), replacement.tileCount())
                for i in range(sharedTileCount):
                    replacementTile = replacement.tileAt(i)
                    properties = replacementTile.properties()
                    properties.merge(tileset.tileAt(i).properties())
                    undoCommands.append(ChangeProperties(self,
                                                             self.tr("Tile"),
                                                             replacementTile,
                                                             properties))

                map.replaceTileset(tileset, replacement)
                tilesetManager.addReference(replacement)
                tilesetManager.removeReference(tileset)

            if (not undoCommands.isEmpty()):
                self.mUndoStack.beginMacro(self.tr("Tileset Changes"))
                for command in undoCommands:
                    self.mUndoStack.push(command)
                self.mUndoStack.endMacro()
        elif l==2:
            map, missingTilesets = args
            
            existingTilesets = self.mMap.tilesets()
            tilesetManager = TilesetManager.instance()

            for tileset in map.tilesets():
                # tileset already added
                if existingTilesets.contains(tileset):
                    continue

                replacement = tileset.findSimilarTileset(existingTilesets)

                # tileset not present and no replacement tileset found
                if not replacement:
                    if not missingTilesets.contains(tileset):
                        missingTilesets.append(tileset)
                    continue

                # replacement tileset found, change given map
                map.replaceTileset(tileset, replacement)

                tilesetManager.addReference(replacement)
                tilesetManager.removeReference(tileset)

    ##
    # Emits the map changed signal. This signal should be emitted after changing
    # the map size or its tile size.
    ##
    def emitMapChanged(self):
        self.mapChanged.emit()

    ##
    # Emits the region changed signal for the specified region. The region
    # should be in tile coordinates. This method is used by the TilePainter.
    ##
    def emitRegionChanged(self, region, layer):
        self.regionChanged.emit(region, layer)

    ##
    # Emits the region edited signal for the specified region and tile layer.
    # The region should be in tile coordinates. This should be called from
    # all map document changing classes which are triggered by user input.
    ##
    def emitRegionEdited(self, region, layer):
        self.regionEdited.emit(region, layer)

    def emitTileLayerDrawMarginsChanged(self, layer):
        self.tileLayerDrawMarginsChanged.emit(layer)

    ##
    # Emits the tileset changed signal. This signal is currently used when adding
    # or removing tiles from a tileset.
    #
    # @todo Emit more specific signals.
    ##
    def emitTilesetChanged(self, tileset):
        self.tilesetChanged.emit(tileset)

    ##
    # Emits the signal notifying about the terrain probability of a tile changing.
    ##
    def emitTileProbabilityChanged(self, tile):
        self.tileProbabilityChanged.emit(tile)

    ##
    # Emits the signal notifying tileset models about changes to tile terrain
    # information. All the \a tiles need to be from the same tileset.
    ##
    def emitTileTerrainChanged(self, tiles):
        if (not tiles.isEmpty()):
            self.tileTerrainChanged.emit(tiles)

    ##
    # Emits the signal notifying the TileCollisionEditor about the object group
    # of a tile changing.
    ##
    def emitTileObjectGroupChanged(self, tile):
        self.tileObjectGroupChanged.emit(tile)

    ##
    # Emits the signal notifying about the animation of a tile changing.
    ##
    def emitTileAnimationChanged(self, tile):
        self.tileAnimationChanged.emit(tile)

    ##
    # Emits the objectGroupChanged signal, should be called when changing the
    # color or drawing order of an object group.
    ##
    def emitObjectGroupChanged(self, objectGroup):
        self.objectGroupChanged.emit(objectGroup)

    ##
    # Emits the imageLayerChanged signal, should be called when changing the
    # image or the transparent color of an image layer.
    ##
    def emitImageLayerChanged(self, imageLayer):
        self.imageLayerChanged.emit(imageLayer)

    ##
    # Emits the editLayerNameRequested signal, to get renamed.
    ##
    def emitEditLayerNameRequested(self):
        self.editLayerNameRequested.emit()

    ##
    # Emits the editCurrentObject signal, which makes the Properties window become
    # visible and take focus.
    ##
    def emitEditCurrentObject(self):
        self.editCurrentObject.emit()

    ##
    # Before forwarding the signal, the objects are removed from the list of
    # selected objects, triggering a selectedObjectsChanged signal when
    # appropriate.
    ##
    def onObjectsRemoved(self, objects):
        self.deselectObjects(objects)
        self.objectsRemoved.emit(objects)

    def onMapObjectModelRowsInserted(self, parent, first, last):
        objectGroup = self.mMapObjectModel.toObjectGroup(parent)
        if (not objectGroup): # we're not dealing with insertion of objects
            return
        self.objectsInserted.emit(objectGroup, first, last)
        self.onMapObjectModelRowsInsertedOrRemoved(parent, first, last)

    def onMapObjectModelRowsInsertedOrRemoved(self, parent, first, last):
        objectGroup = self.mMapObjectModel.toObjectGroup(parent)
        if (not objectGroup):
            return
        # Inserting or removing objects changes the index of any that come after
        lastIndex = objectGroup.objectCount() - 1
        if (last < lastIndex):
            self.objectsIndexChanged.emit(objectGroup, last + 1, lastIndex)

    def onObjectsMoved(self, parent, start, end, destination, row):
        if (parent != destination):
            return
        objectGroup = self.mMapObjectModel.toObjectGroup(parent)
        # Determine the full range over which object indexes changed
        first = min(start, row)
        last = max(end, row - 1)
        self.objectsIndexChanged.emit(objectGroup, first, last)

    def onLayerAdded(self, index):
        self.layerAdded.emit(index)
        # Select the first layer that gets added to the map
        if (self.mMap.layerCount() == 1):
            self.setCurrentLayerIndex(0)

    def onLayerAboutToBeRemoved(self, index):
        layer = self.mMap.layerAt(index)
        if (layer == self.mCurrentObject):
            self.setCurrentObject(None)
        # Deselect any objects on this layer when necessary
        og = layer
        if type(og) == ObjectGroup:
            self.deselectObjects(og.objects())
        self.layerAboutToBeRemoved.emit(index)

    def onLayerRemoved(self, index):
        # Bring the current layer index to safety
        currentLayerRemoved = self.mCurrentLayerIndex == self.mMap.layerCount()
        if (currentLayerRemoved):
            self.mCurrentLayerIndex = self.mCurrentLayerIndex - 1
        self.layerRemoved.emit(index)
        # Emitted after the layerRemoved signal so that the MapScene has a chance
        # of synchronizing before adapting to the newly selected index
        if (currentLayerRemoved):
            self.currentLayerIndexChanged.emit(self.mCurrentLayerIndex)

    def onTerrainRemoved(self, terrain):
        if (terrain == self.mCurrentObject):
            self.setCurrentObject(None)

    def setFileName(self, fileName):
        if (self.mFileName == fileName):
            return
        oldFileName = self.mFileName
        self.mFileName = fileName
        self.fileNameChanged.emit(fileName, oldFileName)

    def deselectObjects(self, objects):
        # Unset the current object when it was part of this list of objects
        if (self.mCurrentObject and self.mCurrentObject.typeId() == Object.MapObjectType):
            if (objects.contains(self.mCurrentObject)):
                self.setCurrentObject(None)
        removedCount = 0
        for object in objects:
            removedCount += self.mSelectedObjects.removeAll(object)
        if (removedCount > 0):
            self.selectedObjectsChanged.emit()

    def disconnect(self):
        try:
            super().disconnect()
        except:
            pass
Ejemplo n.º 6
0
class Document(QWidget, ProjectDocument):
    mark_item_created = pyqtSignal(MarkItem)
    added_mark_item = pyqtSignal(Project, MarkItem)
    browser_result_signal = pyqtSignal(bool)
    selected_mark_item_changed = pyqtSignal(MarkItem)

    def __init__(self,
                 gadget,
                 toolbar_gadget,
                 file_name=None,
                 project_name=None,
                 image_path=None,
                 person_name=None,
                 parent=None,
                 eraser_size=3,
                 eraser_option=SelectionOptionToolBar.Subtract):
        super(Document, self).__init__(parent)
        ProjectDocument.__init__(self, parent=parent)

        self._writer_format = ProjectFormat()
        self._reader_format = ProjectFormat()
        self._export_format = ProjectFormat()

        self._mark_item_to_outline_item = {}
        self._modifier = False

        self._project = Project(image_path, file_name, project_name,
                                person_name)
        self._image_path = image_path if image_path else self._project.image_path

        self._current_tool = gadget
        self._selection_option = toolbar_gadget
        self._eraser_size = eraser_size
        self._eraser_option = eraser_option

        self.__current_index = -1
        self.__mouse_press_index = -1
        self.__mouse_press_offset = QPoint()
        self.__resize_handel_pressed = False
        self.__undo_stack = QUndoStack(self)
        self._selection_item = None
        self._history_widget = None
        self._history_project_manager = None
        self._mark_item_manager = MarkItemManager()
        self._mark_item_manager.selected_item_changed.connect(
            self.selected_mark_item_changed)

        # # 创建场景
        self._workbench_scene.setObjectName("workbench_scene")

        self._is_big_img = False
        # if self._is_big_img:
        #     self.workbench_view = LoadIMGraphicsView(self._mark_item_manager, gadget, toolbar_gadget, eraser_size,
        #                                              image_path, self._workbench_scene, parent=self)
        # else:
        self.workbench_view = GraphicsViewTest(self._mark_item_manager,
                                               gadget,
                                               toolbar_gadget,
                                               eraser_size,
                                               parent=self)
        # 把场景添加到视图中
        self.workbench_view.setScene(self._workbench_scene)

        self.workbench_view.setObjectName("workbench_view")
        self.workbench_view.setContentsMargins(0, 0, 0, 0)
        self.workbench_view.setBackgroundBrush(QColor(147, 147, 147))

        # 布局
        self.tab_vertical_layout = QVBoxLayout(self)
        self._splitter1 = QSplitter(self)
        self._splitter1.setStyleSheet("margin: 0px")
        self._splitter1.addWidget(self.workbench_view)

        self._splitter2 = QSplitter(self)
        self._splitter2.setOrientation(Qt.Vertical)
        self._splitter2.setStyleSheet("margin: 0px")
        self._splitter2.addWidget(self._splitter1)

        self.tab_vertical_layout.addWidget(self._splitter2)
        self.tab_vertical_layout.setContentsMargins(0, 0, 0, 0)

        # 当前选择小工具
        self.change_gadget(gadget)

        # 信号接收
        self.workbench_view.border_moved_signal.connect(self.border_moved)
        self.workbench_view.border_created.connect(self.created_border)
        self.workbench_view.about_to_create_border.connect(
            self.about_to_create_border)
        self.workbench_view.eraser_action_signal.connect(self.eraser_action)

        if all([image_path, project_name, file_name]) and not self._is_big_img:
            self.create_document()

    @property
    def is_big_img(self):
        return self._is_big_img

    def about_to_cmp(self, project_documents: ProjectDocument = None):
        if not self._history_widget:
            self._history_project_manager = HistoryProjectManager(
                project_documents)

            self._history_widget = Thumbnail(self._history_project_manager,
                                             self)
            self._history_project_manager.set_scene(
                self._history_widget.current_project())
            self._splitter1.addWidget(self._history_project_manager.get_view())
            self._splitter2.addWidget(self._history_widget)

            self.workbench_view.set_is_comparing(True)
            self._history_project_manager.get_view().set_is_comparing(True)

            self._history_widget.selected_project_changed.connect(
                self._selected_history_project_changed)
            self._history_widget.close_event_signal.connect(
                self._toggle_cmp_history)
            self._history_widget.synchronize_changed_signal.connect(
                self._toggle_synchronize_view)

            items = project_documents[0].project().get_mark_items()
            for item in items:
                self._project.add_mark_item(item)

        else:
            self._toggle_cmp_history(True)
            self._history_widget.setHidden(False)

        if True:
            self.connect_to_synchronize_view()

    def had_cmp(self):
        return bool(self._history_widget)

    def _toggle_synchronize_view(self, is_synchronize: bool):
        if is_synchronize:
            self.connect_to_synchronize_view()
        else:
            self.disconnect_to_asynchronous_view()

    def connect_to_synchronize_view(self):
        self._history_project_manager.synchronize_with_origin_view(
            self.workbench_view)
        self._history_project_manager.get_view().connect_to_synchronize_with(
            self.workbench_view)
        self.workbench_view.connect_to_synchronize_with(
            self._history_project_manager.get_view())

    def disconnect_to_asynchronous_view(self):
        self._history_project_manager.get_view(
        ).disconnect_to_asynchronous_with(self.workbench_view)
        self.workbench_view.disconnect_to_asynchronous_with(
            self._history_project_manager.get_view())

    def _toggle_cmp_history(self, is_on: bool):
        self._history_project_manager.hidden_view(not is_on)
        self.workbench_view.set_is_comparing(is_on)
        self._history_project_manager.get_view().set_is_comparing(is_on)

    def _selected_history_project_changed(self, project_doc: ProjectDocument):
        self._history_project_manager.set_scene(project_doc)
        self._history_project_manager.synchronize_with_origin_view(
            self.workbench_view)

    def modifier(self):
        return not self.__undo_stack.isClean()

    def set_project(self, project: Project):
        self._project = project
        if not self._image_path:
            self._image_path = project.image_path
        if not self._is_big_img:
            self.load_document(self._image_path)
        for mark_item in self._project.get_mark_items():
            self.add_mark_item(mark_item)

    def set_current_mark_item(self, mark_item: MarkItem):
        """"""
        if not mark_item:
            return
        item = [
            item for item in self._workbench_scene.items()
            if isinstance(item, OutlineItem) and item.mark_item() == mark_item
        ]
        if item:
            self._mark_item_manager.set_selected_item(item[0])
            self.workbench_view.centerOn(item[0])

    def delete_mark_item(self, mark_item: [MarkItem, OutlineItem]):
        if not mark_item:
            return
        if isinstance(mark_item, MarkItem):
            if self._mark_item_manager.selected_mark_item().mark_item(
            ) == mark_item:
                self._mark_item_manager.set_selected_item(None)
            self._project.remove_mark_item(mark_item)
            item = [
                item for item in self._workbench_scene.items() if
                isinstance(item, OutlineItem) and item.mark_item() == mark_item
            ]
            if item:
                self._mark_item_manager.unregister_mark_item(
                    mark_item.item_name)
                self._workbench_scene.removeItem(item[0])
                del item[0]

        elif isinstance(mark_item, OutlineItem):
            if self._mark_item_manager.selected_mark_item() == mark_item:
                self._mark_item_manager.set_selected_item(None)
            self._project.remove_mark_item(mark_item.mark_item())
            self._mark_item_manager.unregister_mark_item(mark_item.item_name)
            self._workbench_scene.removeItem(mark_item)
            del mark_item

    def project(self) -> Project:
        return self._project

    def project_name(self):
        return self._project.project_name

    def undo_stack(self):
        return self.__undo_stack

    def create_document(self):
        self.load_document(self._image_path)
        self.save_project()

    def save_project(self):
        self.writer_format.save_project(self._project)
        self.__undo_stack.clear()

    def export_result(self, path, progress):
        self.writer_format.export_result(path, self._project,
                                         self._image.size(), self)

    def get_file_name(self):
        return self._project.project_full_path()

    def get_project_name(self):
        return self._project.parent()

    def about_to_create_border(self):
        if self._selection_option == SelectionOptionToolBar.Replace:
            self._workbench_scene.removeItem(self._selection_item)
            self._selection_item = None

    def cancel_selection(self):
        self._workbench_scene.removeItem(self._selection_item)
        self._selection_item.disconnect()
        self._selection_item = None

    def selection_as_mark_item(self):
        """TODO"""

    def created_border(self, border: SelectionItem):

        if self._selection_option == SelectionOptionToolBar.Replace:
            self._workbench_scene.removeItem(self._selection_item)
            self._selection_item = border

            self.__undo_stack.push(
                AddSelectionItem(self._workbench_scene, self._selection_item))

        elif self._selection_option == SelectionOptionToolBar.Subtract:
            if self._selection_item:
                self._selection_item -= border
        elif self._selection_option == SelectionOptionToolBar.Add:
            if not self._selection_item:
                self._selection_item = border
            else:
                self._selection_item += border
        elif self._selection_option == SelectionOptionToolBar.Intersect:
            if self._selection_item:
                self._selection_item &= border

        if self._selection_item:
            if self._selection_item.is_empty():
                self._workbench_scene.removeItem(self._selection_item)
                self._selection_item = None
                return

            self.workbench_view.view_zoom_signal.connect(
                self._selection_item.set_pen_width_by_scale)
            self._selection_item.cancel_selection_signal.connect(
                self.cancel_selection)
            self._selection_item.as_mark_item_signal.connect(
                self.selection_as_mark_item)
            self._selection_item.reverse_select_signal.connect(
                self._select_reverser_path)

    def add_border_item(self, item: SelectionItem):
        self.__undo_stack.push(AddItemCommand(self._workbench_scene, item))

    def border_moved(self, item: SelectionItem):
        self.__undo_stack.push(MoveItemCommand(item))

    def change_toolbar_gadget(self, toolbar_gadget: QAction):
        self._selection_option = toolbar_gadget.data()

    def change_eraser_option(self, option_action: QAction):
        self._eraser_option = option_action.data()

    def change_gadget(self, tool: QAction):
        if isinstance(tool, QAction):
            tool = tool.data()

        self.workbench_view.set_gadget(tool)
        if tool == ToolsToolBar.BrowserImageTool:
            self.browser_result()
            self.browser_result_signal.emit(True)
        else:
            if self._current_tool == ToolsToolBar.BrowserImageTool:
                self.end_browser()
                self.browser_result_signal.emit(False)
        self._current_tool = tool

    def eraser_size_changed(self, eraser_size: int):
        self._eraser_size = eraser_size
        self.workbench_view.set_eraser_size(eraser_size)

    def browser_result(self):
        self.workbench_view.setBackgroundBrush(QColor(Qt.black))
        self._pixmap_item.setVisible(False)
        if self.workbench_view.is_comparing():
            self._history_project_manager.browser_result()
        if self._selection_item:
            self._selection_item.setVisible(False)

    def end_browser(self):
        self.workbench_view.setBackgroundBrush(QColor(147, 147, 147))
        self._pixmap_item.setVisible(True)
        if self.workbench_view.is_comparing():
            self._history_project_manager.end_browser()
        if self._selection_item:
            self._selection_item.setVisible(True)

    def get_sub_image_in(self, item: SelectionItem) -> [QImage, None]:

        rect = item.rectangle()
        if self.is_big_img:
            """"""
            # slide_helper = SlideHelper(self.project().image_path)
            # image_from_rect = ImgFromRect(rect, slide_helper)
            # image_from_rect = image_from_rect.area_img
            # return image_from_rect
        else:

            rect_sub_image = self._image.copy(rect)
            polygon_path = item.get_path()
            polygon_sub_image = rect_sub_image

            for row in range(0, rect.width()):
                for clo in range(0, rect.height()):
                    point = QPoint(row, clo)
                    if not polygon_path.contains(point):
                        polygon_sub_image.setPixel(point, 0)
            return polygon_sub_image

    def ai_delete_outline(self, detect_policy):

        result_numpy_array = None
        width_num_array = None

        if not self._selection_item:
            image = self._image
        else:
            image = self._image.copy(self._selection_item.rectangle())

        if detect_policy == 5:
            for h in range(0, image.height(), 256):
                for w in range(0, image.width(), 256):
                    image_ = self._image.copy(QRect(w, h, 255, 255))
                    image_ = qimage2numpy(image_)
                    result = detect_one(image_)
                    numpy_array = mat_to_img(result)
                    if width_num_array is not None:
                        width_num_array = np.hstack(
                            (width_num_array, numpy_array))
                    else:
                        width_num_array = numpy_array

                if result_numpy_array is not None:
                    result_numpy_array = np.vstack(
                        (result_numpy_array, width_num_array))
                else:
                    result_numpy_array = width_num_array
                width_num_array = None

            print(result_numpy_array.shape)
            return result_numpy_array

    def _get_outlines(self, numpy_array, detect_policy):
        outline_path1 = QPainterPath()
        outline_path2 = QPainterPath()
        outline1, outline2 = detect_outline(detect_policy,
                                            numpy_array,
                                            drop_area=80)

        for array in outline1:
            sub_path = []
            for point in array[0]:
                point = self._selection_item.mapToScene(
                    point[0][0], point[0][1])
                sub_path.append(point)

            polygon = QPolygonF(sub_path)
            path = QPainterPath()
            path.addPolygon(polygon)
            outline_path1 += path

        for array in outline2:
            sub_path = []
            for point in array[0]:
                point = self._selection_item.mapToScene(
                    point[0][0], point[0][1])
                sub_path.append(point)

            polygon = QPolygonF(sub_path)
            path = QPainterPath()
            path.addPolygon(polygon)
            outline_path2 += path

        return outline_path1, outline_path2

    def _get_outline_by_no_selection(self, numpy_array, detect_policy):
        outline_path1 = QPainterPath()
        outline_path2 = QPainterPath()
        outline1, outline2 = detect_outline(detect_policy,
                                            numpy_array,
                                            drop_area=80)

        for array in outline1:
            sub_path = []
            for point in array[0]:
                point = QPoint(point[0][0], point[0][1])
                sub_path.append(point)

            polygon = QPolygonF(sub_path)
            path = QPainterPath()
            path.addPolygon(polygon)
            outline_path1 += path

        for array in outline2:
            sub_path = []
            for point in array[0]:
                point = QPoint(point[0][0], point[0][1])
                sub_path.append(point)

            polygon = QPolygonF(sub_path)
            path = QPainterPath()
            path.addPolygon(polygon)
            outline_path2 += path

        return outline_path1, outline_path2

    def _to_create_mark_item(self, outline_path1, outline_path2):
        use_outline1_flag = True
        if not outline_path1.isEmpty():
            self.create_mark_item(outline_path1)
        elif not outline_path2.isEmpty():
            self.create_mark_item(outline_path2)
            use_outline1_flag = False
        if self._selection_item:
            self._selection_item.setFlag(QGraphicsItem.ItemIsMovable, False)
            if use_outline1_flag and not outline_path2.isEmpty():
                self._selection_item.set_reverser_path(outline_path2)

    def detect_outline(self, detect_policy):
        """
        将选中的选区对应的部分图片copy出来,然后转为ndarray类型
        用来转为OpenCV识别轮廓的输入数据
        :param detect_policy: 用哪种识别算法识别轮廓
        :return: None
        """

        if detect_policy >= 5:

            numpy_array = self.ai_delete_outline(detect_policy)
            outline_path1, outline_path2 = self._get_outline_by_no_selection(
                numpy_array, detect_policy)

            if not self._selection_item:
                self._selection_item = SelectionItem(
                    QPoint(0, 0), self._workbench_scene,
                    self.workbench_view.transform().m11())
                path = QPainterPath()
                path.addRect(
                    QRectF(0, 0, self._image.width(), self._image.height()))
                self._selection_item.set_item_path_by_path(path)
                self._selection_item.reverse_select_signal.connect(
                    self._select_reverser_path)
            self._to_create_mark_item(outline_path1, outline_path2)
            return

        if not self._selection_item:
            QMessageBox.warning(self, "警告", "没有选择区域!")
            return

        if isinstance(self._selection_item, SelectionItem):

            outline_path1 = QPainterPath()
            outline_path2 = QPainterPath()
            if detect_policy == 4:
                self._workbench_scene.removeItem(self._selection_item)
                outline_path1 = self._selection_item.mapToScene(
                    self._selection_item.get_path())
                self._selection_item = None
            else:
                sub_img = self.get_sub_image_in(self._selection_item)
                if sub_img is None:
                    return
                if isinstance(sub_img, QImage):
                    sub_img = qimage2numpy(sub_img)
                outline_path1, outline_path2 = self._get_outlines(
                    sub_img, detect_policy)
            self._to_create_mark_item(outline_path1, outline_path2)

    def correction_outline(self, option):
        """"""
        if not self._selection_item:
            return
        mark_items = [
            item for item in self._workbench_scene.items(
                self._selection_item.get_scene_path())
            if isinstance(item, OutlineItem)
        ]
        for mark_item in mark_items:
            if mark_item.locked():
                continue
            elif option == 1:
                mark_item += self._selection_item
                self._mark_item_manager.set_selected_item(mark_item)
                break
            elif option == 2:
                mark_item -= self._selection_item
                self._mark_item_manager.set_selected_item(mark_item)
                break
        self._workbench_scene.removeItem(self._selection_item)
        self._selection_item = None

    def eraser_action(self, eraser_area: SelectionItem):
        if self._selection_item:
            eraser_area &= self._selection_item
        if eraser_area.is_empty():
            return

        mark_items = [
            item for item in self._workbench_scene.items(
                eraser_area.get_scene_path()) if isinstance(item, OutlineItem)
        ]

        # if self._eraser_option == SelectionOptionToolBar.Add:
        #     selected_item = self._mark_item_manager.selected_mark_item()
        #     if selected_item in mark_items:
        #         selected_item += eraser_area
        #     return

        for item in mark_items:
            if item.locked():
                continue
            item -= eraser_area
        self._workbench_scene.removeItem(eraser_area)
        del eraser_area

    def create_mark_item(self, outline: QPainterPath):
        item_name = self._mark_item_manager.get_unique_mark_item_name()
        new_mark_item = MarkItem(list(self._project.persons),
                                 item_name=item_name,
                                 outline_path=outline)
        self._project.add_mark_item(new_mark_item)
        self.add_mark_item(new_mark_item, True)

    def add_mark_item(self, mark_item: MarkItem, new_item=False):
        item = OutlineItem(mark_item, self._workbench_scene,
                           self.workbench_view.transform().m11())

        flag = True
        if new_item:
            flag = self.detect_intersect_with_others(item)

        if flag:
            self._mark_item_to_outline_item[mark_item] = item
            self.browser_result_signal.connect(item.is_browser_result)
            self.workbench_view.view_zoom_signal.connect(
                item.set_pen_width_by_scale)
            self._mark_item_manager.register_mark_item(item,
                                                       mark_item.item_name)

            self.added_mark_item.emit(self._project, mark_item)
            self._mark_item_manager.set_selected_item(item)

    def _select_reverser_path(self):
        if self._selection_item:
            item = self._project.get_mark_items()[-1]
            reverse_path = self._selection_item.get_reverse_path()
            self._selection_item.set_reverser_path(item.get_outline())
            item.set_outline(reverse_path)

    def detect_intersect_with_others(self, new_item: OutlineItem):

        selection_path = new_item.get_scene_path()
        mark_items = [
            item for item in self._workbench_scene.items(selection_path)
            if isinstance(item, OutlineItem)
        ]

        for mark_item in mark_items:
            if mark_item != new_item:
                new_item -= mark_item

        if new_item.get_path().isEmpty():
            self._workbench_scene.removeItem(new_item)
            del new_item
            return False
        else:
            new_item.get_path().closeSubpath()
            return True

    def paint_make_item(self, mark_item: MarkItem):

        pen = QPen()
        pen.setWidth(1)
        pen.setColor(Qt.yellow)
        self._workbench_scene.addPath(mark_item.draw_path(), pen)

    @property
    def writer_format(self):
        return self._reader_format

    @writer_format.setter
    def writer_format(self, new_writer_format):
        self._writer_format = new_writer_format

    @property
    def reader_format(self):
        return self._reader_format

    @reader_format.setter
    def reader_format(self, new_reader_format):
        self._reader_format = new_reader_format

    @property
    def export_format(self):
        return self._export_format

    @export_format.setter
    def export_format(self, new_export_format):
        self._export_format = new_export_format
Ejemplo n.º 7
0
class GMPage:

    @property
    def document(self):
        return self._doc

    @property
    def index(self):
        return self._index

    @property
    def page_image(self):
        return self._page_image

    @property
    def drawing_image(self):
        return self._drawing_image

    @property
    def command_count(self):
        return self._undo_stack.count()

    @property
    def can_undo(self):
        return self._undo_stack.canUndo()

    @property
    def can_redo(self):
        return self._undo_stack.canRedo()

    def __init__(self, document, mu_page, index):
        assert isinstance(document, GMDoc)
        assert isinstance(mu_page, fitz.Page)
        assert type(index) is int

        self._doc = document
        self._index = index

        self._mu_page = mu_page

        # zoom in to get higher resolution pixmap
        # todo: add this param to preferences dialog
        zoom = 2
        matrix = fitz.Matrix(zoom, zoom)

        page_data = self._mu_page.getPixmap(matrix=matrix, alpha=False).getImageData()

        self._undo_stack = QUndoStack()

        self._page_image = QPixmap()
        self._page_image.loadFromData(page_data)
        self._drawing_image = generateDrawingPixmap(self._page_image)
        self._last_drawing_hash = None

        self.captureState()

    def getData(self, alpha=False):
        return self._mu_page.getPixmap(alpha=alpha).getImageData()

    def getSize(self):
        return self._page_image.size()

    def setDrawingPixmap(self, pixmap):
        self._drawing_image = pixmap

    def getMergedPixmap(self):
        result = QPixmap(self.getSize())
        result.fill(QColor(255, 255, 255))

        painter = QPainter(result)
        painter.setRenderHint(QPainter.HighQualityAntialiasing, True)
        painter.drawPixmap(QPoint(0, 0), self._page_image)
        painter.drawPixmap(QPoint(0, 0), self._drawing_image)
        painter.end()

        return result

    def getUndoAction(self, parent):
        return self._undo_stack.createUndoAction(parent)

    def getRedoAction(self, parent):
        return self._undo_stack.createRedoAction(parent)

    def pushCommand(self, command):
        assert isinstance(command, QUndoCommand)
        self._undo_stack.push(command)

    def captureState(self):
        self._last_drawing_hash = self._drawing_image.cacheKey()
        self._undo_stack.setClean()

    def changed(self):
        if self._undo_stack.isClean():
            return False

        return self._undo_stack.count() > 0

    def undo(self):
        self._undo_stack.undo()

    def redo(self):
        self._undo_stack.redo()
Ejemplo n.º 8
0
class AbstractPage(QObject):
    sigChanged = pyqtSignal()

    def __init__(self, parent: MasterDocument):
        super().__init__()
        self._objs = set()
        self._parent = parent
        self.undoStack = QUndoStack()
        self._name = "untitled"

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, n):
        self._name = n
        self.sigChanged.emit()

    @property
    def parentDoc(self):
        return self._parent

    def isModified(self):
        return not self.undoStack.isClean()

    def doCommand(self, cmd):
        cmd.doc = self
        self.undoStack.push(cmd)
        self.sigChanged.emit()

    def objects(self, objType=None):
        if not objType:
            return set(self._objs)
        return {obj for obj in self._objs if type(obj) is objType}

    def hasObject(self, obj):
        return obj in self._objs

    def findObjsInRect(self, rect: QRect, objType=None):
        return {
            obj
            for obj in self.objects(objType) if rect.intersects(obj.bbox())
        }

    def findObjsNear(self, pt: QPoint, dist=1, objType=None):
        hitRect = QRect(pt.x() - dist / 2, pt.y() - dist / 2, dist, dist)
        return {
            obj
            for obj in self.findObjsInRect(hitRect, objType)
            if obj.testHit(pt, dist)
        }

    def addObj(self, obj):
        self._objs.add(obj)
        self.sigChanged.emit()

    def removeObj(self, obj):
        self._objs.remove(obj)
        self.sigChanged.emit()

    @pyqtSlot()
    def undo(self):
        self.undoStack.undo()
        self.sigChanged.emit()

    @pyqtSlot()
    def redo(self):
        self.undoStack.redo()
        self.sigChanged.emit()

    def fromXml(self, pageNode):
        raise NotImplementedError()

    def toXml(self, parentNode):
        raise NotImplementedError()
Ejemplo n.º 9
0
class SchmereoMainWindow(QtWidgets.QMainWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.ui = uic.loadUi(
            uifile=pkg_resources.resource_stream("schmereo", "schmereo.ui"),
            baseinstance=self,
        )
        # Platform-specific semantic keyboard shortcuts cannot be set in Qt Designer
        self.ui.actionNew.setShortcut(QKeySequence.New)
        self.ui.actionOpen.setShortcut(QKeySequence.Open)
        self.ui.actionQuit.setShortcut(
            QKeySequence.Quit)  # no effect on Windows
        self.ui.actionSave.setShortcut(QKeySequence.Save)
        self.ui.actionSave_Project_As.setShortcut(QKeySequence.SaveAs)
        self.ui.actionZoom_In.setShortcuts(
            [QKeySequence.ZoomIn,
             "Ctrl+="])  # '=' so I don't need to press SHIFT
        self.ui.actionZoom_Out.setShortcut(QKeySequence.ZoomOut)

        #
        self.recent_files = RecentFileList(
            open_file_slot=self.load_file,
            settings_key="recent_files",
            menu=self.ui.menuRecent_Files,
        )
        # Link views
        self.shared_camera = Camera()
        self.ui.leftImageWidget.camera = self.shared_camera
        self.ui.rightImageWidget.camera = self.shared_camera
        #
        self.ui.leftImageWidget.file_dropped.connect(self.load_left_file)
        self.ui.rightImageWidget.file_dropped.connect(self.load_right_file)
        #
        self.ui.leftImageWidget.image.transform.center = FractionalImagePos(
            -0.5, 0)
        self.ui.rightImageWidget.image.transform.center = FractionalImagePos(
            +0.5, 0)
        #
        for w in (self.ui.leftImageWidget, self.ui.rightImageWidget):
            w.messageSent.connect(self.ui.statusbar.showMessage)
        #
        self.marker_set = list()
        self.zoom_increment = 1.10
        self.image_saver = ImageSaver(self.ui.leftImageWidget,
                                      self.ui.rightImageWidget)
        # TODO: object for AddMarker tool button
        tb = self.ui.addMarkerToolButton
        tb.setDefaultAction(self.ui.actionAdd_Marker)
        sz = 32
        tb.setFixedSize(sz, sz)
        tb.setIconSize(QtCore.QSize(sz, sz))
        hb = self.ui.handModeToolButton
        hb.setDefaultAction(self.ui.actionHand_Mode)
        hb.setFixedSize(sz, sz)
        hb.setIconSize(QtCore.QSize(sz, sz))
        _set_action_icon(
            self.ui.actionAdd_Marker,
            "schmereo.marker",
            "crosshair64.png",
            "crosshair64blue.png",
        )
        _set_action_icon(self.ui.actionHand_Mode, "schmereo",
                         "cursor-openhand20.png")
        # tb.setDragEnabled(True)  # TODO: drag tool button to place marker
        self.marker_manager = MarkerManager(self)
        self.aligner = Aligner(self)
        self.project_file_name = None
        #
        self.undo_stack = QUndoStack(self)
        undo_action = self.undo_stack.createUndoAction(self, '&Undo')
        undo_action.setShortcuts(QKeySequence.Undo)
        redo_action = self.undo_stack.createRedoAction(self, '&Redo')
        redo_action.setShortcuts(QKeySequence.Redo)
        self.undo_stack.cleanChanged.connect(self.on_undoStack_cleanChanged)
        #
        self.ui.menuEdit.insertAction(self.ui.actionAlign_Now, undo_action)
        self.ui.menuEdit.insertAction(self.ui.actionAlign_Now, redo_action)
        self.ui.menuEdit.insertSeparator(self.ui.actionAlign_Now)
        self.clip_box = ClipBox(parent=self,
                                camera=self.shared_camera,
                                images=[i.image for i in self.eye_widgets()])
        self.ui.actionResolve_Clip_Box.triggered.connect(
            self.recenter_clip_box)
        for w in self.eye_widgets():
            w.undo_stack = self.undo_stack
            w.clip_box = self.clip_box
            self.clip_box.changed.connect(w.update)
        self.project_folder = None

    def check_save(self) -> bool:
        if self.undo_stack.isClean():
            return True  # OK to do whatever now
        result = QMessageBox.warning(
            self,
            "The project has been modified.",
            "The project has been modified.\n"
            "Do you want to save your changes?",
            QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel,
            QMessageBox.Save,
        )
        if result == QMessageBox.Save:
            if self.project_file_name is None:
                return self.on_actionSave_Project_As_triggered()
            else:
                return self.save_project_file(self.project_file_name)
        elif result == QMessageBox.Discard:
            return True  # OK to do whatever now
        elif result == QMessageBox.Cancel:
            return False
        else:  # Unexpected to get here?
            return False  # cancel / abort

    def closeEvent(self, event: QCloseEvent):
        if self.check_save():
            event.accept()
        else:
            event.ignore()

    def eye_widgets(self):
        for w in (self.ui.leftImageWidget, self.ui.rightImageWidget):
            yield w

    def keyReleaseEvent(self, event: QtGui.QKeyEvent):
        if event.key() == Qt.Key_Escape:
            self.marker_manager.set_marker_mode(False)

    def load_left_file(self, file_name: str) -> None:
        self.load_file(file_name)

    def load_right_file(self, file_name: str) -> None:
        self.load_file(file_name)

    @QtCore.pyqtSlot(str)
    def load_file(self, file_name: str) -> bool:
        result = False
        self.log_message(f"Loading file {file_name}...")
        try:
            image = Image.open(file_name)
        except OSError:
            return self.load_project(file_name)
        result = self.ui.leftImageWidget.load_image(file_name)
        if result:
            result = self.ui.rightImageWidget.load_image(file_name)
        if result:
            self.ui.leftImageWidget.update()
            self.ui.rightImageWidget.update()
            self.recent_files.add_file(file_name)
            self.project_folder = os.path.dirname(file_name)
        else:
            self.log_message(f"ERROR: Image load failed.")
        return result

    def load_project(self, file_name):
        with open(file_name, "r") as fh:
            data = json.load(fh)
            self.from_dict(data)
            for w in self.eye_widgets():
                w.update()
            self.recent_files.add_file(file_name)
            self.project_file_name = file_name
            self.project_folder = os.path.dirname(file_name)
            self.setWindowFilePath(self.project_file_name)
            self.undo_stack.clear()
            self.undo_stack.setClean()
            return True

    def log_message(self, message: str) -> None:
        self.ui.statusbar.showMessage(message)

    @QtCore.pyqtSlot()
    def on_actionAbout_Schmereo_triggered(self):
        QtWidgets.QMessageBox.about(
            self,
            "About Schmereo",
            inspect.cleandoc(f"""
                Schmereo stereograph restoration application
                Version: {__version__}
                Author: Christopher M. Bruns
                Code: https://github.com/cmbruns/schmereo
                """),
        )

    @QtCore.pyqtSlot()
    def on_actionAlign_Now_triggered(self):
        self.clip_box.recenter()
        self.undo_stack.push(AlignNowCommand(self))

    @QtCore.pyqtSlot()
    def on_actionClear_Markers_triggered(self):
        self.undo_stack.push(ClearMarkersCommand(*self.eye_widgets()))

    @QtCore.pyqtSlot()
    def on_actionNew_triggered(self):
        if not self.check_save():
            return
        self.project_folder = None
        self.project_file_name = None
        self.setWindowFilePath("untitled")
        self.shared_camera.reset()
        for w in self.eye_widgets():
            w.image.transform.reset()
        self.undo_stack.clear()
        self.undo_stack.setClean()

    @QtCore.pyqtSlot()
    def on_actionOpen_triggered(self):
        folder = None
        if folder is None:
            folder = self.project_folder
        if folder is None:
            folder = ""
        file_name, file_type = QtWidgets.QFileDialog.getOpenFileName(
            parent=self,
            caption="Load Image",
            directory=folder,
            filter="Projects and Images (*.json *.tif);;All Files (*)",
        )
        if file_name is None:
            return
        if len(file_name) < 1:
            return
        self.load_file(file_name)

    @QtCore.pyqtSlot()
    def on_actionQuit_triggered(self):
        if self.check_save():
            QtCore.QCoreApplication.quit()

    @QtCore.pyqtSlot()
    def on_actionReport_a_Problem_triggered(self):
        url = QtCore.QUrl("https://github.com/cmbruns/schmereo/issues")
        QtGui.QDesktopServices.openUrl(url)

    @QtCore.pyqtSlot()
    def on_actionSave_triggered(self):
        if self.project_file_name is None:
            return
        self.clip_box.recenter()
        self.save_project_file(self.project_file_name)

    @QtCore.pyqtSlot()
    def on_actionSave_Images_triggered(self):
        if not self.image_saver.can_save():
            return
        self.clip_box.recenter()
        path = ""
        if self.project_file_name is not None:
            path = f"{os.path.splitext(self.project_file_name)[0]}.pns"
        elif self.project_folder is not None:
            path = self.project_folder
        file_name, file_type = QtWidgets.QFileDialog.getSaveFileName(
            parent=self,
            caption="Save File(s)",
            directory=path,
            filter="3D Images (*.pns *.jps)",
        )
        if file_name is None:
            return
        if len(file_name) < 1:
            return
        bs = self.clip_box.size
        self.image_saver.eye_size = (int(bs[0]), int(bs[1]))
        self.image_saver.save_image(file_name, file_type)
        if self.project_folder is None:
            self.project_folder = os.path.dirname(file_name)

    @QtCore.pyqtSlot()
    def on_actionSave_Project_As_triggered(self) -> bool:
        path = ""
        if self.project_file_name is not None:
            path = os.path.dirname(self.project_file_name)
        elif self.project_folder is not None:
            path = self.project_folder
        file_name, file_type = QtWidgets.QFileDialog.getSaveFileName(
            parent=self,
            caption="Save Project",
            directory=path,
            filter="Schmereo Projects (*.json);;All Files (*)",
        )
        if file_name is None:
            return False
        if len(file_name) < 1:
            return False
        return self.save_project_file(file_name)

    @QtCore.pyqtSlot()
    def on_actionZoom_In_triggered(self):
        self.zoom(amount=self.zoom_increment)

    @QtCore.pyqtSlot()
    def on_actionZoom_Out_triggered(self):
        self.zoom(amount=1.0 / self.zoom_increment)

    @QtCore.pyqtSlot(bool)
    def on_undoStack_cleanChanged(self, is_clean: bool):
        self.ui.actionSave.setEnabled(not is_clean)
        doc_title = "untitled"
        if self.project_file_name is not None:
            doc_title = self.project_file_name
        if not is_clean:
            doc_title = f"{doc_title}*"
        self.setWindowFilePath(doc_title)

    def recenter_clip_box(self):
        self.clip_box.recenter()
        self.clip_box.notify()
        self.camera.notify()

    def save_project_file(self, file_name) -> bool:
        with open(file_name, "w") as fh:
            self.clip_box.recenter()
            json.dump(self.to_dict(), fh, indent=2)
            self.recent_files.add_file(file_name)
            self.setWindowFilePath(file_name)
            self.project_file_name = file_name
            self.project_folder = os.path.dirname(file_name)
            self.undo_stack.setClean()
            return True
        return False

    def to_dict(self):
        self.clip_box.recenter()  # Normalize values before serialization
        return {
            "app": {
                "name": "schmereo",
                "version": __version__
            },
            "clip_box": self.clip_box.to_dict(),
            "left": self.ui.leftImageWidget.to_dict(),
            "right": self.ui.rightImageWidget.to_dict(),
        }

    def from_dict(self, data):
        self.ui.leftImageWidget.from_dict(data["left"])
        self.ui.rightImageWidget.from_dict(data["right"])
        if "clip_box" in data:
            self.clip_box.from_dict(data["clip_box"])

    @QtCore.pyqtSlot()
    def zoom(self, amount: float):
        # In case the zoom is not linked between the two image widgets...
        widgets = (self.ui.leftImageWidget, self.ui.rightImageWidget)
        # store zoom values in case the cameras are all the same
        zooms = [w.camera.zoom for w in widgets]
        for idx, w in enumerate(widgets):
            w.camera.zoom = zooms[idx] * amount
        for w in widgets:
            w.camera.notify()  # repaint now
Ejemplo n.º 10
0
class IconEditorGrid(QWidget):
    """
    Class implementing the icon editor grid.
    
    @signal canRedoChanged(bool) emitted after the redo status has changed
    @signal canUndoChanged(bool) emitted after the undo status has changed
    @signal clipboardImageAvailable(bool) emitted to signal the availability
        of an image to be pasted
    @signal colorChanged(QColor) emitted after the drawing color was changed
    @signal imageChanged(bool) emitted after the image was modified
    @signal positionChanged(int, int) emitted after the cursor poition was
        changed
    @signal previewChanged(QPixmap) emitted to signal a new preview pixmap
    @signal selectionAvailable(bool) emitted to signal a change of the
        selection
    @signal sizeChanged(int, int) emitted after the size has been changed
    @signal zoomChanged(int) emitted to signal a change of the zoom value
    """
    canRedoChanged = pyqtSignal(bool)
    canUndoChanged = pyqtSignal(bool)
    clipboardImageAvailable = pyqtSignal(bool)
    colorChanged = pyqtSignal(QColor)
    imageChanged = pyqtSignal(bool)
    positionChanged = pyqtSignal(int, int)
    previewChanged = pyqtSignal(QPixmap)
    selectionAvailable = pyqtSignal(bool)
    sizeChanged = pyqtSignal(int, int)
    zoomChanged = pyqtSignal(int)
    
    Pencil = 1
    Rubber = 2
    Line = 3
    Rectangle = 4
    FilledRectangle = 5
    Circle = 6
    FilledCircle = 7
    Ellipse = 8
    FilledEllipse = 9
    Fill = 10
    ColorPicker = 11
    
    RectangleSelection = 20
    CircleSelection = 21
    
    MarkColor = QColor(255, 255, 255, 255)
    NoMarkColor = QColor(0, 0, 0, 0)
    
    ZoomMinimum = 100
    ZoomMaximum = 10000
    ZoomStep = 100
    ZoomDefault = 1200
    ZoomPercent = True
    
    def __init__(self, parent=None):
        """
        Constructor
        
        @param parent reference to the parent widget (QWidget)
        """
        super(IconEditorGrid, self).__init__(parent)
        
        self.setAttribute(Qt.WA_StaticContents)
        self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
        
        self.__curColor = Qt.black
        self.__zoom = 12
        self.__curTool = self.Pencil
        self.__startPos = QPoint()
        self.__endPos = QPoint()
        self.__dirty = False
        self.__selecting = False
        self.__selRect = QRect()
        self.__isPasting = False
        self.__clipboardSize = QSize()
        self.__pasteRect = QRect()
        
        self.__undoStack = QUndoStack(self)
        self.__currentUndoCmd = None
        
        self.__image = QImage(32, 32, QImage.Format_ARGB32)
        self.__image.fill(qRgba(0, 0, 0, 0))
        self.__markImage = QImage(self.__image)
        self.__markImage.fill(self.NoMarkColor.rgba())
        
        self.__compositingMode = QPainter.CompositionMode_SourceOver
        self.__lastPos = (-1, -1)
        
        self.__gridEnabled = True
        self.__selectionAvailable = False
        
        self.__initCursors()
        self.__initUndoTexts()
        
        self.setMouseTracking(True)
        
        self.__undoStack.canRedoChanged.connect(self.canRedoChanged)
        self.__undoStack.canUndoChanged.connect(self.canUndoChanged)
        self.__undoStack.cleanChanged.connect(self.__cleanChanged)
        
        self.imageChanged.connect(self.__updatePreviewPixmap)
        QApplication.clipboard().dataChanged.connect(self.__checkClipboard)
        
        self.__checkClipboard()
    
    def __initCursors(self):
        """
        Private method to initialize the various cursors.
        """
        self.__normalCursor = QCursor(Qt.ArrowCursor)
        
        pix = QPixmap(":colorpicker-cursor.xpm")
        mask = pix.createHeuristicMask()
        pix.setMask(mask)
        self.__colorPickerCursor = QCursor(pix, 1, 21)
        
        pix = QPixmap(":paintbrush-cursor.xpm")
        mask = pix.createHeuristicMask()
        pix.setMask(mask)
        self.__paintCursor = QCursor(pix, 0, 19)
        
        pix = QPixmap(":fill-cursor.xpm")
        mask = pix.createHeuristicMask()
        pix.setMask(mask)
        self.__fillCursor = QCursor(pix, 3, 20)
        
        pix = QPixmap(":aim-cursor.xpm")
        mask = pix.createHeuristicMask()
        pix.setMask(mask)
        self.__aimCursor = QCursor(pix, 10, 10)
        
        pix = QPixmap(":eraser-cursor.xpm")
        mask = pix.createHeuristicMask()
        pix.setMask(mask)
        self.__rubberCursor = QCursor(pix, 1, 16)
    
    def __initUndoTexts(self):
        """
        Private method to initialize texts to be associated with undo commands
        for the various drawing tools.
        """
        self.__undoTexts = {
            self.Pencil: self.tr("Set Pixel"),
            self.Rubber: self.tr("Erase Pixel"),
            self.Line: self.tr("Draw Line"),
            self.Rectangle: self.tr("Draw Rectangle"),
            self.FilledRectangle: self.tr("Draw Filled Rectangle"),
            self.Circle: self.tr("Draw Circle"),
            self.FilledCircle: self.tr("Draw Filled Circle"),
            self.Ellipse: self.tr("Draw Ellipse"),
            self.FilledEllipse: self.tr("Draw Filled Ellipse"),
            self.Fill: self.tr("Fill Region"),
        }
    
    def isDirty(self):
        """
        Public method to check the dirty status.
        
        @return flag indicating a modified status (boolean)
        """
        return self.__dirty
    
    def setDirty(self, dirty, setCleanState=False):
        """
        Public slot to set the dirty flag.
        
        @param dirty flag indicating the new modification status (boolean)
        @param setCleanState flag indicating to set the undo stack to clean
            (boolean)
        """
        self.__dirty = dirty
        self.imageChanged.emit(dirty)
        
        if not dirty and setCleanState:
            self.__undoStack.setClean()
    
    def sizeHint(self):
        """
        Public method to report the size hint.
        
        @return size hint (QSize)
        """
        size = self.__zoom * self.__image.size()
        if self.__zoom >= 3 and self.__gridEnabled:
            size += QSize(1, 1)
        return size
    
    def setPenColor(self, newColor):
        """
        Public method to set the drawing color.
        
        @param newColor reference to the new color (QColor)
        """
        self.__curColor = QColor(newColor)
        self.colorChanged.emit(QColor(newColor))
    
    def penColor(self):
        """
        Public method to get the current drawing color.
        
        @return current drawing color (QColor)
        """
        return QColor(self.__curColor)
    
    def setCompositingMode(self, mode):
        """
        Public method to set the compositing mode.
        
        @param mode compositing mode to set (QPainter.CompositionMode)
        """
        self.__compositingMode = mode
    
    def compositingMode(self):
        """
        Public method to get the compositing mode.
        
        @return compositing mode (QPainter.CompositionMode)
        """
        return self.__compositingMode
    
    def setTool(self, tool):
        """
        Public method to set the current drawing tool.
        
        @param tool drawing tool to be used
            (IconEditorGrid.Pencil ... IconEditorGrid.CircleSelection)
        """
        self.__curTool = tool
        self.__lastPos = (-1, -1)
        
        if self.__curTool in [self.RectangleSelection, self.CircleSelection]:
            self.__selecting = True
        else:
            self.__selecting = False
        
        if self.__curTool in [self.RectangleSelection, self.CircleSelection,
                              self.Line, self.Rectangle, self.FilledRectangle,
                              self.Circle, self.FilledCircle,
                              self.Ellipse, self.FilledEllipse]:
            self.setCursor(self.__aimCursor)
        elif self.__curTool == self.Fill:
            self.setCursor(self.__fillCursor)
        elif self.__curTool == self.ColorPicker:
            self.setCursor(self.__colorPickerCursor)
        elif self.__curTool == self.Pencil:
            self.setCursor(self.__paintCursor)
        elif self.__curTool == self.Rubber:
            self.setCursor(self.__rubberCursor)
        else:
            self.setCursor(self.__normalCursor)
    
    def tool(self):
        """
        Public method to get the current drawing tool.
        
        @return current drawing tool
            (IconEditorGrid.Pencil ... IconEditorGrid.CircleSelection)
        """
        return self.__curTool
    
    def setIconImage(self, newImage, undoRedo=False, clearUndo=False):
        """
        Public method to set a new icon image.
        
        @param newImage reference to the new image (QImage)
        @keyparam undoRedo flag indicating an undo or redo operation (boolean)
        @keyparam clearUndo flag indicating to clear the undo stack (boolean)
        """
        if newImage != self.__image:
            self.__image = newImage.convertToFormat(QImage.Format_ARGB32)
            self.update()
            self.updateGeometry()
            self.resize(self.sizeHint())
            
            self.__markImage = QImage(self.__image)
            self.__markImage.fill(self.NoMarkColor.rgba())
            
            if undoRedo:
                self.setDirty(not self.__undoStack.isClean())
            else:
                self.setDirty(False)
            
            if clearUndo:
                self.__undoStack.clear()
            
            self.sizeChanged.emit(*self.iconSize())
    
    def iconImage(self):
        """
        Public method to get a copy of the icon image.
        
        @return copy of the icon image (QImage)
        """
        return QImage(self.__image)
    
    def iconSize(self):
        """
        Public method to get the size of the icon.
        
        @return width and height of the image as a tuple (integer, integer)
        """
        return self.__image.width(), self.__image.height()
    
    def setZoomFactor(self, newZoom):
        """
        Public method to set the zoom factor in percent.
        
        @param newZoom zoom factor (integer >= 100)
        """
        newZoom = max(100, newZoom)   # must not be less than 100
        if newZoom != self.__zoom:
            self.__zoom = newZoom // 100
            self.update()
            self.updateGeometry()
            self.resize(self.sizeHint())
            self.zoomChanged.emit(int(self.__zoom * 100))
    
    def zoomFactor(self):
        """
        Public method to get the current zoom factor in percent.
        
        @return zoom factor (integer)
        """
        return self.__zoom * 100
    
    def setGridEnabled(self, enable):
        """
        Public method to enable the display of grid lines.
        
        @param enable enabled status of the grid lines (boolean)
        """
        if enable != self.__gridEnabled:
            self.__gridEnabled = enable
            self.update()
    
    def isGridEnabled(self):
        """
        Public method to get the grid lines status.
        
        @return enabled status of the grid lines (boolean)
        """
        return self.__gridEnabled
    
    def paintEvent(self, evt):
        """
        Protected method called to repaint some of the widget.
        
        @param evt reference to the paint event object (QPaintEvent)
        """
        painter = QPainter(self)
        
        if self.__zoom >= 3 and self.__gridEnabled:
            painter.setPen(self.palette().windowText().color())
            i = 0
            while i <= self.__image.width():
                painter.drawLine(
                    self.__zoom * i, 0,
                    self.__zoom * i, self.__zoom * self.__image.height())
                i += 1
            j = 0
            while j <= self.__image.height():
                painter.drawLine(
                    0, self.__zoom * j,
                    self.__zoom * self.__image.width(), self.__zoom * j)
                j += 1
        
        col = QColor("#aaa")
        painter.setPen(Qt.DashLine)
        for i in range(0, self.__image.width()):
            for j in range(0, self.__image.height()):
                rect = self.__pixelRect(i, j)
                if evt.region().intersects(rect):
                    color = QColor.fromRgba(self.__image.pixel(i, j))
                    painter.fillRect(rect, QBrush(Qt.white))
                    painter.fillRect(QRect(rect.topLeft(), rect.center()), col)
                    painter.fillRect(QRect(rect.center(), rect.bottomRight()),
                                     col)
                    painter.fillRect(rect, QBrush(color))
                
                    if self.__isMarked(i, j):
                        painter.drawRect(rect.adjusted(0, 0, -1, -1))
        
        painter.end()
    
    def __pixelRect(self, i, j):
        """
        Private method to determine the rectangle for a given pixel coordinate.
        
        @param i x-coordinate of the pixel in the image (integer)
        @param j y-coordinate of the pixel in the image (integer)
        @return rectangle for the given pixel coordinates (QRect)
        """
        if self.__zoom >= 3 and self.__gridEnabled:
            return QRect(self.__zoom * i + 1, self.__zoom * j + 1,
                         self.__zoom - 1, self.__zoom - 1)
        else:
            return QRect(self.__zoom * i, self.__zoom * j,
                         self.__zoom, self.__zoom)
    
    def mousePressEvent(self, evt):
        """
        Protected method to handle mouse button press events.
        
        @param evt reference to the mouse event object (QMouseEvent)
        """
        if evt.button() == Qt.LeftButton:
            if self.__isPasting:
                self.__isPasting = False
                self.editPaste(True)
                self.__markImage.fill(self.NoMarkColor.rgba())
                self.update(self.__pasteRect)
                self.__pasteRect = QRect()
                return
            
            if self.__curTool == self.Pencil:
                cmd = IconEditCommand(self, self.__undoTexts[self.__curTool],
                                      self.__image)
                self.__setImagePixel(evt.pos(), True)
                self.setDirty(True)
                self.__undoStack.push(cmd)
                self.__currentUndoCmd = cmd
            elif self.__curTool == self.Rubber:
                cmd = IconEditCommand(self, self.__undoTexts[self.__curTool],
                                      self.__image)
                self.__setImagePixel(evt.pos(), False)
                self.setDirty(True)
                self.__undoStack.push(cmd)
                self.__currentUndoCmd = cmd
            elif self.__curTool == self.Fill:
                i, j = self.__imageCoordinates(evt.pos())
                col = QColor()
                col.setRgba(self.__image.pixel(i, j))
                cmd = IconEditCommand(self, self.__undoTexts[self.__curTool],
                                      self.__image)
                self.__drawFlood(i, j, col)
                self.setDirty(True)
                self.__undoStack.push(cmd)
                cmd.setAfterImage(self.__image)
            elif self.__curTool == self.ColorPicker:
                i, j = self.__imageCoordinates(evt.pos())
                col = QColor()
                col.setRgba(self.__image.pixel(i, j))
                self.setPenColor(col)
            else:
                self.__unMark()
                self.__startPos = evt.pos()
                self.__endPos = evt.pos()
    
    def mouseMoveEvent(self, evt):
        """
        Protected method to handle mouse move events.
        
        @param evt reference to the mouse event object (QMouseEvent)
        """
        self.positionChanged.emit(*self.__imageCoordinates(evt.pos()))
        
        if self.__isPasting and not (evt.buttons() & Qt.LeftButton):
            self.__drawPasteRect(evt.pos())
            return
        
        if evt.buttons() & Qt.LeftButton:
            if self.__curTool == self.Pencil:
                self.__setImagePixel(evt.pos(), True)
                self.setDirty(True)
            elif self.__curTool == self.Rubber:
                self.__setImagePixel(evt.pos(), False)
                self.setDirty(True)
            elif self.__curTool in [self.Fill, self.ColorPicker]:
                pass    # do nothing
            else:
                self.__drawTool(evt.pos(), True)
    
    def mouseReleaseEvent(self, evt):
        """
        Protected method to handle mouse button release events.
        
        @param evt reference to the mouse event object (QMouseEvent)
        """
        if evt.button() == Qt.LeftButton:
            if self.__curTool in [self.Pencil, self.Rubber]:
                if self.__currentUndoCmd:
                    self.__currentUndoCmd.setAfterImage(self.__image)
                    self.__currentUndoCmd = None
            
            if self.__curTool not in [self.Pencil, self.Rubber,
                                      self.Fill, self.ColorPicker,
                                      self.RectangleSelection,
                                      self.CircleSelection]:
                cmd = IconEditCommand(self, self.__undoTexts[self.__curTool],
                                      self.__image)
                if self.__drawTool(evt.pos(), False):
                    self.__undoStack.push(cmd)
                    cmd.setAfterImage(self.__image)
                    self.setDirty(True)
    
    def __setImagePixel(self, pos, opaque):
        """
        Private slot to set or erase a pixel.
        
        @param pos position of the pixel in the widget (QPoint)
        @param opaque flag indicating a set operation (boolean)
        """
        i, j = self.__imageCoordinates(pos)
        
        if self.__image.rect().contains(i, j) and (i, j) != self.__lastPos:
            if opaque:
                painter = QPainter(self.__image)
                painter.setPen(self.penColor())
                painter.setCompositionMode(self.__compositingMode)
                painter.drawPoint(i, j)
            else:
                self.__image.setPixel(i, j, qRgba(0, 0, 0, 0))
            self.__lastPos = (i, j)
        
            self.update(self.__pixelRect(i, j))
    
    def __imageCoordinates(self, pos):
        """
        Private method to convert from widget to image coordinates.
        
        @param pos widget coordinate (QPoint)
        @return tuple with the image coordinates (tuple of two integers)
        """
        i = pos.x() // self.__zoom
        j = pos.y() // self.__zoom
        return i, j
    
    def __drawPasteRect(self, pos):
        """
        Private slot to draw a rectangle for signaling a paste operation.
        
        @param pos widget position of the paste rectangle (QPoint)
        """
        self.__markImage.fill(self.NoMarkColor.rgba())
        if self.__pasteRect.isValid():
            self.__updateImageRect(
                self.__pasteRect.topLeft(),
                self.__pasteRect.bottomRight() + QPoint(1, 1))
        
        x, y = self.__imageCoordinates(pos)
        isize = self.__image.size()
        if x + self.__clipboardSize.width() <= isize.width():
            sx = self.__clipboardSize.width()
        else:
            sx = isize.width() - x
        if y + self.__clipboardSize.height() <= isize.height():
            sy = self.__clipboardSize.height()
        else:
            sy = isize.height() - y
        
        self.__pasteRect = QRect(QPoint(x, y), QSize(sx - 1, sy - 1))
        
        painter = QPainter(self.__markImage)
        painter.setPen(self.MarkColor)
        painter.drawRect(self.__pasteRect)
        painter.end()
        
        self.__updateImageRect(self.__pasteRect.topLeft(),
                               self.__pasteRect.bottomRight() + QPoint(1, 1))
    
    def __drawTool(self, pos, mark):
        """
        Private method to perform a draw operation depending of the current
        tool.
        
        @param pos widget coordinate to perform the draw operation at (QPoint)
        @param mark flag indicating a mark operation (boolean)
        @return flag indicating a successful draw (boolean)
        """
        self.__unMark()
        
        if mark:
            self.__endPos = QPoint(pos)
            drawColor = self.MarkColor
            img = self.__markImage
        else:
            drawColor = self.penColor()
            img = self.__image
        
        start = QPoint(*self.__imageCoordinates(self.__startPos))
        end = QPoint(*self.__imageCoordinates(pos))
        
        painter = QPainter(img)
        painter.setPen(drawColor)
        painter.setCompositionMode(self.__compositingMode)
        
        if self.__curTool == self.Line:
            painter.drawLine(start, end)
        
        elif self.__curTool in [self.Rectangle, self.FilledRectangle,
                                self.RectangleSelection]:
            left = min(start.x(), end.x())
            top = min(start.y(), end.y())
            right = max(start.x(), end.x())
            bottom = max(start.y(), end.y())
            if self.__curTool == self.RectangleSelection:
                painter.setBrush(QBrush(drawColor))
            if self.__curTool == self.FilledRectangle:
                for y in range(top, bottom + 1):
                    painter.drawLine(left, y, right, y)
            else:
                painter.drawRect(left, top, right - left, bottom - top)
            if self.__selecting:
                self.__selRect = QRect(
                    left, top, right - left + 1, bottom - top + 1)
                self.__selectionAvailable = True
                self.selectionAvailable.emit(True)
        
        elif self.__curTool in [self.Circle, self.FilledCircle,
                                self.CircleSelection]:
            r = max(abs(start.x() - end.x()), abs(start.y() - end.y()))
            if self.__curTool in [self.FilledCircle, self.CircleSelection]:
                painter.setBrush(QBrush(drawColor))
            painter.drawEllipse(start, r, r)
            if self.__selecting:
                self.__selRect = QRect(start.x() - r, start.y() - r,
                                       2 * r + 1, 2 * r + 1)
                self.__selectionAvailable = True
                self.selectionAvailable.emit(True)
        
        elif self.__curTool in [self.Ellipse, self.FilledEllipse]:
            r1 = abs(start.x() - end.x())
            r2 = abs(start.y() - end.y())
            if r1 == 0 or r2 == 0:
                return False
            if self.__curTool == self.FilledEllipse:
                painter.setBrush(QBrush(drawColor))
            painter.drawEllipse(start, r1, r2)
        
        painter.end()
        
        if self.__curTool in [self.Circle, self.FilledCircle,
                              self.Ellipse, self.FilledEllipse]:
            self.update()
        else:
            self.__updateRect(self.__startPos, pos)
        
        return True
    
    def __drawFlood(self, i, j, oldColor, doUpdate=True):
        """
        Private method to perform a flood fill operation.
        
        @param i x-value in image coordinates (integer)
        @param j y-value in image coordinates (integer)
        @param oldColor reference to the color at position i, j (QColor)
        @param doUpdate flag indicating an update is requested (boolean)
            (used for speed optimizations)
        """
        if not self.__image.rect().contains(i, j) or \
           self.__image.pixel(i, j) != oldColor.rgba() or \
           self.__image.pixel(i, j) == self.penColor().rgba():
            return
        
        self.__image.setPixel(i, j, self.penColor().rgba())
        
        self.__drawFlood(i, j - 1, oldColor, False)
        self.__drawFlood(i, j + 1, oldColor, False)
        self.__drawFlood(i - 1, j, oldColor, False)
        self.__drawFlood(i + 1, j, oldColor, False)
        
        if doUpdate:
            self.update()
    
    def __updateRect(self, pos1, pos2):
        """
        Private slot to update parts of the widget.
        
        @param pos1 top, left position for the update in widget coordinates
            (QPoint)
        @param pos2 bottom, right position for the update in widget
            coordinates (QPoint)
        """
        self.__updateImageRect(QPoint(*self.__imageCoordinates(pos1)),
                               QPoint(*self.__imageCoordinates(pos2)))
    
    def __updateImageRect(self, ipos1, ipos2):
        """
        Private slot to update parts of the widget.
        
        @param ipos1 top, left position for the update in image coordinates
            (QPoint)
        @param ipos2 bottom, right position for the update in image
            coordinates (QPoint)
        """
        r1 = self.__pixelRect(ipos1.x(), ipos1.y())
        r2 = self.__pixelRect(ipos2.x(), ipos2.y())
        
        left = min(r1.x(), r2.x())
        top = min(r1.y(), r2.y())
        right = max(r1.x() + r1.width(), r2.x() + r2.width())
        bottom = max(r1.y() + r1.height(), r2.y() + r2.height())
        self.update(left, top, right - left + 1, bottom - top + 1)
    
    def __unMark(self):
        """
        Private slot to remove the mark indicator.
        """
        self.__markImage.fill(self.NoMarkColor.rgba())
        if self.__curTool in [self.Circle, self.FilledCircle,
                              self.Ellipse, self.FilledEllipse,
                              self.CircleSelection]:
            self.update()
        else:
            self.__updateRect(self.__startPos, self.__endPos)
        
        if self.__selecting:
            self.__selRect = QRect()
            self.__selectionAvailable = False
            self.selectionAvailable.emit(False)
    
    def __isMarked(self, i, j):
        """
        Private method to check, if a pixel is marked.
        
        @param i x-value in image coordinates (integer)
        @param j y-value in image coordinates (integer)
        @return flag indicating a marked pixel (boolean)
        """
        return self.__markImage.pixel(i, j) == self.MarkColor.rgba()
    
    def __updatePreviewPixmap(self):
        """
        Private slot to generate and signal an updated preview pixmap.
        """
        p = QPixmap.fromImage(self.__image)
        self.previewChanged.emit(p)
    
    def previewPixmap(self):
        """
        Public method to generate a preview pixmap.
        
        @return preview pixmap (QPixmap)
        """
        p = QPixmap.fromImage(self.__image)
        return p
    
    def __checkClipboard(self):
        """
        Private slot to check, if the clipboard contains a valid image, and
        signal the result.
        """
        ok = self.__clipboardImage()[1]
        self.__clipboardImageAvailable = ok
        self.clipboardImageAvailable.emit(ok)
    
    def canPaste(self):
        """
        Public slot to check the availability of the paste operation.
        
        @return flag indicating availability of paste (boolean)
        """
        return self.__clipboardImageAvailable
    
    def __clipboardImage(self):
        """
        Private method to get an image from the clipboard.
        
        @return tuple with the image (QImage) and a flag indicating a
            valid image (boolean)
        """
        img = QApplication.clipboard().image()
        ok = not img.isNull()
        if ok:
            img = img.convertToFormat(QImage.Format_ARGB32)
        
        return img, ok
    
    def __getSelectionImage(self, cut):
        """
        Private method to get an image from the selection.
        
        @param cut flag indicating to cut the selection (boolean)
        @return image of the selection (QImage)
        """
        if cut:
            cmd = IconEditCommand(self, self.tr("Cut Selection"),
                                  self.__image)
        
        img = QImage(self.__selRect.size(), QImage.Format_ARGB32)
        img.fill(qRgba(0, 0, 0, 0))
        for i in range(0, self.__selRect.width()):
            for j in range(0, self.__selRect.height()):
                if self.__image.rect().contains(self.__selRect.x() + i,
                                                self.__selRect.y() + j):
                    if self.__isMarked(
                            self.__selRect.x() + i, self.__selRect.y() + j):
                        img.setPixel(i, j, self.__image.pixel(
                            self.__selRect.x() + i, self.__selRect.y() + j))
                        if cut:
                            self.__image.setPixel(self.__selRect.x() + i,
                                                  self.__selRect.y() + j,
                                                  qRgba(0, 0, 0, 0))
        
        if cut:
            self.__undoStack.push(cmd)
            cmd.setAfterImage(self.__image)
        
        self.__unMark()
        
        if cut:
            self.update(self.__selRect)
        
        return img
    
    def editCopy(self):
        """
        Public slot to copy the selection.
        """
        if self.__selRect.isValid():
            img = self.__getSelectionImage(False)
            QApplication.clipboard().setImage(img)
    
    def editCut(self):
        """
        Public slot to cut the selection.
        """
        if self.__selRect.isValid():
            img = self.__getSelectionImage(True)
            QApplication.clipboard().setImage(img)
    
    @pyqtSlot()
    def editPaste(self, pasting=False):
        """
        Public slot to paste an image from the clipboard.
        
        @param pasting flag indicating part two of the paste operation
            (boolean)
        """
        img, ok = self.__clipboardImage()
        if ok:
            if img.width() > self.__image.width() or \
                    img.height() > self.__image.height():
                res = E5MessageBox.yesNo(
                    self,
                    self.tr("Paste"),
                    self.tr(
                        """<p>The clipboard image is larger than the"""
                        """ current image.<br/>Paste as new image?</p>"""))
                if res:
                    self.editPasteAsNew()
                return
            elif not pasting:
                self.__isPasting = True
                self.__clipboardSize = img.size()
            else:
                cmd = IconEditCommand(self, self.tr("Paste Clipboard"),
                                      self.__image)
                self.__markImage.fill(self.NoMarkColor.rgba())
                painter = QPainter(self.__image)
                painter.setPen(self.penColor())
                painter.setCompositionMode(self.__compositingMode)
                painter.drawImage(
                    self.__pasteRect.x(), self.__pasteRect.y(), img, 0, 0,
                    self.__pasteRect.width() + 1,
                    self.__pasteRect.height() + 1)
                
                self.__undoStack.push(cmd)
                cmd.setAfterImage(self.__image)
                
                self.__updateImageRect(
                    self.__pasteRect.topLeft(),
                    self.__pasteRect.bottomRight() + QPoint(1, 1))
        else:
            E5MessageBox.warning(
                self,
                self.tr("Pasting Image"),
                self.tr("""Invalid image data in clipboard."""))
    
    def editPasteAsNew(self):
        """
        Public slot to paste the clipboard as a new image.
        """
        img, ok = self.__clipboardImage()
        if ok:
            cmd = IconEditCommand(
                self, self.tr("Paste Clipboard as New Image"),
                self.__image)
            self.setIconImage(img)
            self.setDirty(True)
            self.__undoStack.push(cmd)
            cmd.setAfterImage(self.__image)
    
    def editSelectAll(self):
        """
        Public slot to select the complete image.
        """
        self.__unMark()
        
        self.__startPos = QPoint(0, 0)
        self.__endPos = QPoint(self.rect().bottomRight())
        self.__markImage.fill(self.MarkColor.rgba())
        self.__selRect = self.__image.rect()
        self.__selectionAvailable = True
        self.selectionAvailable.emit(True)
        
        self.update()
    
    def editClear(self):
        """
        Public slot to clear the image.
        """
        self.__unMark()
        
        cmd = IconEditCommand(self, self.tr("Clear Image"), self.__image)
        self.__image.fill(qRgba(0, 0, 0, 0))
        self.update()
        self.setDirty(True)
        self.__undoStack.push(cmd)
        cmd.setAfterImage(self.__image)
    
    def editResize(self):
        """
        Public slot to resize the image.
        """
        from .IconSizeDialog import IconSizeDialog
        dlg = IconSizeDialog(self.__image.width(), self.__image.height())
        res = dlg.exec_()
        if res == QDialog.Accepted:
            newWidth, newHeight = dlg.getData()
            if newWidth != self.__image.width() or \
                    newHeight != self.__image.height():
                cmd = IconEditCommand(self, self.tr("Resize Image"),
                                      self.__image)
                img = self.__image.scaled(
                    newWidth, newHeight, Qt.IgnoreAspectRatio,
                    Qt.SmoothTransformation)
                self.setIconImage(img)
                self.setDirty(True)
                self.__undoStack.push(cmd)
                cmd.setAfterImage(self.__image)
    
    def editNew(self):
        """
        Public slot to generate a new, empty image.
        """
        from .IconSizeDialog import IconSizeDialog
        dlg = IconSizeDialog(self.__image.width(), self.__image.height())
        res = dlg.exec_()
        if res == QDialog.Accepted:
            width, height = dlg.getData()
            img = QImage(width, height, QImage.Format_ARGB32)
            img.fill(qRgba(0, 0, 0, 0))
            self.setIconImage(img)
    
    def grayScale(self):
        """
        Public slot to convert the image to gray preserving transparency.
        """
        cmd = IconEditCommand(self, self.tr("Convert to Grayscale"),
                              self.__image)
        for x in range(self.__image.width()):
            for y in range(self.__image.height()):
                col = self.__image.pixel(x, y)
                if col != qRgba(0, 0, 0, 0):
                    gray = qGray(col)
                    self.__image.setPixel(
                        x, y, qRgba(gray, gray, gray, qAlpha(col)))
        self.update()
        self.setDirty(True)
        self.__undoStack.push(cmd)
        cmd.setAfterImage(self.__image)

    def editUndo(self):
        """
        Public slot to perform an undo operation.
        """
        if self.__undoStack.canUndo():
            self.__undoStack.undo()
    
    def editRedo(self):
        """
        Public slot to perform a redo operation.
        """
        if self.__undoStack.canRedo():
            self.__undoStack.redo()
    
    def canUndo(self):
        """
        Public method to return the undo status.
        
        @return flag indicating the availability of undo (boolean)
        """
        return self.__undoStack.canUndo()
    
    def canRedo(self):
        """
        Public method to return the redo status.
        
        @return flag indicating the availability of redo (boolean)
        """
        return self.__undoStack.canRedo()
    
    def __cleanChanged(self, clean):
        """
        Private slot to handle the undo stack clean state change.
        
        @param clean flag indicating the clean state (boolean)
        """
        self.setDirty(not clean)
    
    def shutdown(self):
        """
        Public slot to perform some shutdown actions.
        """
        self.__undoStack.canRedoChanged.disconnect(self.canRedoChanged)
        self.__undoStack.canUndoChanged.disconnect(self.canUndoChanged)
        self.__undoStack.cleanChanged.disconnect(self.__cleanChanged)
    
    def isSelectionAvailable(self):
        """
        Public method to check the availability of a selection.
        
        @return flag indicating the availability of a selection (boolean)
        """
        return self.__selectionAvailable
Ejemplo n.º 11
0
class AbstractPage(QObject):
    sigChanged = pyqtSignal()

    def __init__(self, parent: MasterDocument):
        super().__init__()
        self._objs = set()
        self._parent = parent
        self.undoStack = QUndoStack()
        self._name = "untitled"

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, n):
        self._name = n
        self.sigChanged.emit()

    @property
    def parentDoc(self):
        return self._parent

    def isModified(self):
        return not self.undoStack.isClean()

    def doCommand(self, cmd):
        cmd.doc = self
        self.undoStack.push(cmd)
        self.sigChanged.emit()

    def objects(self, objType=None, exclude=None):
        if not objType and not exclude:
            return set(self._objs)
        if exclude is None:
            exclude = ()
        if objType:
            return {obj for obj in self._objs if type(obj) is objType and not (type(obj) in exclude)}
        else:
            return {obj for obj in self._objs if not (type(obj) in exclude)}

    def hasObject(self, obj):
        return obj in self._objs

    def findObjsInRect(self, rect: QRect, objType=None):
        return {obj for obj in self.objects(objType) if rect.intersects(obj.bbox())}

    def findObjsNear(self, pt: QPoint, dist=1, objType=None):
        hitRect = QRect(pt.x()-dist/2, pt.y()-dist/2, dist, dist)
        return {obj for obj in self.findObjsInRect(hitRect, objType) if obj.testHit(pt, dist)}

    def addObj(self, obj):
        self._objs.add(obj)
        self.sigChanged.emit()

    def removeObj(self, obj):
        self._objs.remove(obj)
        self.sigChanged.emit()

    @pyqtSlot()
    def undo(self):
        self.undoStack.undo()
        self.sigChanged.emit()

    @pyqtSlot()
    def redo(self):
        self.undoStack.redo()
        self.sigChanged.emit()

    def fromXml(self, pageNode):
        raise NotImplementedError()

    def toXml(self, parentNode):
        raise NotImplementedError()