Exemple #1
0
 def usedTilesets(self):
     tilesets = QSet()
     for object in self.mObjects:
         tile = object.cell().tile
         if tile:
             tilesets.insert(tile.sharedTileset())
     return tilesets
Exemple #2
0
 def usedTilesets(self):
     tilesets = QSet()
     for object in self.mObjects:
         tile = object.cell().tile
         if tile:
             tilesets.insert(tile.sharedTileset())
     return tilesets
Exemple #3
0
 def usedTilesets(self):
     tilesets = QSet()
     i = 0
     while(i<self.mGrid.size()):
         tile = self.mGrid.at(i).tile
         if tile:
             tilesets.insert(tile.tileset())
         i += 1
     return tilesets
Exemple #4
0
 def usedTilesets(self):
     tilesets = QSet()
     i = 0
     while (i < self.mGrid.size()):
         tile = self.mGrid.at(i).tile
         if tile:
             tilesets.insert(tile.tileset())
         i += 1
     return tilesets
Exemple #5
0
    def updateSelectedObjectItems(self):
        objects = self.mMapDocument.selectedObjects()
        items = QSet()
        for object in objects:
            item = self.itemForObject(object)
            if item:
                items.insert(item)

        self.mSelectedObjectItems = items
        self.selectedObjectItemsChanged.emit()
Exemple #6
0
    def updateSelectedObjectItems(self):
        objects = self.mMapDocument.selectedObjects()
        items = QSet()
        for object in objects:
            item = self.itemForObject(object)
            if item:
                items.insert(item)

        self.mSelectedObjectItems = items
        self.selectedObjectItemsChanged.emit()
    def updateSelection(self, pos, modifiers):
        rect = QRectF(self.mStart, pos).normalized()
        # Make sure the rect has some contents, otherwise intersects returns False
        rect.setWidth(max(1.0, rect.width()))
        rect.setHeight(max(1.0, rect.height()))
        selectedItems = QSet()
        for item in self.mapScene().items(rect):
            if type(item) == MapObjectItem:
                selectedItems.insert(item)

        if (modifiers & (Qt.ControlModifier | Qt.ShiftModifier)):
            selectedItems |= self.mapScene().selectedObjectItems()
        else:
            self.setMode(Mode.Resize)
        self.mapScene().setSelectedObjectItems(selectedItems)
    def updateSelection(self, pos, modifiers):
        rect = QRectF(self.mStart, pos).normalized()
        # Make sure the rect has some contents, otherwise intersects returns False
        rect.setWidth(max(1.0, rect.width()))
        rect.setHeight(max(1.0, rect.height()))
        selectedItems = QSet()
        for item in self.mapScene().items(rect):
            if type(item) == MapObjectItem:
                selectedItems.insert(item)

        if (modifiers & (Qt.ControlModifier | Qt.ShiftModifier)):
            selectedItems |= self.mapScene().selectedObjectItems()
        else:
            self.setMode(Mode.Resize)
        self.mapScene().setSelectedObjectItems(selectedItems)
Exemple #9
0
    def __init__(self):
        super().__init__()

        ##
        # Stores the tilesets and maps them to the number of references.
        ##
        self.mTilesets = QMap()
        self.mChangedFiles = QSet()
        self.mWatcher = FileSystemWatcher(self)
        self.mAnimationDriver = TileAnimationDriver(self)
        self.mReloadTilesetsOnChange = False
        self.mChangedFilesTimer = QTimer()

        self.mWatcher.fileChanged.connect(self.fileChanged)
        self.mChangedFilesTimer.setInterval(500)
        self.mChangedFilesTimer.setSingleShot(True)
        self.mChangedFilesTimer.timeout.connect(self.fileChangedTimeout)
        self.mAnimationDriver.update.connect(self.advanceTileAnimations)
Exemple #10
0
    def __init__(self, parent):
        super().__init__(parent)
        self.mMapDocument = None
        self.mSelectedTool = None
        self.mActiveTool = None
        self.mObjectSelectionItem = None
        self.mUnderMouse = False
        self.mCurrentModifiers = Qt.NoModifier,
        self.mDarkRectangle = QGraphicsRectItem()
        self.mDefaultBackgroundColor = Qt.darkGray

        self.mLayerItems = QVector()
        self.mObjectItems = QMap()
        self.mObjectLineWidth = 0.0
        self.mSelectedObjectItems = QSet()
        self.mLastMousePos = QPointF()
        self.mShowTileObjectOutlines = False
        self.mHighlightCurrentLayer = False
        self.mGridVisible = False

        self.setBackgroundBrush(self.mDefaultBackgroundColor)
        tilesetManager = TilesetManager.instance()
        tilesetManager.tilesetChanged.connect(self.tilesetChanged)
        tilesetManager.repaintTileset.connect(self.tilesetChanged)
        prefs = preferences.Preferences.instance()
        prefs.showGridChanged.connect(self.setGridVisible)
        prefs.showTileObjectOutlinesChanged.connect(
            self.setShowTileObjectOutlines)
        prefs.objectTypesChanged.connect(self.syncAllObjectItems)
        prefs.highlightCurrentLayerChanged.connect(
            self.setHighlightCurrentLayer)
        prefs.gridColorChanged.connect(self.update)
        prefs.objectLineWidthChanged.connect(self.setObjectLineWidth)
        self.mDarkRectangle.setPen(QPen(Qt.NoPen))
        self.mDarkRectangle.setBrush(Qt.black)
        self.mDarkRectangle.setOpacity(darkeningFactor)
        self.addItem(self.mDarkRectangle)
        self.mGridVisible = prefs.showGrid()
        self.mObjectLineWidth = prefs.objectLineWidth()
        self.mShowTileObjectOutlines = prefs.showTileObjectOutlines()
        self.mHighlightCurrentLayer = prefs.highlightCurrentLayer()
        # Install an event filter so that we can get key events on behalf of the
        # active tool without having to have the current focus.
        QCoreApplication.instance().installEventFilter(self)
    def updateSelection(self, event):
        rect = QRectF(self.mStart, event.scenePos()).normalized()
        # Make sure the rect has some contents, otherwise intersects returns False
        rect.setWidth(max(1.0, rect.width()))
        rect.setHeight(max(1.0, rect.height()))
        oldSelection = self.mapScene().selectedObjectItems()
        if (oldSelection.isEmpty()):
            # Allow selecting some map objects only when there aren't any selected
            selectedItems = QSet()
            for item in self.mapScene().items(rect, Qt.IntersectsItemShape, Qt.DescendingOrder, viewTransform(event)):
                if type(item) == MapObjectItem:
                    selectedItems.insert(item)

            newSelection = QSet()
            if (event.modifiers() & (Qt.ControlModifier | Qt.ShiftModifier)):
                newSelection = oldSelection | selectedItems
            else:
                newSelection = selectedItems

            self.mapScene().setSelectedObjectItems(newSelection)
            self.updateHandles()
        else:
            # Update the selected handles
            selectedHandles = QSet()
            for item in self.mapScene().items(rect, Qt.IntersectsItemShape, Qt.DescendingOrder, viewTransform(event)):
                if type(item) == PointHandle:
                    selectedHandles.insert(item)

            if (event.modifiers() & (Qt.ControlModifier | Qt.ShiftModifier)):
                self.setSelectedHandles(self.mSelectedHandles | selectedHandles)
            else:
                self.setSelectedHandles(selectedHandles)
    def __init__(self, mapDocument, autoMapper, where):
        super().__init__()

        self.mLayersAfter = QVector()
        self.mLayersBefore = QVector()
        self.mMapDocument = mapDocument
        map = self.mMapDocument.Map()
        touchedLayers = QSet()
        index = 0
        while (index < autoMapper.size()):
            a = autoMapper.at(index)
            if (a.prepareAutoMap()):
                touchedLayers |= a.getTouchedTileLayers()
                index += 1
            else:
                autoMapper.remove(index)

        for layerName in touchedLayers:
            layerindex = map.indexOfLayer(layerName)
            self.mLayersBefore(map.layerAt(layerindex).clone())

        for a in autoMapper:
            a.autoMap(where)
        for layerName in touchedLayers:
            layerindex = map.indexOfLayer(layerName)
            # layerindex exists, because AutoMapper is still alive, dont check
            self.mLayersAfter(map.layerAt(layerindex).clone())

        # reduce memory usage by saving only diffs
        for i in range(self.mLayersAfter.size()):
            before = self.mLayersBefore.at(i)
            after = self.mLayersAfter.at(i)
            diffRegion = before.computeDiffRegion(after).boundingRect()
            before1 = before.copy(diffRegion)
            after1 = after.copy(diffRegion)
            before1.setPosition(diffRegion.topLeft())
            after1.setPosition(diffRegion.topLeft())
            before1.setName(before.name())
            after1.setName(after.name())
            self.mLayersBefore.replace(i, before1)
            self.mLayersAfter.replace(i, after1)
            del before
            del after

        for a in autoMapper:
            a.cleanAll()
    def mouseReleased(self, event):
        if (event.button() != Qt.LeftButton):
            return
        x = self.mAction
        if x == Action.NoAction:
            if (not self.mClickedRotateHandle
                    and not self.mClickedResizeHandle):
                # Don't change selection as a result of clicking on a handle
                modifiers = event.modifiers()
                if (self.mClickedObjectItem):
                    selection = self.mapScene().selectedObjectItems()
                    if (modifiers & (Qt.ShiftModifier | Qt.ControlModifier)):
                        if (selection.contains(self.mClickedObjectItem)):
                            selection.remove(self.mClickedObjectItem)
                        else:
                            selection.insert(self.mClickedObjectItem)
                    elif (selection.contains(self.mClickedObjectItem)):
                        # Clicking one of the selected items changes the edit mode
                        if self.mMode == Mode.Resize:
                            _x = Mode.Rotate
                        else:
                            _x = Mode.Resize
                        self.setMode(_x)
                    else:
                        selection.clear()
                        selection.insert(self.mClickedObjectItem)
                        self.setMode(Mode.Resize)
                    self.mapScene().setSelectedObjectItems(selection)
                elif (not (modifiers & Qt.ShiftModifier)):
                    self.mapScene().setSelectedObjectItems(QSet())
        elif x == Action.Selecting:
            self.updateSelection(event.scenePos(), event.modifiers())
            self.mapScene().removeItem(self.mSelectionRectangle)
            self.mAction = Action.NoAction
        elif x == Action.Moving:
            self.finishMoving(event.scenePos())
        elif x == Action.Rotating:
            self.finishRotating(event.scenePos())
        elif x == Action.Resizing:
            self.finishResizing(event.scenePos())

        self.mMousePressed = False
        self.mClickedObjectItem = None
        self.mClickedRotateHandle = None
        self.mClickedResizeHandle = None
        self.refreshCursor()
Exemple #14
0
    def __init__(self):
        super().__init__()

        ##
        # Stores the tilesets and maps them to the number of references.
        ##
        self.mTilesets = QMap()
        self.mChangedFiles = QSet()
        self.mWatcher = FileSystemWatcher(self)
        self.mAnimationDriver = TileAnimationDriver(self)
        self.mReloadTilesetsOnChange = False
        self.mChangedFilesTimer = QTimer()

        self.mWatcher.fileChanged.connect(self.fileChanged)
        self.mChangedFilesTimer.setInterval(500)
        self.mChangedFilesTimer.setSingleShot(True)
        self.mChangedFilesTimer.timeout.connect(self.fileChangedTimeout)
        self.mAnimationDriver.update.connect(self.advanceTileAnimations)
    def startMoving(self, modifiers):
        # Move only the clicked item, if it was not part of the selection
        if (self.mClickedObjectItem and not (modifiers & Qt.AltModifier)):
            if (not self.mapScene().selectedObjectItems().contains(
                    self.mClickedObjectItem)):
                self.mapScene().setSelectedObjectItems(
                    QSet([self.mClickedObjectItem]))

        self.saveSelectionState()
        self.mAction = Action.Moving
        self.mAlignPosition = self.mMovingObjects[0].oldPosition
        for object in self.mMovingObjects:
            pos = object.oldPosition
            if (pos.x() < self.mAlignPosition.x()):
                self.mAlignPosition.setX(pos.x())
            if (pos.y() < self.mAlignPosition.y()):
                self.mAlignPosition.setY(pos.y())

        self.updateHandleVisibility()
Exemple #16
0
    def selectTilesInStamp(self, stamp):
        if self.mEmittingStampCaptured:
            return
        processed = QSet()
        selections = QMap()
        for variation in stamp.variations():
            tileLayer = variation.tileLayer()
            for cell in tileLayer:
                tile = cell.tile
                if tile:
                    if (processed.contains(tile)):
                        continue
                    processed.insert(tile)  # avoid spending time on duplicates
                    tileset = tile.tileset()
                    tilesetIndex = self.mTilesets.indexOf(
                        tileset.sharedPointer())
                    if (tilesetIndex != -1):
                        view = self.tilesetViewAt(tilesetIndex)
                        if (not view.model()):  # Lazily set up the model
                            self.setupTilesetModel(view, tileset)
                        model = view.tilesetModel()
                        modelIndex = model.tileIndex(tile)
                        selectionModel = view.selectionModel()

                        _x = QItemSelection()
                        _x.select(modelIndex, modelIndex)
                        selections[selectionModel] = _x

        if (not selections.isEmpty()):
            self.mSynchronizingSelection = True
            # Mark captured tiles as selected
            for i in selections:
                selectionModel = i[0]
                selection = i[1]
                selectionModel.select(selection,
                                      QItemSelectionModel.SelectCurrent)

            # Show/edit properties of all captured tiles
            self.mMapDocument.setSelectedTiles(processed.toList())
            # Update the current tile (useful for animation and collision editors)
            first = selections.first()
            selectionModel = first[0]
            selection = first[1]
            currentIndex = QModelIndex(selection.first().topLeft())
            if (selectionModel.currentIndex() != currentIndex):
                selectionModel.setCurrentIndex(currentIndex,
                                               QItemSelectionModel.NoUpdate)
            else:
                self.currentChanged(currentIndex)
            self.mSynchronizingSelection = False
Exemple #17
0
    def __init__(self, parent):
        super().__init__(parent)
        self.mMapDocument = None
        self.mSelectedTool = None
        self.mActiveTool = None
        self.mObjectSelectionItem = None
        self.mUnderMouse = False
        self.mCurrentModifiers = Qt.NoModifier,
        self.mDarkRectangle = QGraphicsRectItem()
        self.mDefaultBackgroundColor = Qt.darkGray

        self.mLayerItems = QVector()
        self.mObjectItems = QMap()
        self.mObjectLineWidth = 0.0
        self.mSelectedObjectItems = QSet()
        self.mLastMousePos = QPointF()
        self.mShowTileObjectOutlines = False
        self.mHighlightCurrentLayer = False
        self.mGridVisible = False

        self.setBackgroundBrush(self.mDefaultBackgroundColor)
        tilesetManager = TilesetManager.instance()
        tilesetManager.tilesetChanged.connect(self.tilesetChanged)
        tilesetManager.repaintTileset.connect(self.tilesetChanged)
        prefs = preferences.Preferences.instance()
        prefs.showGridChanged.connect(self.setGridVisible)
        prefs.showTileObjectOutlinesChanged.connect(self.setShowTileObjectOutlines)
        prefs.objectTypesChanged.connect(self.syncAllObjectItems)
        prefs.highlightCurrentLayerChanged.connect(self.setHighlightCurrentLayer)
        prefs.gridColorChanged.connect(self.update)
        prefs.objectLineWidthChanged.connect(self.setObjectLineWidth)
        self.mDarkRectangle.setPen(QPen(Qt.NoPen))
        self.mDarkRectangle.setBrush(Qt.black)
        self.mDarkRectangle.setOpacity(darkeningFactor)
        self.addItem(self.mDarkRectangle)
        self.mGridVisible = prefs.showGrid()
        self.mObjectLineWidth = prefs.objectLineWidth()
        self.mShowTileObjectOutlines = prefs.showTileObjectOutlines()
        self.mHighlightCurrentLayer = prefs.highlightCurrentLayer()
        # Install an event filter so that we can get key events on behalf of the
        # active tool without having to have the current focus.
        QCoreApplication.instance().installEventFilter(self)
    def __init__(self, parent = None):
        super().__init__(self.tr("Edit Polygons"),
              QIcon(":images/24x24/tool-edit-polygons.png"),
              QKeySequence(self.tr("E")),
              parent)

        self.mSelectedHandles = QSet()
        self.mModifiers = Qt.KeyboardModifiers()
        self.mScreenStart = QPoint()
        self.mOldHandlePositions = QVector()
        self.mAlignPosition = QPointF()
        ## The list of handles associated with each selected map object
        self.mHandles = QMapList()
        self.mOldPolygons = QMap()
        self.mStart = QPointF()

        self.mSelectionRectangle = SelectionRectangle()
        self.mMousePressed = False
        self.mClickedHandle = None
        self.mClickedObjectItem = None
        self.mMode = EditPolygonTool.NoMode
Exemple #19
0
 def __init__(self):
     self.names = QSet()
Exemple #20
0
 def __init__(self):
     self.indexes = QSet()
     self.names = QSet()  # all names
class EditPolygonTool(AbstractObjectTool):
    NoMode, Selecting, Moving = range(3)

    def __init__(self, parent = None):
        super().__init__(self.tr("Edit Polygons"),
              QIcon(":images/24x24/tool-edit-polygons.png"),
              QKeySequence(self.tr("E")),
              parent)

        self.mSelectedHandles = QSet()
        self.mModifiers = Qt.KeyboardModifiers()
        self.mScreenStart = QPoint()
        self.mOldHandlePositions = QVector()
        self.mAlignPosition = QPointF()
        ## The list of handles associated with each selected map object
        self.mHandles = QMapList()
        self.mOldPolygons = QMap()
        self.mStart = QPointF()

        self.mSelectionRectangle = SelectionRectangle()
        self.mMousePressed = False
        self.mClickedHandle = None
        self.mClickedObjectItem = None
        self.mMode = EditPolygonTool.NoMode

    def __del__(self):
        del self.mSelectionRectangle

    def tr(self, sourceText, disambiguation = '', n = -1):
        return QCoreApplication.translate('EditPolygonTool', sourceText, disambiguation, n)

    def activate(self, scene):
        super().activate(scene)
        self.updateHandles()
        # TODO: Could be more optimal by separating the updating of handles from
        # the creation and removal of handles depending on changes in the
        # selection, and by only updating the handles of the objects that changed.
        self.mapDocument().objectsChanged.connect(self.updateHandles)
        scene.selectedObjectItemsChanged.connect(self.updateHandles)
        self.mapDocument().objectsRemoved.connect(self.objectsRemoved)

    def deactivate(self, scene):
        try:
            self.mapDocument().objectsChanged.disconnect(self.updateHandles)
            scene.selectedObjectItemsChanged.disconnect(self.updateHandles)
        except:
            pass
        # Delete all handles
        self.mHandles.clear()
        self.mSelectedHandles.clear()
        self.mClickedHandle = None
        super().deactivate(scene)

    def mouseEntered(self):
        pass
    def mouseMoved(self, pos, modifiers):
        super().mouseMoved(pos, modifiers)
        if (self.mMode == EditPolygonTool.NoMode and self.mMousePressed):
            screenPos = QCursor.pos()
            dragDistance = (self.mScreenStart - screenPos).manhattanLength()
            if (dragDistance >= QApplication.startDragDistance()):
                if (self.mClickedHandle):
                    self.startMoving()
                else:
                    self.startSelecting()

        x = self.mMode
        if x==EditPolygonTool.Selecting:
            self.mSelectionRectangle.setRectangle(QRectF(self.mStart, pos).normalized())
        elif x==EditPolygonTool.Moving:
            self.updateMovingItems(pos, modifiers)
        elif x==EditPolygonTool.NoMode:
            pass

    def mousePressed(self, event):
        if (self.mMode != EditPolygonTool.NoMode): # Ignore additional presses during select/move
            return
        x = event.button()
        if x==Qt.LeftButton:
            self.mMousePressed = True
            self.mStart = event.scenePos()
            self.mScreenStart = event.screenPos()
            items = self.mapScene().items(self.mStart,
                                                                   Qt.IntersectsItemShape,
                                                                   Qt.DescendingOrder,
                                                                   viewTransform(event))
            self.mClickedObjectItem = first(items, MapObjectItem)
            self.mClickedHandle = first(items, PointHandle)
        elif x==Qt.RightButton:
            items = self.mapScene().items(event.scenePos(),
                                                                   Qt.IntersectsItemShape,
                                                                   Qt.DescendingOrder,
                                                                   viewTransform(event))
            clickedHandle = first(items)
            if (clickedHandle or not self.mSelectedHandles.isEmpty()):
                self.showHandleContextMenu(clickedHandle,
                                      event.screenPos())
            else:
                super().mousePressed(event)
        else:
            super().mousePressed(event)

    def mouseReleased(self, event):
        if (event.button() != Qt.LeftButton):
            return
        x = self.mMode
        if x==EditPolygonTool.NoMode:
            if (self.mClickedHandle):
                selection = self.mSelectedHandles
                modifiers = event.modifiers()
                if (modifiers & (Qt.ShiftModifier | Qt.ControlModifier)):
                    if (selection.contains(self.mClickedHandle)):
                        selection.remove(self.mClickedHandle)
                    else:
                        selection.insert(self.mClickedHandle)
                else:
                    selection.clear()
                    selection.insert(self.mClickedHandle)

                self.setSelectedHandles(selection)
            elif (self.mClickedObjectItem):
                selection = self.mapScene().selectedObjectItems()
                modifiers = event.modifiers()
                if (modifiers & (Qt.ShiftModifier | Qt.ControlModifier)):
                    if (selection.contains(self.mClickedObjectItem)):
                        selection.remove(self.mClickedObjectItem)
                    else:
                        selection.insert(self.mClickedObjectItem)
                else:
                    selection.clear()
                    selection.insert(self.mClickedObjectItem)

                self.mapScene().setSelectedObjectItems(selection)
                self.updateHandles()
            elif (not self.mSelectedHandles.isEmpty()):
                # First clear the handle selection
                self.setSelectedHandles(QSet())
            else:
                # If there is no handle selection, clear the object selection
                self.mapScene().setSelectedObjectItems(QSet())
                self.updateHandles()
        elif x==EditPolygonTool.Selecting:
            self.updateSelection(event)
            self.mapScene().removeItem(self.mSelectionRectangle)
            self.mMode = EditPolygonTool.NoMode
        elif x==EditPolygonTool.Moving:
            self.finishMoving(event.scenePos())

        self.mMousePressed = False
        self.mClickedHandle = None

    def modifiersChanged(self, modifiers):
        self.mModifiers = modifiers

    def languageChanged(self):
        self.setName(self.tr("Edit Polygons"))
        self.setShortcut(QKeySequence(self.tr("E")))

    def updateHandles(self):
        selection = self.mapScene().selectedObjectItems()
        # First destroy the handles for objects that are no longer selected

        for l in range(len(self.mHandles)):
            i = self.mHandles.itemByIndex(l)
            if (not selection.contains(i[0])):
                for handle in i[1]:
                    if (handle.isSelected()):
                        self.mSelectedHandles.remove(handle)
                    del handle

                del self.mHandles[l]

        renderer = self.mapDocument().renderer()
        for item in selection:
            object = item.mapObject()
            if (not object.cell().isEmpty()):
                continue
            polygon = object.polygon()
            polygon.translate(object.position())
            pointHandles = self.mHandles.get(item)
            # Create missing handles
            while (pointHandles.size() < polygon.size()):
                handle = PointHandle(item, pointHandles.size())
                pointHandles.append(handle)
                self.mapScene().addItem(handle)

            # Remove superfluous handles
            while (pointHandles.size() > polygon.size()):
                handle = pointHandles.takeLast()
                if (handle.isSelected()):
                    self.mSelectedHandles.remove(handle)
                del handle

            # Update the position of all handles
            for i in range(pointHandles.size()):
                point = polygon.at(i)
                handlePos = renderer.pixelToScreenCoords_(point)
                internalHandlePos = handlePos - item.pos()
                pointHandles.at(i).setPos(item.mapToScene(internalHandlePos))

            self.mHandles.insert(item, pointHandles)

    def objectsRemoved(self, objects):
        if (self.mMode == EditPolygonTool.Moving):
            # Make sure we're not going to try to still change these objects when
            # finishing the move operation.
            # TODO: In addition to avoiding crashes, it would also be good to
            # disallow other actions while moving.
            for object in objects:
                self.mOldPolygons.remove(object)

    def deleteNodes(self):
        if (self.mSelectedHandles.isEmpty()):
            return
        p = groupIndexesByObject(self.mSelectedHandles)
        undoStack = self.mapDocument().undoStack()
        delText = self.tr("Delete %n Node(s)", "", self.mSelectedHandles.size())
        undoStack.beginMacro(delText)
        for i in p:
            object = i[0]
            indexRanges = i[1]
            oldPolygon = object.polygon()
            newPolygon = oldPolygon
            # Remove points, back to front to keep the indexes valid
            it = indexRanges.end()
            begin = indexRanges.begin()
            # assert: end != begin, since there is at least one entry
            while(it != begin):
                it -= 1
                newPolygon.remove(it.first(), it.length())
            if (newPolygon.size() < 2):
                # We've removed the entire object
                undoStack.push(RemoveMapObject(self.mapDocument(), object))
            else:
                undoStack.push(ChangePolygon(self.mapDocument(), object, newPolygon, oldPolygon))

        undoStack.endMacro()

    def joinNodes(self):
        if (self.mSelectedHandles.size() < 2):
            return
        p = groupIndexesByObject(self.mSelectedHandles)
        undoStack = self.mapDocument().undoStack()
        macroStarted = False
        for i in p:
            object = i[0]
            indexRanges = i[1]
            closed = object.shape() == MapObject.Polygon
            oldPolygon = object.polygon()
            newPolygon = joinPolygonNodes(oldPolygon, indexRanges,
                                                    closed)
            if (newPolygon.size() < oldPolygon.size()):
                if (not macroStarted):
                    undoStack.beginMacro(self.tr("Join Nodes"))
                    macroStarted = True

                undoStack.push(ChangePolygon(self.mapDocument(), object, newPolygon, oldPolygon))

        if (macroStarted):
            undoStack.endMacro()

    def splitSegments(self):
        if (self.mSelectedHandles.size() < 2):
            return
        p = groupIndexesByObject(self.mSelectedHandles)
        undoStack = self.mapDocument().undoStack()
        macroStarted = False
        for i in p:
            object = i[0]
            indexRanges = i[1]
            closed = object.shape() == MapObject.Polygon
            oldPolygon = object.polygon()
            newPolygon = splitPolygonSegments(oldPolygon, indexRanges,
                                                        closed)
            if (newPolygon.size() > oldPolygon.size()):
                if (not macroStarted):
                    undoStack.beginMacro(self.tr("Split Segments"))
                    macroStarted = True

                undoStack.push(ChangePolygon(self.mapDocument(), object, newPolygon, oldPolygon))

        if (macroStarted):
            undoStack.endMacro()

    def setSelectedHandles(self, handles):
        for handle in self.mSelectedHandles:
            if (not handles.contains(handle)):
                handle.setSelected(False)
        for handle in handles:
            if (not self.mSelectedHandles.contains(handle)):
                handle.setSelected(True)
        self.mSelectedHandles = handles

    def setSelectedHandle(self, handle):
        self.setSelectedHandles(QSet([handle]))

    def updateSelection(self, event):
        rect = QRectF(self.mStart, event.scenePos()).normalized()
        # Make sure the rect has some contents, otherwise intersects returns False
        rect.setWidth(max(1.0, rect.width()))
        rect.setHeight(max(1.0, rect.height()))
        oldSelection = self.mapScene().selectedObjectItems()
        if (oldSelection.isEmpty()):
            # Allow selecting some map objects only when there aren't any selected
            selectedItems = QSet()
            for item in self.mapScene().items(rect, Qt.IntersectsItemShape, Qt.DescendingOrder, viewTransform(event)):
                if type(item) == MapObjectItem:
                    selectedItems.insert(item)

            newSelection = QSet()
            if (event.modifiers() & (Qt.ControlModifier | Qt.ShiftModifier)):
                newSelection = oldSelection | selectedItems
            else:
                newSelection = selectedItems

            self.mapScene().setSelectedObjectItems(newSelection)
            self.updateHandles()
        else:
            # Update the selected handles
            selectedHandles = QSet()
            for item in self.mapScene().items(rect, Qt.IntersectsItemShape, Qt.DescendingOrder, viewTransform(event)):
                if type(item) == PointHandle:
                    selectedHandles.insert(item)

            if (event.modifiers() & (Qt.ControlModifier | Qt.ShiftModifier)):
                self.setSelectedHandles(self.mSelectedHandles | selectedHandles)
            else:
                self.setSelectedHandles(selectedHandles)

    def startSelecting(self):
        self.mMode = EditPolygonTool.Selecting
        self.mapScene().addItem(self.mSelectionRectangle)

    def startMoving(self):
        # Move only the clicked handle, if it was not part of the selection
        if (not self.mSelectedHandles.contains(self.mClickedHandle)):
            self.setSelectedHandle(self.mClickedHandle)
        self.mMode = EditPolygonTool.Moving
        renderer = self.mapDocument().renderer()
        # Remember the current object positions
        self.mOldHandlePositions.clear()
        self.mOldPolygons.clear()
        self.mAlignPosition = renderer.screenToPixelCoords_((self.mSelectedHandles.begin()).pos())
        for handle in self.mSelectedHandles:
            pos = renderer.screenToPixelCoords_(handle.pos())
            self.mOldHandlePositions.append(handle.pos())
            if (pos.x() < self.mAlignPosition.x()):
                self.mAlignPosition.setX(pos.x())
            if (pos.y() < self.mAlignPosition.y()):
                self.mAlignPosition.setY(pos.y())
            mapObject = handle.mapObject()
            if (not self.mOldPolygons.contains(mapObject)):
                self.mOldPolygons.insert(mapObject, mapObject.polygon())

    def updateMovingItems(self, pos, modifiers):
        renderer = self.mapDocument().renderer()
        diff = pos - self.mStart
        snapHelper = SnapHelper(renderer, modifiers)
        if (snapHelper.snaps()):
            alignScreenPos = renderer.pixelToScreenCoords_(self.mAlignPosition)
            newAlignScreenPos = alignScreenPos + diff
            newAlignPixelPos = renderer.screenToPixelCoords_(newAlignScreenPos)
            snapHelper.snap(newAlignPixelPos)
            diff = renderer.pixelToScreenCoords_(newAlignPixelPos) - alignScreenPos

        i = 0
        for handle in self.mSelectedHandles:
            # update handle position
            newScreenPos = self.mOldHandlePositions.at(i) + diff
            handle.setPos(newScreenPos)

            # calculate new pixel position of polygon node
            item = handle.mapObjectItem()
            newInternalPos = item.mapFromScene(newScreenPos)
            newScenePos = item.pos() + newInternalPos
            newPixelPos = renderer.screenToPixelCoords_(newScenePos)

            # update the polygon
            mapObject = item.mapObject()
            polygon = mapObject.polygon()
            polygon[handle.pointIndex()] = newPixelPos - mapObject.position()
            self.mapDocument().mapObjectModel().setObjectPolygon(mapObject, polygon)

            i += 1

    def finishMoving(self, pos):
        self.mMode = EditPolygonTool.NoMode
        if (self.mStart == pos or self.mOldPolygons.isEmpty()): # Move is a no-op
            return
        undoStack = self.mapDocument().undoStack()
        undoStack.beginMacro(self.tr("Move %n Point(s)", "", self.mSelectedHandles.size()))
        # TODO: This isn't really optimal. Would be better to have a single undo
        # command that supports changing multiple map objects.
        for i in self.mOldPolygons:
            undoStack.push(ChangePolygon(self.mapDocument(), i[0], i[1]))

        undoStack.endMacro()
        self.mOldHandlePositions.clear()
        self.mOldPolygons.clear()

    def showHandleContextMenu(self, clickedHandle, screenPos):
        if (clickedHandle and not self.mSelectedHandles.contains(clickedHandle)):
            self.setSelectedHandle(clickedHandle)
        n = self.mSelectedHandles.size()
        delIcon = QIcon(":images/16x16/edit-delete.png")
        delText = self.tr("Delete %n Node(s)", "", n)
        menu = QMenu()
        deleteNodesAction = menu.addAction(delIcon, delText)
        joinNodesAction = menu.addAction(self.tr("Join Nodes"))
        splitSegmentsAction = menu.addAction(self.tr("Split Segments"))
        Utils.setThemeIcon(deleteNodesAction, "edit-delete")
        joinNodesAction.setEnabled(n > 1)
        splitSegmentsAction.setEnabled(n > 1)
        deleteNodesAction.triggered.connect(self.deleteNodes)
        joinNodesAction.triggered.connect(self.joinNodes)
        splitSegmentsAction.triggered.connect(self.splitSegments)
        menu.exec(screenPos)
Exemple #22
0
    def __init__(self, workingDocument, rules, rulePath):
        ##
        # where to work in
        ##
        self.mMapDocument = workingDocument

        ##
        # the same as mMapDocument.map()
        ##
        self.mMapWork = None
        if workingDocument:
            self.mMapWork = workingDocument.map()

        ##
        # map containing the rules, usually different than mMapWork
        ##
        self.mMapRules = rules

        ##
        # This contains all added tilesets as pointers.
        # if rules use Tilesets which are not in the mMapWork they are added.
        # keep track of them, because we need to delete them afterwards,
        # when they still are unused
        # they will be added while setupTilesets().
        ##
        self.mAddedTilesets = QVector()

        ##
        # description see: mAddedTilesets, just described by Strings
        ##
        self.mAddedTileLayers = QList()

        ##
        # Points to the tilelayer, which defines the inputregions.
        ##
        self.mLayerInputRegions = None

        ##
        # Points to the tilelayer, which defines the outputregions.
        ##
        self.mLayerOutputRegions = None

        ##
        # Contains all tilelayer pointers, which names begin with input*
        # It is sorted by index and name
        ##
        self.mInputRules = InputLayers()

        ##
        # List of Regions in mMapRules to know where the input rules are
        ##
        self.mRulesInput = QList()

        ##
        # List of regions in mMapRules to know where the output of a
        # rule is.
        # mRulesOutput[i] is the output of that rule,
        # which has the input at mRulesInput[i], meaning that mRulesInput
        # and mRulesOutput must match with the indexes.
        ##
        self.mRulesOutput = QList()

        ##
        # The inner set with layers to indexes is needed for translating
        # tile layers from mMapRules to mMapWork.
        #
        # The key is the pointer to the layer in the rulemap. The
        # pointer to the layer within the working map is not hardwired, but the
        # position in the layerlist, where it was found the last time.
        # This loosely bound pointer ensures we will get the right layer, since we
        # need to check before anyway, and it is still fast.
        #
        # The list is used to hold different translation tables
        # => one of the tables is chosen by chance, so randomness is available
        ##
        self.mLayerList = QList()
        ##
        # store the name of the processed rules file, to have detailed
        # error messages available
        ##
        self.mRulePath = rulePath

        ##
        # determines if all tiles in all touched layers should be deleted first.
        ##
        self.mDeleteTiles = False

        ##
        # This variable determines, how many overlapping tiles should be used.
        # The bigger the more area is remapped at an automapping operation.
        # This can lead to higher latency, but provides a better behavior on
        # interactive automapping.
        # It defaults to zero.
        ##
        self.mAutoMappingRadius = 0

        ##
        # Determines if a rule is allowed to overlap it
        ##
        self.mNoOverlappingRules = False

        self.mTouchedObjectGroups = QSet()
        self.mWarning = QString()
        self.mTouchedTileLayers = QSet()
        self.mError = ''

        if (not self.setupRuleMapProperties()):
            return
        if (not self.setupRuleMapTileLayers()):
            return
        if (not self.setupRuleList()):
            return
Exemple #23
0
class TilesetManager(QObject):
    mInstance = None
    ##
    # Emitted when a tileset's images have changed and views need updating.
    ##
    tilesetChanged = pyqtSignal(Tileset)
    ##
    # Emitted when any images of the tiles in the given \a tileset have
    # changed. This is used to trigger repaints for displaying tile
    # animations.
    ##
    repaintTileset = pyqtSignal(Tileset)

    ##
    # Constructor. Only used by the tileset manager it
    ##
    def __init__(self):
        super().__init__()

        ##
        # Stores the tilesets and maps them to the number of references.
        ##
        self.mTilesets = QMap()
        self.mChangedFiles = QSet()
        self.mWatcher = FileSystemWatcher(self)
        self.mAnimationDriver = TileAnimationDriver(self)
        self.mReloadTilesetsOnChange = False
        self.mChangedFilesTimer = QTimer()

        self.mWatcher.fileChanged.connect(self.fileChanged)
        self.mChangedFilesTimer.setInterval(500)
        self.mChangedFilesTimer.setSingleShot(True)
        self.mChangedFilesTimer.timeout.connect(self.fileChangedTimeout)
        self.mAnimationDriver.update.connect(self.advanceTileAnimations)

    ##
    # Destructor.
    ##
    def __del__(self):
        # Since all MapDocuments should be deleted first, we assert that there are
        # no remaining tileset references.
        self.mTilesets.size() == 0

    ##
    # Requests the tileset manager. When the manager doesn't exist yet, it
    # will be created.
    ##
    def instance():
        if (not TilesetManager.mInstance):
            TilesetManager.mInstance = TilesetManager()
        return TilesetManager.mInstance

    ##
    # Deletes the tileset manager instance, when it exists.
    ##
    def deleteInstance():
        del TilesetManager.mInstance
        TilesetManager.mInstance = None

    def findTileset(self, arg):
        tp = type(arg)
        if tp in [QString, str]:
            ##
            # Searches for a tileset matching the given file name.
            # @return a tileset matching the given file name, or 0 if none exists
            ##
            fileName = arg
            for tileset in self.tilesets():
                if (tileset.fileName() == fileName):
                    return tileset
            return None
        elif tp==TilesetSpec:
            ##
            # Searches for a tileset matching the given specification.
            # @return a tileset matching the given specification, or 0 if none exists
            ##
            spec = arg
            for tileset in self.tilesets():
                if (tileset.imageSource() == spec.imageSource
                    and tileset.tileWidth() == spec.tileWidth
                    and tileset.tileHeight() == spec.tileHeight
                    and tileset.tileSpacing() == spec.tileSpacing
                    and tileset.margin() == spec.margin):

                    return tileset

            return None

    ##
    # Adds a tileset reference. This will make sure the tileset is watched for
    # changes and can be found using findTileset().
    ##
    def addReference(self, tileset):
        if (self.mTilesets.contains(tileset)):
            self.mTilesets[tileset] += 1
        else:
            self.mTilesets.insert(tileset, 1)
            imgSrc = tileset.imageSource()
            if (imgSrc != ''):
                self.mWatcher.addPath(imgSrc)

    ##
    # Removes a tileset reference. When the last reference has been removed,
    # the tileset is no longer watched for changes.
    ##
    def removeReference(self, tileset):
        if self.mTilesets[tileset]:
            self.mTilesets[tileset] -= 1
        if (self.mTilesets[tileset] == 0):
            self.mTilesets.remove(tileset)
            if (tileset.imageSource()!=''):
                self.mWatcher.removePath(tileset.imageSource())

    ##
    # Convenience method to add references to multiple tilesets.
    # @see addReference
    ##
    def addReferences(self, tilesets):
        for tileset in tilesets:
            self.addReference(tileset)

    ##
    # Convenience method to remove references from multiple tilesets.
    # @see removeReference
    ##
    def removeReferences(self, tilesets):
        for tileset in tilesets:
            self.removeReference(tileset)

    ##
    # Returns all currently available tilesets.
    ##
    def tilesets(self):
        return self.mTilesets.keys()

    ##
    # Forces a tileset to reload.
    ##
    def forceTilesetReload(self, tileset):
        if (not self.mTilesets.contains(tileset)):
            return
        fileName = tileset.imageSource()
        if (tileset.loadFromImage(fileName)):
            self.tilesetChanged.emit(tileset)

    ##
    # Sets whether tilesets are automatically reloaded when their tileset
    # image changes.
    ##
    def setReloadTilesetsOnChange(self, enabled):
        self.mReloadTilesetsOnChange = enabled
        # TODO: Clear the file system watcher when disabled

    def reloadTilesetsOnChange(self):
        return self.mReloadTilesetsOnChange
    ##
    # Sets whether tile animations are running.
    ##
    def setAnimateTiles(self, enabled):
        # TODO: Avoid running the driver when there are no animated tiles
        if (enabled):
            self.mAnimationDriver.start()
        else:
            self.mAnimationDriver.stop()

    def animateTiles(self):
        return self.mAnimationDriver.state() == QAbstractAnimation.Running

    def fileChanged(self, path):
        if (not self.mReloadTilesetsOnChange):
            return
        ##
        # Use a one-shot timer since GIMP (for example) seems to generate many
        # file changes during a save, and some of the intermediate attempts to
        # reload the tileset images actually fail (at least for .png files).
        ##
        self.mChangedFiles.insert(path)
        self.mChangedFilesTimer.start()

    def fileChangedTimeout(self):
        for tileset in self.tilesets():
            fileName = tileset.imageSource()
            if (self.mChangedFiles.contains(fileName)):
                if (tileset.loadFromImage(fileName)):
                    self.tilesetChanged.emit(tileset)

        self.mChangedFiles.clear()

    def advanceTileAnimations(self, ms):
        # TODO: This could be more optimal by keeping track of the list of
        # actually animated tiles
        for tileset in self.tilesets():
            imageChanged = False
            for tile in tileset.tiles():
                imageChanged |= tile.advanceAnimation(ms)
            if (imageChanged):
                self.repaintTileset.emit(tileset)
Exemple #24
0
 def selectedObjectItems(self):
     return QSet(self.mSelectedObjectItems)
Exemple #25
0
class MapScene(QGraphicsScene):
    selectedObjectItemsChanged = pyqtSignal()

    ##
    # Constructor.
    ##
    def __init__(self, parent):
        super().__init__(parent)
        self.mMapDocument = None
        self.mSelectedTool = None
        self.mActiveTool = None
        self.mObjectSelectionItem = None
        self.mUnderMouse = False
        self.mCurrentModifiers = Qt.NoModifier,
        self.mDarkRectangle = QGraphicsRectItem()
        self.mDefaultBackgroundColor = Qt.darkGray

        self.mLayerItems = QVector()
        self.mObjectItems = QMap()
        self.mObjectLineWidth = 0.0
        self.mSelectedObjectItems = QSet()
        self.mLastMousePos = QPointF()
        self.mShowTileObjectOutlines = False
        self.mHighlightCurrentLayer = False
        self.mGridVisible = False

        self.setBackgroundBrush(self.mDefaultBackgroundColor)
        tilesetManager = TilesetManager.instance()
        tilesetManager.tilesetChanged.connect(self.tilesetChanged)
        tilesetManager.repaintTileset.connect(self.tilesetChanged)
        prefs = preferences.Preferences.instance()
        prefs.showGridChanged.connect(self.setGridVisible)
        prefs.showTileObjectOutlinesChanged.connect(
            self.setShowTileObjectOutlines)
        prefs.objectTypesChanged.connect(self.syncAllObjectItems)
        prefs.highlightCurrentLayerChanged.connect(
            self.setHighlightCurrentLayer)
        prefs.gridColorChanged.connect(self.update)
        prefs.objectLineWidthChanged.connect(self.setObjectLineWidth)
        self.mDarkRectangle.setPen(QPen(Qt.NoPen))
        self.mDarkRectangle.setBrush(Qt.black)
        self.mDarkRectangle.setOpacity(darkeningFactor)
        self.addItem(self.mDarkRectangle)
        self.mGridVisible = prefs.showGrid()
        self.mObjectLineWidth = prefs.objectLineWidth()
        self.mShowTileObjectOutlines = prefs.showTileObjectOutlines()
        self.mHighlightCurrentLayer = prefs.highlightCurrentLayer()
        # Install an event filter so that we can get key events on behalf of the
        # active tool without having to have the current focus.
        QCoreApplication.instance().installEventFilter(self)

    ##
    # Destructor.
    ##
    def __del__(self):
        if QCoreApplication.instance():
            QCoreApplication.instance().removeEventFilter(self)

    ##
    # Returns the map document this scene is displaying.
    ##
    def mapDocument(self):
        return self.mMapDocument

    ##
    # Sets the map this scene displays.
    ##
    def setMapDocument(self, mapDocument):
        if (self.mMapDocument):
            self.mMapDocument.disconnect()
            if (not self.mSelectedObjectItems.isEmpty()):
                self.mSelectedObjectItems.clear()
                self.selectedObjectItemsChanged.emit()

        self.mMapDocument = mapDocument
        if (self.mMapDocument):
            renderer = self.mMapDocument.renderer()
            renderer.setObjectLineWidth(self.mObjectLineWidth)
            renderer.setFlag(RenderFlag.ShowTileObjectOutlines,
                             self.mShowTileObjectOutlines)
            self.mMapDocument.mapChanged.connect(self.mapChanged)
            self.mMapDocument.regionChanged.connect(self.repaintRegion)
            self.mMapDocument.tileLayerDrawMarginsChanged.connect(
                self.tileLayerDrawMarginsChanged)
            self.mMapDocument.layerAdded.connect(self.layerAdded)
            self.mMapDocument.layerRemoved.connect(self.layerRemoved)
            self.mMapDocument.layerChanged.connect(self.layerChanged)
            self.mMapDocument.objectGroupChanged.connect(
                self.objectGroupChanged)
            self.mMapDocument.imageLayerChanged.connect(self.imageLayerChanged)
            self.mMapDocument.currentLayerIndexChanged.connect(
                self.currentLayerIndexChanged)
            self.mMapDocument.tilesetTileOffsetChanged.connect(
                self.tilesetTileOffsetChanged)
            self.mMapDocument.objectsInserted.connect(self.objectsInserted)
            self.mMapDocument.objectsRemoved.connect(self.objectsRemoved)
            self.mMapDocument.objectsChanged.connect(self.objectsChanged)
            self.mMapDocument.objectsIndexChanged.connect(
                self.objectsIndexChanged)
            self.mMapDocument.selectedObjectsChanged.connect(
                self.updateSelectedObjectItems)

        self.refreshScene()

    ##
    # Returns whether the tile grid is visible.
    ##
    def isGridVisible(self):
        return self.mGridVisible

    ##
    # Returns the set of selected map object items.
    ##
    def selectedObjectItems(self):
        return QSet(self.mSelectedObjectItems)

    ##
    # Sets the set of selected map object items. This translates to a call to
    # MapDocument.setSelectedObjects.
    ##
    def setSelectedObjectItems(self, items):
        # Inform the map document about the newly selected objects
        selectedObjects = QList()
        #selectedObjects.reserve(items.size())
        for item in items:
            selectedObjects.append(item.mapObject())
        self.mMapDocument.setSelectedObjects(selectedObjects)

    ##
    # Returns the MapObjectItem associated with the given \a mapObject.
    ##
    def itemForObject(self, object):
        return self.mObjectItems[object]

    ##
    # Enables the selected tool at this map scene.
    # Therefore it tells that tool, that this is the active map scene.
    ##
    def enableSelectedTool(self):
        if (not self.mSelectedTool or not self.mMapDocument):
            return
        self.mActiveTool = self.mSelectedTool
        self.mActiveTool.activate(self)
        self.mCurrentModifiers = QApplication.keyboardModifiers()
        if (self.mCurrentModifiers != Qt.NoModifier):
            self.mActiveTool.modifiersChanged(self.mCurrentModifiers)
        if (self.mUnderMouse):
            self.mActiveTool.mouseEntered()
            self.mActiveTool.mouseMoved(self.mLastMousePos,
                                        Qt.KeyboardModifiers())

    def disableSelectedTool(self):
        if (not self.mActiveTool):
            return
        if (self.mUnderMouse):
            self.mActiveTool.mouseLeft()
        self.mActiveTool.deactivate(self)
        self.mActiveTool = None

    ##
    # Sets the currently selected tool.
    ##
    def setSelectedTool(self, tool):
        self.mSelectedTool = tool

    ##
    # QGraphicsScene.drawForeground override that draws the tile grid.
    ##
    def drawForeground(self, painter, rect):
        if (not self.mMapDocument or not self.mGridVisible):
            return

        offset = QPointF()

        # Take into account the offset of the current layer
        layer = self.mMapDocument.currentLayer()
        if layer:
            offset = layer.offset()
            painter.translate(offset)

        prefs = preferences.Preferences.instance()
        self.mMapDocument.renderer().drawGrid(painter,
                                              rect.translated(-offset),
                                              prefs.gridColor())

    ##
    # Override for handling enter and leave events.
    ##
    def event(self, event):
        x = event.type()
        if x == QEvent.Enter:
            self.mUnderMouse = True
            if (self.mActiveTool):
                self.mActiveTool.mouseEntered()
        elif x == QEvent.Leave:
            self.mUnderMouse = False
            if (self.mActiveTool):
                self.mActiveTool.mouseLeft()
        else:
            pass

        return super().event(event)

    def keyPressEvent(self, event):
        if (self.mActiveTool):
            self.mActiveTool.keyPressed(event)
        if (not (self.mActiveTool and event.isAccepted())):
            super().keyPressEvent(event)

    def mouseMoveEvent(self, mouseEvent):
        self.mLastMousePos = mouseEvent.scenePos()
        if (not self.mMapDocument):
            return
        super().mouseMoveEvent(mouseEvent)
        if (mouseEvent.isAccepted()):
            return
        if (self.mActiveTool):
            self.mActiveTool.mouseMoved(mouseEvent.scenePos(),
                                        mouseEvent.modifiers())
            mouseEvent.accept()

    def mousePressEvent(self, mouseEvent):
        super().mousePressEvent(mouseEvent)
        if (mouseEvent.isAccepted()):
            return
        if (self.mActiveTool):
            mouseEvent.accept()
            self.mActiveTool.mousePressed(mouseEvent)

    def mouseReleaseEvent(self, mouseEvent):
        super().mouseReleaseEvent(mouseEvent)
        if (mouseEvent.isAccepted()):
            return
        if (self.mActiveTool):
            mouseEvent.accept()
            self.mActiveTool.mouseReleased(mouseEvent)

    ##
    # Override to ignore drag enter events.
    ##
    def dragEnterEvent(self, event):
        event.ignore()

    ##
    # Sets whether the tile grid is visible.
    ##
    def setGridVisible(self, visible):
        if (self.mGridVisible == visible):
            return
        self.mGridVisible = visible
        self.update()

    def setObjectLineWidth(self, lineWidth):
        if (self.mObjectLineWidth == lineWidth):
            return
        self.mObjectLineWidth = lineWidth
        if (self.mMapDocument):
            self.mMapDocument.renderer().setObjectLineWidth(lineWidth)
            # Changing the line width can change the size of the object items
            if (not self.mObjectItems.isEmpty()):
                for item in self.mObjectItems:
                    item[1].syncWithMapObject()
                self.update()

    def setShowTileObjectOutlines(self, enabled):
        if (self.mShowTileObjectOutlines == enabled):
            return
        self.mShowTileObjectOutlines = enabled
        if (self.mMapDocument):
            self.mMapDocument.renderer().setFlag(
                RenderFlag.ShowTileObjectOutlines, enabled)
            if (not self.mObjectItems.isEmpty()):
                self.update()

    ##
    # Sets whether the current layer should be highlighted.
    ##
    def setHighlightCurrentLayer(self, highlightCurrentLayer):
        if (self.mHighlightCurrentLayer == highlightCurrentLayer):
            return
        self.mHighlightCurrentLayer = highlightCurrentLayer
        self.updateCurrentLayerHighlight()

    ##
    # Refreshes the map scene.
    ##
    def refreshScene(self):
        self.mLayerItems.clear()
        self.mObjectItems.clear()
        self.removeItem(self.mDarkRectangle)
        self.clear()
        self.addItem(self.mDarkRectangle)
        if (not self.mMapDocument):
            self.setSceneRect(QRectF())
            return

        self.updateSceneRect()

        map = self.mMapDocument.map()
        self.mLayerItems.resize(map.layerCount())
        if (map.backgroundColor().isValid()):
            self.setBackgroundBrush(map.backgroundColor())
        else:
            self.setBackgroundBrush(self.mDefaultBackgroundColor)
        layerIndex = 0
        for layer in map.layers():
            layerItem = self.createLayerItem(layer)
            layerItem.setZValue(layerIndex)
            self.addItem(layerItem)
            self.mLayerItems[layerIndex] = layerItem
            layerIndex += 1

        tileSelectionItem = TileSelectionItem(self.mMapDocument)
        tileSelectionItem.setZValue(10000 - 2)
        self.addItem(tileSelectionItem)
        self.mObjectSelectionItem = ObjectSelectionItem(self.mMapDocument)
        self.mObjectSelectionItem.setZValue(10000 - 1)
        self.addItem(self.mObjectSelectionItem)
        self.updateCurrentLayerHighlight()

    ##
    # Repaints the specified region. The region is in tile coordinates.
    ##
    def repaintRegion(self, region, layer):
        renderer = self.mMapDocument.renderer()
        margins = self.mMapDocument.map().drawMargins()
        for r in region.rects():
            boundingRect = QRectF(renderer.boundingRect(r))
            self.update(
                QRectF(
                    renderer.boundingRect(r).adjusted(-margins.left(),
                                                      -margins.top(),
                                                      margins.right(),
                                                      margins.bottom())))
            boundingRect.translate(layer.offset())
            self.update(boundingRect)

    def currentLayerIndexChanged(self):
        self.updateCurrentLayerHighlight()
        # New layer may have a different offset, affecting the grid
        if self.mGridVisible:
            self.update()

    ##
    # Adapts the scene, layers and objects to new map size, orientation or
    # background color.
    ##
    def mapChanged(self):
        self.updateSceneRect()
        for item in self.mLayerItems:
            tli = item
            if type(tli) == TileLayerItem:
                tli.syncWithTileLayer()

        for item in self.mObjectItems.values():
            item.syncWithMapObject()
        map = self.mMapDocument.map()
        if (map.backgroundColor().isValid()):
            self.setBackgroundBrush(map.backgroundColor())
        else:
            self.setBackgroundBrush(self.mDefaultBackgroundColor)

    def tilesetChanged(self, tileset):
        if (not self.mMapDocument):
            return
        if (contains(self.mMapDocument.map().tilesets(), tileset)):
            self.update()

    def tileLayerDrawMarginsChanged(self, tileLayer):
        index = self.mMapDocument.map().layers().indexOf(tileLayer)
        item = self.mLayerItems.at(index)
        item.syncWithTileLayer()

    def layerAdded(self, index):
        layer = self.mMapDocument.map().layerAt(index)
        layerItem = self.createLayerItem(layer)
        self.addItem(layerItem)
        self.mLayerItems.insert(index, layerItem)
        z = 0
        for item in self.mLayerItems:
            item.setZValue(z)
            z += 1

    def layerRemoved(self, index):
        self.mLayerItems.remove(index)

    ##
    # A layer has changed. This can mean that the layer visibility, opacity or
    # offset changed.
    ##
    def layerChanged(self, index):
        layer = self.mMapDocument.map().layerAt(index)
        layerItem = self.mLayerItems.at(index)
        layerItem.setVisible(layer.isVisible())
        multiplier = 1
        if (self.mHighlightCurrentLayer
                and self.mMapDocument.currentLayerIndex() < index):
            multiplier = opacityFactor
        layerItem.setOpacity(layer.opacity() * multiplier)
        layerItem.setPos(layer.offset())

        # Layer offset may have changed, affecting the scene rect and grid
        self.updateSceneRect()
        if self.mGridVisible:
            self.update()

    ##
    # When an object group has changed it may mean its color or drawing order
    # changed, which affects all its objects.
    ##
    def objectGroupChanged(self, objectGroup):
        self.objectsChanged(objectGroup.objects())
        self.objectsIndexChanged(objectGroup, 0, objectGroup.objectCount() - 1)

    ##
    # When an image layer has changed, it may change size and it may look
    # differently.
    ##
    def imageLayerChanged(self, imageLayer):
        index = self.mMapDocument.map().layers().indexOf(imageLayer)
        item = self.mLayerItems.at(index)
        item.syncWithImageLayer()
        item.update()

    ##
    # When the tile offset of a tileset has changed, it can affect the bounding
    # rect of all tile layers and tile objects. It also requires a full repaint.
    ##
    def tilesetTileOffsetChanged(self, tileset):
        self.update()
        for item in self.mLayerItems:
            tli = item
            if type(tli) == TileLayerItem:
                tli.syncWithTileLayer()
        for item in self.mObjectItems:
            cell = item.mapObject().cell()
            if (not cell.isEmpty() and cell.tile.tileset() == tileset):
                item.syncWithMapObject()

    ##
    # Inserts map object items for the given objects.
    ##
    def objectsInserted(self, objectGroup, first, last):
        ogItem = None
        # Find the object group item for the object group
        for item in self.mLayerItems:
            ogi = item
            if type(ogi) == ObjectGroupItem:
                if (ogi.objectGroup() == objectGroup):
                    ogItem = ogi
                    break

        drawOrder = objectGroup.drawOrder()
        for i in range(first, last + 1):
            object = objectGroup.objectAt(i)
            item = MapObjectItem(object, self.mMapDocument, ogItem)
            if (drawOrder == ObjectGroup.DrawOrder.TopDownOrder):
                item.setZValue(item.y())
            else:
                item.setZValue(i)
            self.mObjectItems.insert(object, item)

    ##
    # Removes the map object items related to the given objects.
    ##
    def objectsRemoved(self, objects):
        for o in objects:
            i = self.mObjectItems.find(o)
            self.mSelectedObjectItems.remove(i)
            # python would not force delete QGraphicsItem
            self.removeItem(i)
            self.mObjectItems.erase(o)

    ##
    # Updates the map object items related to the given objects.
    ##
    def objectsChanged(self, objects):
        for object in objects:
            item = self.itemForObject(object)
            item.syncWithMapObject()

    ##
    # Updates the Z value of the objects when appropriate.
    ##
    def objectsIndexChanged(self, objectGroup, first, last):
        if (objectGroup.drawOrder() != ObjectGroup.DrawOrder.IndexOrder):
            return
        for i in range(first, last + 1):
            item = self.itemForObject(objectGroup.objectAt(i))
            item.setZValue(i)

    def updateSelectedObjectItems(self):
        objects = self.mMapDocument.selectedObjects()
        items = QSet()
        for object in objects:
            item = self.itemForObject(object)
            if item:
                items.insert(item)

        self.mSelectedObjectItems = items
        self.selectedObjectItemsChanged.emit()

    def syncAllObjectItems(self):
        for item in self.mObjectItems:
            item.syncWithMapObject()

    def createLayerItem(self, layer):
        layerItem = None
        tl = layer.asTileLayer()
        if tl:
            layerItem = TileLayerItem(tl, self.mMapDocument)
        else:
            og = layer.asObjectGroup()
            if og:
                drawOrder = og.drawOrder()
                ogItem = ObjectGroupItem(og)
                objectIndex = 0
                for object in og.objects():
                    item = MapObjectItem(object, self.mMapDocument, ogItem)
                    if (drawOrder == ObjectGroup.DrawOrder.TopDownOrder):
                        item.setZValue(item.y())
                    else:
                        item.setZValue(objectIndex)
                    self.mObjectItems.insert(object, item)
                    objectIndex += 1

                layerItem = ogItem
            else:
                il = layer.asImageLayer()
                if il:
                    layerItem = ImageLayerItem(il, self.mMapDocument)

        layerItem.setVisible(layer.isVisible())
        return layerItem

    def updateSceneRect(self):
        mapSize = self.mMapDocument.renderer().mapSize()
        sceneRect = QRectF(0, 0, mapSize.width(), mapSize.height())

        margins = self.mMapDocument.map().computeLayerOffsetMargins()
        sceneRect.adjust(-margins.left(), -margins.top(), margins.right(),
                         margins.bottom())

        self.setSceneRect(sceneRect)
        self.mDarkRectangle.setRect(sceneRect)

    def updateCurrentLayerHighlight(self):
        if (not self.mMapDocument):
            return
        currentLayerIndex = self.mMapDocument.currentLayerIndex()
        if (not self.mHighlightCurrentLayer or currentLayerIndex == -1):
            self.mDarkRectangle.setVisible(False)
            # Restore opacity for all layers
            for i in range(self.mLayerItems.size()):
                layer = self.mMapDocument.map().layerAt(i)
                self.mLayerItems.at(i).setOpacity(layer.opacity())

            return

        # Darken layers below the current layer
        self.mDarkRectangle.setZValue(currentLayerIndex - 0.5)
        self.mDarkRectangle.setVisible(True)
        # Set layers above the current layer to half opacity
        for i in range(1, self.mLayerItems.size()):
            layer = self.mMapDocument.map().layerAt(i)
            if currentLayerIndex < i:
                _x = opacityFactor
            else:
                _x = 1
            multiplier = _x
            self.mLayerItems.at(i).setOpacity(layer.opacity() * multiplier)

    def eventFilter(self, object, event):
        x = event.type()
        if x == QEvent.KeyPress or x == QEvent.KeyRelease:
            keyEvent = event
            newModifiers = keyEvent.modifiers()
            if (self.mActiveTool and newModifiers != self.mCurrentModifiers):
                self.mActiveTool.modifiersChanged(newModifiers)
                self.mCurrentModifiers = newModifiers
        else:
            pass

        return False
Exemple #26
0
class TilesetManager(QObject):
    mInstance = None
    ##
    # Emitted when a tileset's images have changed and views need updating.
    ##
    tilesetChanged = pyqtSignal(Tileset)
    ##
    # Emitted when any images of the tiles in the given \a tileset have
    # changed. This is used to trigger repaints for displaying tile
    # animations.
    ##
    repaintTileset = pyqtSignal(Tileset)

    ##
    # Constructor. Only used by the tileset manager it
    ##
    def __init__(self):
        super().__init__()

        ##
        # Stores the tilesets and maps them to the number of references.
        ##
        self.mTilesets = QMap()
        self.mChangedFiles = QSet()
        self.mWatcher = FileSystemWatcher(self)
        self.mAnimationDriver = TileAnimationDriver(self)
        self.mReloadTilesetsOnChange = False
        self.mChangedFilesTimer = QTimer()

        self.mWatcher.fileChanged.connect(self.fileChanged)
        self.mChangedFilesTimer.setInterval(500)
        self.mChangedFilesTimer.setSingleShot(True)
        self.mChangedFilesTimer.timeout.connect(self.fileChangedTimeout)
        self.mAnimationDriver.update.connect(self.advanceTileAnimations)

    ##
    # Destructor.
    ##
    def __del__(self):
        # Since all MapDocuments should be deleted first, we assert that there are
        # no remaining tileset references.
        self.mTilesets.size() == 0

    ##
    # Requests the tileset manager. When the manager doesn't exist yet, it
    # will be created.
    ##
    def instance():
        if (not TilesetManager.mInstance):
            TilesetManager.mInstance = TilesetManager()
        return TilesetManager.mInstance

    ##
    # Deletes the tileset manager instance, when it exists.
    ##
    def deleteInstance():
        del TilesetManager.mInstance
        TilesetManager.mInstance = None

    def findTileset(self, arg):
        tp = type(arg)
        if tp in [QString, str]:
            ##
            # Searches for a tileset matching the given file name.
            # @return a tileset matching the given file name, or 0 if none exists
            ##
            fileName = arg
            for tileset in self.tilesets():
                if (tileset.fileName() == fileName):
                    return tileset
            return None
        elif tp == TilesetSpec:
            ##
            # Searches for a tileset matching the given specification.
            # @return a tileset matching the given specification, or 0 if none exists
            ##
            spec = arg
            for tileset in self.tilesets():
                if (tileset.imageSource() == spec.imageSource
                        and tileset.tileWidth() == spec.tileWidth
                        and tileset.tileHeight() == spec.tileHeight
                        and tileset.tileSpacing() == spec.tileSpacing
                        and tileset.margin() == spec.margin):

                    return tileset

            return None

    ##
    # Adds a tileset reference. This will make sure the tileset is watched for
    # changes and can be found using findTileset().
    ##
    def addReference(self, tileset):
        if (self.mTilesets.contains(tileset)):
            self.mTilesets[tileset] += 1
        else:
            self.mTilesets.insert(tileset, 1)
            imgSrc = tileset.imageSource()
            if (imgSrc != ''):
                self.mWatcher.addPath(imgSrc)

    ##
    # Removes a tileset reference. When the last reference has been removed,
    # the tileset is no longer watched for changes.
    ##
    def removeReference(self, tileset):
        if self.mTilesets[tileset]:
            self.mTilesets[tileset] -= 1
        if (self.mTilesets[tileset] == 0):
            self.mTilesets.remove(tileset)
            if (tileset.imageSource() != ''):
                self.mWatcher.removePath(tileset.imageSource())

    ##
    # Convenience method to add references to multiple tilesets.
    # @see addReference
    ##
    def addReferences(self, tilesets):
        for tileset in tilesets:
            self.addReference(tileset)

    ##
    # Convenience method to remove references from multiple tilesets.
    # @see removeReference
    ##
    def removeReferences(self, tilesets):
        for tileset in tilesets:
            self.removeReference(tileset)

    ##
    # Returns all currently available tilesets.
    ##
    def tilesets(self):
        return self.mTilesets.keys()

    ##
    # Forces a tileset to reload.
    ##
    def forceTilesetReload(self, tileset):
        if (not self.mTilesets.contains(tileset)):
            return
        fileName = tileset.imageSource()
        if (tileset.loadFromImage(fileName)):
            self.tilesetChanged.emit(tileset)

    ##
    # Sets whether tilesets are automatically reloaded when their tileset
    # image changes.
    ##
    def setReloadTilesetsOnChange(self, enabled):
        self.mReloadTilesetsOnChange = enabled
        # TODO: Clear the file system watcher when disabled

    def reloadTilesetsOnChange(self):
        return self.mReloadTilesetsOnChange

    ##
    # Sets whether tile animations are running.
    ##
    def setAnimateTiles(self, enabled):
        # TODO: Avoid running the driver when there are no animated tiles
        if (enabled):
            self.mAnimationDriver.start()
        else:
            self.mAnimationDriver.stop()

    def animateTiles(self):
        return self.mAnimationDriver.state() == QAbstractAnimation.Running

    def fileChanged(self, path):
        if (not self.mReloadTilesetsOnChange):
            return
        ##
        # Use a one-shot timer since GIMP (for example) seems to generate many
        # file changes during a save, and some of the intermediate attempts to
        # reload the tileset images actually fail (at least for .png files).
        ##
        self.mChangedFiles.insert(path)
        self.mChangedFilesTimer.start()

    def fileChangedTimeout(self):
        for tileset in self.tilesets():
            fileName = tileset.imageSource()
            if (self.mChangedFiles.contains(fileName)):
                if (tileset.loadFromImage(fileName)):
                    self.tilesetChanged.emit(tileset)

        self.mChangedFiles.clear()

    def advanceTileAnimations(self, ms):
        # TODO: This could be more optimal by keeping track of the list of
        # actually animated tiles
        for tileset in self.tilesets():
            imageChanged = False
            for tile in tileset.tiles():
                imageChanged |= tile.advanceAnimation(ms)
            if (imageChanged):
                self.repaintTileset.emit(tileset)
Exemple #27
0
class MapScene(QGraphicsScene):
    selectedObjectItemsChanged = pyqtSignal()

    ##
    # Constructor.
    ##
    def __init__(self, parent):
        super().__init__(parent)
        self.mMapDocument = None
        self.mSelectedTool = None
        self.mActiveTool = None
        self.mObjectSelectionItem = None
        self.mUnderMouse = False
        self.mCurrentModifiers = Qt.NoModifier,
        self.mDarkRectangle = QGraphicsRectItem()
        self.mDefaultBackgroundColor = Qt.darkGray

        self.mLayerItems = QVector()
        self.mObjectItems = QMap()
        self.mObjectLineWidth = 0.0
        self.mSelectedObjectItems = QSet()
        self.mLastMousePos = QPointF()
        self.mShowTileObjectOutlines = False
        self.mHighlightCurrentLayer = False
        self.mGridVisible = False

        self.setBackgroundBrush(self.mDefaultBackgroundColor)
        tilesetManager = TilesetManager.instance()
        tilesetManager.tilesetChanged.connect(self.tilesetChanged)
        tilesetManager.repaintTileset.connect(self.tilesetChanged)
        prefs = preferences.Preferences.instance()
        prefs.showGridChanged.connect(self.setGridVisible)
        prefs.showTileObjectOutlinesChanged.connect(self.setShowTileObjectOutlines)
        prefs.objectTypesChanged.connect(self.syncAllObjectItems)
        prefs.highlightCurrentLayerChanged.connect(self.setHighlightCurrentLayer)
        prefs.gridColorChanged.connect(self.update)
        prefs.objectLineWidthChanged.connect(self.setObjectLineWidth)
        self.mDarkRectangle.setPen(QPen(Qt.NoPen))
        self.mDarkRectangle.setBrush(Qt.black)
        self.mDarkRectangle.setOpacity(darkeningFactor)
        self.addItem(self.mDarkRectangle)
        self.mGridVisible = prefs.showGrid()
        self.mObjectLineWidth = prefs.objectLineWidth()
        self.mShowTileObjectOutlines = prefs.showTileObjectOutlines()
        self.mHighlightCurrentLayer = prefs.highlightCurrentLayer()
        # Install an event filter so that we can get key events on behalf of the
        # active tool without having to have the current focus.
        QCoreApplication.instance().installEventFilter(self)

    ##
    # Destructor.
    ##
    def __del__(self):
        if QCoreApplication.instance():
            QCoreApplication.instance().removeEventFilter(self)

    ##
    # Returns the map document this scene is displaying.
    ##
    def mapDocument(self):
        return self.mMapDocument

    ##
    # Sets the map this scene displays.
    ##
    def setMapDocument(self, mapDocument):
        if (self.mMapDocument):
            self.mMapDocument.disconnect()
            if (not self.mSelectedObjectItems.isEmpty()):
                self.mSelectedObjectItems.clear()
                self.selectedObjectItemsChanged.emit()

        self.mMapDocument = mapDocument
        if (self.mMapDocument):
            renderer = self.mMapDocument.renderer()
            renderer.setObjectLineWidth(self.mObjectLineWidth)
            renderer.setFlag(RenderFlag.ShowTileObjectOutlines, self.mShowTileObjectOutlines)
            self.mMapDocument.mapChanged.connect(self.mapChanged)
            self.mMapDocument.regionChanged.connect(self.repaintRegion)
            self.mMapDocument.tileLayerDrawMarginsChanged.connect(self.tileLayerDrawMarginsChanged)
            self.mMapDocument.layerAdded.connect(self.layerAdded)
            self.mMapDocument.layerRemoved.connect(self.layerRemoved)
            self.mMapDocument.layerChanged.connect(self.layerChanged)
            self.mMapDocument.objectGroupChanged.connect(self.objectGroupChanged)
            self.mMapDocument.imageLayerChanged.connect(self.imageLayerChanged)
            self.mMapDocument.currentLayerIndexChanged.connect(self.currentLayerIndexChanged)
            self.mMapDocument.tilesetTileOffsetChanged.connect(self.tilesetTileOffsetChanged)
            self.mMapDocument.objectsInserted.connect(self.objectsInserted)
            self.mMapDocument.objectsRemoved.connect(self.objectsRemoved)
            self.mMapDocument.objectsChanged.connect(self.objectsChanged)
            self.mMapDocument.objectsIndexChanged.connect(self.objectsIndexChanged)
            self.mMapDocument.selectedObjectsChanged.connect(self.updateSelectedObjectItems)

        self.refreshScene()

    ##
    # Returns whether the tile grid is visible.
    ##
    def isGridVisible(self):
        return self.mGridVisible

    ##
    # Returns the set of selected map object items.
    ##
    def selectedObjectItems(self):
        return QSet(self.mSelectedObjectItems)

    ##
    # Sets the set of selected map object items. This translates to a call to
    # MapDocument.setSelectedObjects.
    ##
    def setSelectedObjectItems(self, items):
        # Inform the map document about the newly selected objects
        selectedObjects = QList()
        #selectedObjects.reserve(items.size())
        for item in items:
            selectedObjects.append(item.mapObject())
        self.mMapDocument.setSelectedObjects(selectedObjects)

    ##
    # Returns the MapObjectItem associated with the given \a mapObject.
    ##
    def itemForObject(self, object):
        return self.mObjectItems[object]

    ##
    # Enables the selected tool at this map scene.
    # Therefore it tells that tool, that this is the active map scene.
    ##
    def enableSelectedTool(self):
        if (not self.mSelectedTool or not self.mMapDocument):
            return
        self.mActiveTool = self.mSelectedTool
        self.mActiveTool.activate(self)
        self.mCurrentModifiers = QApplication.keyboardModifiers()
        if (self.mCurrentModifiers != Qt.NoModifier):
            self.mActiveTool.modifiersChanged(self.mCurrentModifiers)
        if (self.mUnderMouse):
            self.mActiveTool.mouseEntered()
            self.mActiveTool.mouseMoved(self.mLastMousePos, Qt.KeyboardModifiers())

    def disableSelectedTool(self):
        if (not self.mActiveTool):
            return
        if (self.mUnderMouse):
            self.mActiveTool.mouseLeft()
        self.mActiveTool.deactivate(self)
        self.mActiveTool = None

    ##
    # Sets the currently selected tool.
    ##
    def setSelectedTool(self, tool):
        self.mSelectedTool = tool

    ##
    # QGraphicsScene.drawForeground override that draws the tile grid.
    ##
    def drawForeground(self, painter, rect):
        if (not self.mMapDocument or not self.mGridVisible):
            return
            
        offset = QPointF()

        # Take into account the offset of the current layer
        layer = self.mMapDocument.currentLayer()
        if layer:
            offset = layer.offset()
            painter.translate(offset)
        
        prefs = preferences.Preferences.instance()
        self.mMapDocument.renderer().drawGrid(painter, rect.translated(-offset), prefs.gridColor())

    ##
    # Override for handling enter and leave events.
    ##
    def event(self, event):
        x = event.type()
        if x==QEvent.Enter:
            self.mUnderMouse = True
            if (self.mActiveTool):
                self.mActiveTool.mouseEntered()
        elif x==QEvent.Leave:
            self.mUnderMouse = False
            if (self.mActiveTool):
                self.mActiveTool.mouseLeft()
        else:
            pass

        return super().event(event)

    def keyPressEvent(self, event):
        if (self.mActiveTool):
            self.mActiveTool.keyPressed(event)
        if (not (self.mActiveTool and event.isAccepted())):
            super().keyPressEvent(event)

    def mouseMoveEvent(self, mouseEvent):
        self.mLastMousePos = mouseEvent.scenePos()
        if (not self.mMapDocument):
            return
        super().mouseMoveEvent(mouseEvent)
        if (mouseEvent.isAccepted()):
            return
        if (self.mActiveTool):
            self.mActiveTool.mouseMoved(mouseEvent.scenePos(), mouseEvent.modifiers())
            mouseEvent.accept()

    def mousePressEvent(self, mouseEvent):
        super().mousePressEvent(mouseEvent)
        if (mouseEvent.isAccepted()):
            return
        if (self.mActiveTool):
            mouseEvent.accept()
            self.mActiveTool.mousePressed(mouseEvent)

    def mouseReleaseEvent(self, mouseEvent):
        super().mouseReleaseEvent(mouseEvent)
        if (mouseEvent.isAccepted()):
            return
        if (self.mActiveTool):
            mouseEvent.accept()
            self.mActiveTool.mouseReleased(mouseEvent)

    ##
    # Override to ignore drag enter events.
    ##
    def dragEnterEvent(self, event):
        event.ignore()

    ##
    # Sets whether the tile grid is visible.
    ##
    def setGridVisible(self, visible):
        if (self.mGridVisible == visible):
            return
        self.mGridVisible = visible
        self.update()

    def setObjectLineWidth(self, lineWidth):
        if (self.mObjectLineWidth == lineWidth):
            return
        self.mObjectLineWidth = lineWidth
        if (self.mMapDocument):
            self.mMapDocument.renderer().setObjectLineWidth(lineWidth)
            # Changing the line width can change the size of the object items
            if (not self.mObjectItems.isEmpty()):
                for item in self.mObjectItems:
                    item[1].syncWithMapObject()
                self.update()

    def setShowTileObjectOutlines(self, enabled):
        if (self.mShowTileObjectOutlines == enabled):
            return
        self.mShowTileObjectOutlines = enabled
        if (self.mMapDocument):
            self.mMapDocument.renderer().setFlag(RenderFlag.ShowTileObjectOutlines, enabled)
            if (not self.mObjectItems.isEmpty()):
                self.update()

    ##
    # Sets whether the current layer should be highlighted.
    ##
    def setHighlightCurrentLayer(self, highlightCurrentLayer):
        if (self.mHighlightCurrentLayer == highlightCurrentLayer):
            return
        self.mHighlightCurrentLayer = highlightCurrentLayer
        self.updateCurrentLayerHighlight()

    ##
    # Refreshes the map scene.
    ##
    def refreshScene(self):
        self.mLayerItems.clear()
        self.mObjectItems.clear()
        self.removeItem(self.mDarkRectangle)
        self.clear()
        self.addItem(self.mDarkRectangle)
        if (not self.mMapDocument):
            self.setSceneRect(QRectF())
            return

        self.updateSceneRect()
        
        map = self.mMapDocument.map()
        self.mLayerItems.resize(map.layerCount())
        if (map.backgroundColor().isValid()):
            self.setBackgroundBrush(map.backgroundColor())
        else:
            self.setBackgroundBrush(self.mDefaultBackgroundColor)
        layerIndex = 0
        for layer in map.layers():
            layerItem = self.createLayerItem(layer)
            layerItem.setZValue(layerIndex)
            self.addItem(layerItem)
            self.mLayerItems[layerIndex] = layerItem
            layerIndex += 1

        tileSelectionItem = TileSelectionItem(self.mMapDocument)
        tileSelectionItem.setZValue(10000 - 2)
        self.addItem(tileSelectionItem)
        self.mObjectSelectionItem = ObjectSelectionItem(self.mMapDocument)
        self.mObjectSelectionItem.setZValue(10000 - 1)
        self.addItem(self.mObjectSelectionItem)
        self.updateCurrentLayerHighlight()

    ##
    # Repaints the specified region. The region is in tile coordinates.
    ##
    def repaintRegion(self, region, layer):
        renderer = self.mMapDocument.renderer()
        margins = self.mMapDocument.map().drawMargins()
        for r in region.rects():
            boundingRect = QRectF(renderer.boundingRect(r))
            self.update(QRectF(renderer.boundingRect(r).adjusted(-margins.left(),
                                                      -margins.top(),
                                                      margins.right(),
                                                      margins.bottom())))
            boundingRect.translate(layer.offset())
            self.update(boundingRect)

    def currentLayerIndexChanged(self):
        self.updateCurrentLayerHighlight()
        # New layer may have a different offset, affecting the grid
        if self.mGridVisible:
            self.update()
        
    ##
    # Adapts the scene, layers and objects to new map size, orientation or
    # background color.
    ##
    def mapChanged(self):
        self.updateSceneRect()
        for item in self.mLayerItems:
            tli = item
            if type(tli) == TileLayerItem:
                tli.syncWithTileLayer()

        for item in self.mObjectItems.values():
            item.syncWithMapObject()
        map = self.mMapDocument.map()
        if (map.backgroundColor().isValid()):
            self.setBackgroundBrush(map.backgroundColor())
        else:
            self.setBackgroundBrush(self.mDefaultBackgroundColor)

    def tilesetChanged(self, tileset):
        if (not self.mMapDocument):
            return
        if (contains(self.mMapDocument.map().tilesets(), tileset)):
            self.update()

    def tileLayerDrawMarginsChanged(self, tileLayer):
        index = self.mMapDocument.map().layers().indexOf(tileLayer)
        item = self.mLayerItems.at(index)
        item.syncWithTileLayer()

    def layerAdded(self, index):
        layer = self.mMapDocument.map().layerAt(index)
        layerItem = self.createLayerItem(layer)
        self.addItem(layerItem)
        self.mLayerItems.insert(index, layerItem)
        z = 0
        for item in self.mLayerItems:
            item.setZValue(z)
            z += 1

    def layerRemoved(self, index):
        self.mLayerItems.remove(index)

    ##
    # A layer has changed. This can mean that the layer visibility, opacity or
    # offset changed.
    ##
    def layerChanged(self, index):
        layer = self.mMapDocument.map().layerAt(index)
        layerItem = self.mLayerItems.at(index)
        layerItem.setVisible(layer.isVisible())
        multiplier = 1
        if (self.mHighlightCurrentLayer and self.mMapDocument.currentLayerIndex() < index):
            multiplier = opacityFactor
        layerItem.setOpacity(layer.opacity() * multiplier)
        layerItem.setPos(layer.offset())

        # Layer offset may have changed, affecting the scene rect and grid
        self.updateSceneRect()
        if self.mGridVisible:
            self.update()

    ##
    # When an object group has changed it may mean its color or drawing order
    # changed, which affects all its objects.
    ##
    def objectGroupChanged(self, objectGroup):
        self.objectsChanged(objectGroup.objects())
        self.objectsIndexChanged(objectGroup, 0, objectGroup.objectCount() - 1)

    ##
    # When an image layer has changed, it may change size and it may look
    # differently.
    ##
    def imageLayerChanged(self, imageLayer):
        index = self.mMapDocument.map().layers().indexOf(imageLayer)
        item = self.mLayerItems.at(index)
        item.syncWithImageLayer()
        item.update()

    ##
    # When the tile offset of a tileset has changed, it can affect the bounding
    # rect of all tile layers and tile objects. It also requires a full repaint.
    ##
    def tilesetTileOffsetChanged(self, tileset):
        self.update()
        for item in self.mLayerItems:
            tli = item
            if type(tli) == TileLayerItem:
                tli.syncWithTileLayer()
        for item in self.mObjectItems:
            cell = item.mapObject().cell()
            if (not cell.isEmpty() and cell.tile.tileset() == tileset):
                item.syncWithMapObject()

    ##
    # Inserts map object items for the given objects.
    ##
    def objectsInserted(self, objectGroup, first, last):
        ogItem = None
        # Find the object group item for the object group
        for item in self.mLayerItems:
            ogi = item
            if type(ogi)==ObjectGroupItem:
                if (ogi.objectGroup() == objectGroup):
                    ogItem = ogi
                    break

        drawOrder = objectGroup.drawOrder()
        for i in range(first, last+1):
            object = objectGroup.objectAt(i)
            item = MapObjectItem(object, self.mMapDocument, ogItem)
            if (drawOrder == ObjectGroup.DrawOrder.TopDownOrder):
                item.setZValue(item.y())
            else:
                item.setZValue(i)
            self.mObjectItems.insert(object, item)

    ##
    # Removes the map object items related to the given objects.
    ##
    def objectsRemoved(self, objects):
        for o in objects:
            i = self.mObjectItems.find(o)
            self.mSelectedObjectItems.remove(i)
            # python would not force delete QGraphicsItem
            self.removeItem(i)
            self.mObjectItems.erase(o)

    ##
    # Updates the map object items related to the given objects.
    ##
    def objectsChanged(self, objects):
        for object in objects:
            item = self.itemForObject(object)
            item.syncWithMapObject()

    ##
    # Updates the Z value of the objects when appropriate.
    ##
    def objectsIndexChanged(self, objectGroup, first, last):
        if (objectGroup.drawOrder() != ObjectGroup.DrawOrder.IndexOrder):
            return
        for i in range(first, last+1):
            item = self.itemForObject(objectGroup.objectAt(i))
            item.setZValue(i)

    def updateSelectedObjectItems(self):
        objects = self.mMapDocument.selectedObjects()
        items = QSet()
        for object in objects:
            item = self.itemForObject(object)
            if item:
                items.insert(item)

        self.mSelectedObjectItems = items
        self.selectedObjectItemsChanged.emit()

    def syncAllObjectItems(self):
        for item in self.mObjectItems:
            item.syncWithMapObject()

    def createLayerItem(self, layer):
        layerItem = None
        tl = layer.asTileLayer()
        if tl:
            layerItem = TileLayerItem(tl, self.mMapDocument)
        else:
            og = layer.asObjectGroup()
            if og:
                drawOrder = og.drawOrder()
                ogItem = ObjectGroupItem(og)
                objectIndex = 0
                for object in og.objects():
                    item = MapObjectItem(object, self.mMapDocument, ogItem)
                    if (drawOrder == ObjectGroup.DrawOrder.TopDownOrder):
                        item.setZValue(item.y())
                    else:
                        item.setZValue(objectIndex)
                    self.mObjectItems.insert(object, item)
                    objectIndex += 1

                layerItem = ogItem
            else:
                il = layer.asImageLayer()
                if il:
                    layerItem = ImageLayerItem(il, self.mMapDocument)

        layerItem.setVisible(layer.isVisible())
        return layerItem

    def updateSceneRect(self):
        mapSize = self.mMapDocument.renderer().mapSize()
        sceneRect = QRectF(0, 0, mapSize.width(), mapSize.height())

        margins = self.mMapDocument.map().computeLayerOffsetMargins()
        sceneRect.adjust(-margins.left(),
                         -margins.top(),
                         margins.right(),
                         margins.bottom())

        self.setSceneRect(sceneRect)
        self.mDarkRectangle.setRect(sceneRect)
        
    def updateCurrentLayerHighlight(self):
        if (not self.mMapDocument):
            return
        currentLayerIndex = self.mMapDocument.currentLayerIndex()
        if (not self.mHighlightCurrentLayer or currentLayerIndex == -1):
            self.mDarkRectangle.setVisible(False)
            # Restore opacity for all layers
            for i in range(self.mLayerItems.size()):
                layer = self.mMapDocument.map().layerAt(i)
                self.mLayerItems.at(i).setOpacity(layer.opacity())

            return

        # Darken layers below the current layer
        self.mDarkRectangle.setZValue(currentLayerIndex - 0.5)
        self.mDarkRectangle.setVisible(True)
        # Set layers above the current layer to half opacity
        for i in range(1, self.mLayerItems.size()):
            layer = self.mMapDocument.map().layerAt(i)
            if currentLayerIndex < i:
                _x = opacityFactor
            else:
                _x = 1
            multiplier = _x
            self.mLayerItems.at(i).setOpacity(layer.opacity() * multiplier)

    def eventFilter(self, object, event):
        x = event.type()
        if x==QEvent.KeyPress or x==QEvent.KeyRelease:
                keyEvent = event
                newModifiers = keyEvent.modifiers()
                if (self.mActiveTool and newModifiers != self.mCurrentModifiers):
                    self.mActiveTool.modifiersChanged(newModifiers)
                    self.mCurrentModifiers = newModifiers
        else:
            pass

        return False
Exemple #28
0
class AutoMapper(QObject):
    ##
    # Constructs an AutoMapper.
    # All data structures, which only rely on the rules map are setup
    # here.
    #
    # @param workingDocument: the map to work on.
    # @param rules: The rule map which should be used for automapping
    # @param rulePath: The filepath to the rule map.
    ##
    def __init__(self, workingDocument, rules, rulePath):
        ##
        # where to work in
        ##
        self.mMapDocument = workingDocument

        ##
        # the same as mMapDocument.map()
        ##
        self.mMapWork = None
        if workingDocument:
            self.mMapWork = workingDocument.map()

        ##
        # map containing the rules, usually different than mMapWork
        ##
        self.mMapRules = rules

        ##
        # This contains all added tilesets as pointers.
        # if rules use Tilesets which are not in the mMapWork they are added.
        # keep track of them, because we need to delete them afterwards,
        # when they still are unused
        # they will be added while setupTilesets().
        ##
        self.mAddedTilesets = QVector()

        ##
        # description see: mAddedTilesets, just described by Strings
        ##
        self.mAddedTileLayers = QList()

        ##
        # Points to the tilelayer, which defines the inputregions.
        ##
        self.mLayerInputRegions = None

        ##
        # Points to the tilelayer, which defines the outputregions.
        ##
        self.mLayerOutputRegions = None

        ##
        # Contains all tilelayer pointers, which names begin with input*
        # It is sorted by index and name
        ##
        self.mInputRules = InputLayers()

        ##
        # List of Regions in mMapRules to know where the input rules are
        ##
        self.mRulesInput = QList()

        ##
        # List of regions in mMapRules to know where the output of a
        # rule is.
        # mRulesOutput[i] is the output of that rule,
        # which has the input at mRulesInput[i], meaning that mRulesInput
        # and mRulesOutput must match with the indexes.
        ##
        self.mRulesOutput = QList()

        ##
        # The inner set with layers to indexes is needed for translating
        # tile layers from mMapRules to mMapWork.
        #
        # The key is the pointer to the layer in the rulemap. The
        # pointer to the layer within the working map is not hardwired, but the
        # position in the layerlist, where it was found the last time.
        # This loosely bound pointer ensures we will get the right layer, since we
        # need to check before anyway, and it is still fast.
        #
        # The list is used to hold different translation tables
        # => one of the tables is chosen by chance, so randomness is available
        ##
        self.mLayerList = QList()
        ##
        # store the name of the processed rules file, to have detailed
        # error messages available
        ##
        self.mRulePath = rulePath

        ##
        # determines if all tiles in all touched layers should be deleted first.
        ##
        self.mDeleteTiles = False

        ##
        # This variable determines, how many overlapping tiles should be used.
        # The bigger the more area is remapped at an automapping operation.
        # This can lead to higher latency, but provides a better behavior on
        # interactive automapping.
        # It defaults to zero.
        ##
        self.mAutoMappingRadius = 0

        ##
        # Determines if a rule is allowed to overlap it
        ##
        self.mNoOverlappingRules = False

        self.mTouchedObjectGroups = QSet()
        self.mWarning = QString()
        self.mTouchedTileLayers = QSet()
        self.mError = ''

        if (not self.setupRuleMapProperties()):
            return
        if (not self.setupRuleMapTileLayers()):
            return
        if (not self.setupRuleList()):
            return

    def __del__(self):
        self.cleanUpRulesMap()

    ##
    # Checks if the passed \a ruleLayerName is used in this instance
    # of Automapper.
    ##
    def ruleLayerNameUsed(self, ruleLayerName):
        return self.mInputRules.names.contains(ruleLayerName)

    ##
    # Call prepareLoad first! Returns a set of strings describing the tile
    # layers, which could be touched considering the given layers of the
    # rule map.
    ##
    def getTouchedTileLayers(self):
        return self.mTouchedTileLayers

    ##
    # This needs to be called directly before the autoMap call.
    # It sets up some data structures which change rapidly, so it is quite
    # painful to keep these datastructures up to date all time. (indices of
    # layers of the working map)
    ##
    def prepareAutoMap(self):
        self.mError = ''
        self.mWarning = ''
        if (not self.setupMissingLayers()):
            return False
        if (not self.setupCorrectIndexes()):
            return False
        if (not self.setupTilesets(self.mMapRules, self.mMapWork)):
            return False
        return True

    ##
    # Here is done all the automapping.
    ##
    def autoMap(self, where):
        # first resize the active area
        if (self.mAutoMappingRadius):
            region = QRegion()
            for r in where.rects():
                region += r.adjusted(-self.mAutoMappingRadius,
                                     -self.mAutoMappingRadius,
                                     +self.mAutoMappingRadius,
                                     +self.mAutoMappingRadius)

        #where += region

        # delete all the relevant area, if the property "DeleteTiles" is set
        if (self.mDeleteTiles):
            setLayersRegion = self.getSetLayersRegion()
            for i in range(self.mLayerList.size()):
                translationTable = self.mLayerList.at(i)
                for layer in translationTable.keys():
                    index = self.mLayerList.at(i).value(layer)
                    dstLayer = self.mMapWork.layerAt(index)
                    region = setLayersRegion.intersected(where)
                    dstTileLayer = dstLayer.asTileLayer()
                    if (dstTileLayer):
                        dstTileLayer.erase(region)
                    else:
                        self.eraseRegionObjectGroup(self.mMapDocument,
                                                    dstLayer.asObjectGroup(),
                                                    region)

        # Increase the given region where the next automapper should work.
        # This needs to be done, so you can rely on the order of the rules at all
        # locations
        ret = QRegion()
        for rect in where.rects():
            for i in range(self.mRulesInput.size()):
                # at the moment the parallel execution does not work yet
                # TODO: make multithreading available!
                # either by dividing the rules or the region to multiple threads
                ret = ret.united(self.applyRule(i, rect))

        #where = where.united(ret)

    ##
    # This cleans all datastructures, which are setup via prepareAutoMap,
    # so the auto mapper becomes ready for its next automatic mapping.
    ##
    def cleanAll(self):
        self.cleanTilesets()
        self.cleanTileLayers()

    ##
    # Contains all errors until operation was canceled.
    # The errorlist is cleared within prepareLoad and prepareAutoMap.
    ##
    def errorString(self):
        return self.mError

    ##
    # Contains all warnings which occur at loading a rules map or while
    # automapping.
    # The errorlist is cleared within prepareLoad and prepareAutoMap.
    ##
    def warningString(self):
        return self.mWarning

    ##
    # Reads the map properties of the rulesmap.
    # @return returns True when anything is ok, False when errors occured.
    ##
    def setupRuleMapProperties(self):
        properties = self.mMapRules.properties()
        for key in properties.keys():
            value = properties.value(key)
            raiseWarning = True
            if (key.toLower() == "deletetiles"):
                if (value.canConvert(QVariant.Bool)):
                    self.mDeleteTiles = value.toBool()
                    raiseWarning = False
            elif (key.toLower() == "automappingradius"):
                if (value.canConvert(QVariant.Int)):
                    self.mAutoMappingRadius = value
                    raiseWarning = False
            elif (key.toLower() == "nooverlappingrules"):
                if (value.canConvert(QVariant.Bool)):
                    self.mNoOverlappingRules = value.toBool()
                    raiseWarning = False

            if (raiseWarning):
                self.mWarning += self.tr(
                    "'%s': Property '%s' = '%s' does not make sense. \nIgnoring this property."
                    % (self.mRulePath, key, value.toString()) + '\n')

        return True

    def cleanUpRulesMap(self):
        self.cleanTilesets()
        # mMapRules can be empty, when in prepareLoad the very first stages fail.
        if (not self.mMapRules):
            return
        tilesetManager = TilesetManager.instance()
        tilesetManager.removeReferences(self.mMapRules.tilesets())
        del self.mMapRules
        self.mMapRules = None
        self.cleanUpRuleMapLayers()
        self.mRulesInput.clear()
        self.mRulesOutput.clear()

    ##
    # Searches the rules layer for regions and stores these in \a rules.
    # @return returns True when anything is ok, False when errors occured.
    ##
    def setupRuleList(self):
        combinedRegions = coherentRegions(self.mLayerInputRegions.region() +
                                          self.mLayerOutputRegions.region())
        combinedRegions = QList(
            sorted(combinedRegions, key=lambda x: x.y(), reverse=True))
        rulesInput = coherentRegions(self.mLayerInputRegions.region())
        rulesOutput = coherentRegions(self.mLayerOutputRegions.region())
        for i in range(combinedRegions.size()):
            self.mRulesInput.append(QRegion())
            self.mRulesOutput.append(QRegion())

        for reg in rulesInput:
            for i in range(combinedRegions.size()):
                if (reg.intersects(combinedRegions[i])):
                    self.mRulesInput[i] += reg
                    break

        for reg in rulesOutput:
            for i in range(combinedRegions.size()):
                if (reg.intersects(combinedRegions[i])):
                    self.mRulesOutput[i] += reg
                    break

        for i in range(self.mRulesInput.size()):
            checkCoherent = self.mRulesInput.at(i).united(
                self.mRulesOutput.at(i))
            coherentRegions(checkCoherent).length() == 1
        return True

    ##
    # Sets up the layers in the rules map, which are used for automapping.
    # The layers are detected and put in the internal data structures
    # @return returns True when anything is ok, False when errors occured.
    ##
    def setupRuleMapTileLayers(self):
        error = QString()
        for layer in self.mMapRules.layers():
            layerName = layer.name()
            if (layerName.lower().startswith("regions")):
                treatAsBoth = layerName.toLower() == "regions"
                if (layerName.lower().endswith("input") or treatAsBoth):
                    if (self.mLayerInputRegions):
                        error += self.tr(
                            "'regions_input' layer must not occur more than once.\n"
                        )

                    if (layer.isTileLayer()):
                        self.mLayerInputRegions = layer.asTileLayer()
                    else:
                        error += self.tr(
                            "'regions_*' layers must be tile layers.\n")

                if (layerName.lower().endswith("output") or treatAsBoth):
                    if (self.mLayerOutputRegions):
                        error += self.tr(
                            "'regions_output' layer must not occur more than once.\n"
                        )

                    if (layer.isTileLayer()):
                        self.mLayerOutputRegions = layer.asTileLayer()
                    else:
                        error += self.tr(
                            "'regions_*' layers must be tile layers.\n")

                continue

            nameStartPosition = layerName.indexOf('_') + 1
            # name is all characters behind the underscore (excluded)
            name = layerName.right(layerName.size() - nameStartPosition)
            # group is all before the underscore (included)
            index = layerName.left(nameStartPosition)
            if (index.lower().startswith("output")):
                index.remove(0, 6)
            elif (index.lower().startswith("inputnot")):
                index.remove(0, 8)
            elif (index.lower().startswith("input")):
                index.remove(0, 5)
            # both 'rule' and 'output' layers will require and underscore and
            # rely on the correct position detected of the underscore
            if (nameStartPosition == 0):
                error += self.tr(
                    "Did you forget an underscore in layer '%d'?\n" %
                    layerName)
                continue

            if (layerName.startsWith("input", Qt.CaseInsensitive)):
                isNotList = layerName.lower().startswith("inputnot")
                if (not layer.isTileLayer()):
                    error += self.tr(
                        "'input_*' and 'inputnot_*' layers must be tile layers.\n"
                    )
                    continue

                self.mInputRules.names.insert(name)
                if (not self.mInputRules.indexes.contains(index)):
                    self.mInputRules.indexes.insert(index)
                    self.mInputRules.insert(index, InputIndex())

                if (not self.mInputRules[index].names.contains(name)):
                    self.mInputRules[index].names.insert(name)
                    self.mInputRules[index].insert(name, InputIndexName())

                if (isNotList):
                    self.mInputRules[index][name].listNo.append(
                        layer.asTileLayer())
                else:
                    self.mInputRules[index][name].listYes.append(
                        layer.asTileLayer())
                continue

            if layerName.lower().startswith("output"):
                if (layer.isTileLayer()):
                    self.mTouchedTileLayers.insert(name)
                else:
                    self.mTouchedObjectGroups.insert(name)
                type = layer.layerType()
                layerIndex = self.mMapWork.indexOfLayer(name, type)
                found = False
                for translationTable in self.mLayerList:
                    if (translationTable.index == index):
                        translationTable.insert(layer, layerIndex)
                        found = True
                        break

                if (not found):
                    self.mLayerList.append(RuleOutput())
                    self.mLayerList.last().insert(layer, layerIndex)
                    self.mLayerList.last().index = index

                continue

            error += self.tr(
                "Layer '%s' is not recognized as a valid layer for Automapping.\n"
                % layerName)

        if (not self.mLayerInputRegions):
            error += self.tr("No 'regions' or 'regions_input' layer found.\n")
        if (not self.mLayerOutputRegions):
            error += self.tr("No 'regions' or 'regions_output' layer found.\n")
        if (self.mInputRules.isEmpty()):
            error += self.tr("No input_<name> layer found!\n")
        # no need to check for mInputNotRules.size() == 0 here.
        # these layers are not necessary.
        if error != '':
            error = self.mRulePath + '\n' + error
            self.mError += error
            return False

        return True

    ##
    # Checks if all needed layers in the working map are there.
    # If not, add them in the correct order.
    ##
    def setupMissingLayers(self):
        # make sure all needed layers are there:
        for name in self.mTouchedTileLayers:
            if (self.mMapWork.indexOfLayer(name, Layer.TileLayerType) != -1):
                continue
            index = self.mMapWork.layerCount()
            tilelayer = TileLayer(name, 0, 0, self.mMapWork.width(),
                                  self.mMapWork.height())
            self.mMapDocument.undoStack().push(
                AddLayer(self.mMapDocument, index, tilelayer))
            self.mAddedTileLayers.append(name)

        for name in self.mTouchedObjectGroups:
            if (self.mMapWork.indexOfLayer(name, Layer.ObjectGroupType) != -1):
                continue
            index = self.mMapWork.layerCount()
            objectGroup = ObjectGroup(name, 0, 0, self.mMapWork.width(),
                                      self.mMapWork.height())
            self.mMapDocument.undoStack().push(
                AddLayer(self.mMapDocument, index, objectGroup))
            self.mAddedTileLayers.append(name)

        return True

    ##
    # Checks if the layers setup as in setupRuleMapLayers are still right.
    # If it's not right, correct them.
    # @return returns True if everything went fine. False is returned when
    #         no set layer was found
    ##
    def setupCorrectIndexes(self):
        # make sure all indexes of the layer translationtables are correct.
        for i in range(self.mLayerList.size()):
            translationTable = self.mLayerList.at(i)
            for layerKey in translationTable.keys():
                name = layerKey.name()
                pos = name.indexOf('_') + 1
                name = name.right(name.length() - pos)
                index = translationTable.value(layerKey, -1)
                if (index >= self.mMapWork.layerCount() or index == -1
                        or name != self.mMapWork.layerAt(index).name()):
                    newIndex = self.mMapWork.indexOfLayer(
                        name, layerKey.layerType())
                    translationTable.insert(layerKey, newIndex)

        return True

    ##
    # sets up the tilesets which are used in automapping.
    # @return returns True when anything is ok, False when errors occured.
    #        (in that case will be a msg box anyway)
    ##
    # This cannot just be replaced by MapDocument::unifyTileset(Map),
    # because here mAddedTileset is modified.
    def setupTilesets(self, src, dst):
        existingTilesets = dst.tilesets()
        tilesetManager = TilesetManager.instance()
        # Add tilesets that are not yet part of dst map
        for tileset in src.tilesets():
            if (existingTilesets.contains(tileset)):
                continue
            undoStack = self.mMapDocument.undoStack()
            replacement = tileset.findSimilarTileset(existingTilesets)
            if (not replacement):
                self.mAddedTilesets.append(tileset)
                undoStack.push(AddTileset(self.mMapDocument, 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())
                undoStack.push(
                    ChangeProperties(self.mMapDocument, self.tr("Tile"),
                                     replacementTile, properties))

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

        return True

    ##
    # Returns the conjunction of of all regions of all setlayers
    ##
    def getSetLayersRegion(self):
        result = QRegion()
        for name in self.mInputRules.names:
            index = self.mMapWork.indexOfLayer(name, Layer.TileLayerType)
            if (index == -1):
                continue
            setLayer = self.mMapWork.layerAt(index).asTileLayer()
            result |= setLayer.region()

        return result

    ##
    # This copies all Tiles from TileLayer src to TileLayer dst
    #
    # In src the Tiles are taken from the rectangle given by
    # src_x, src_y, width and height.
    # In dst they get copied to a rectangle given by
    # dst_x, dst_y, width, height .
    # if there is no tile in src TileLayer, there will nothing be copied,
    # so the maybe existing tile in dst will not be overwritten.
    #
    ##
    def copyTileRegion(self, srcLayer, srcX, srcY, width, height, dstLayer,
                       dstX, dstY):
        startX = max(dstX, 0)
        startY = max(dstY, 0)
        endX = min(dstX + width, dstLayer.width())
        endY = min(dstY + height, dstLayer.height())
        offsetX = srcX - dstX
        offsetY = srcY - dstY
        for x in range(startX, endX):
            for y in range(startY, endY):
                cell = srcLayer.cellAt(x + offsetX, y + offsetY)
                if (not cell.isEmpty()):
                    # this is without graphics update, it's done afterwards for all
                    dstLayer.setCell(x, y, cell)

    ##
    # This copies all objects from the \a src_lr ObjectGroup to the \a dst_lr
    # in the given rectangle.
    #
    # The rectangle is described by the upper left corner \a src_x \a src_y
    # and its \a width and \a height. The parameter \a dst_x and \a dst_y
    # offset the copied objects in the destination object group.
    ##
    def copyObjectRegion(self, srcLayer, srcX, srcY, width, height, dstLayer,
                         dstX, dstY):
        undo = self.mMapDocument.undoStack()
        rect = QRectF(srcX, srcY, width, height)
        pixelRect = self.mMapDocument.renderer().tileToPixelCoords_(rect)
        objects = objectsInRegion(srcLayer, pixelRect.toAlignedRect())
        pixelOffset = self.mMapDocument.renderer().tileToPixelCoords(
            dstX, dstY)
        pixelOffset -= pixelRect.topLeft()
        clones = QList()
        for obj in objects:
            clone = obj.clone()
            clones.append(clone)
            clone.setX(clone.x() + pixelOffset.x())
            clone.setY(clone.y() + pixelOffset.y())
            undo.push(AddMapObject(self.mMapDocument, dstLayer, clone))

    ##
    # This copies multiple TileLayers from one map to another.
    # Only the region \a region is considered for copying.
    # In the destination it will come to the region translated by Offset.
    # The parameter \a LayerTranslation is a map of which layers of the rulesmap
    # should get copied into which layers of the working map.
    ##
    def copyMapRegion(self, region, offset, layerTranslation):
        for i in range(layerTranslation.keys().size()):
            _from = layerTranslation.keys().at(i)
            to = self.mMapWork.layerAt(layerTranslation.value(_from))
            for rect in region.rects():
                fromTileLayer = _from.asTileLayer()
                fromObjectGroup = _from.asObjectGroup()
                if (fromTileLayer):
                    toTileLayer = to.asTileLayer()
                    self.copyTileRegion(fromTileLayer, rect.x(), rect.y(),
                                        rect.width(), rect.height(),
                                        toTileLayer,
                                        rect.x() + offset.x(),
                                        rect.y() + offset.y())
                elif (fromObjectGroup):
                    toObjectGroup = to.asObjectGroup()
                    self.copyObjectRegion(fromObjectGroup, rect.x(), rect.y(),
                                          rect.width(), rect.height(),
                                          toObjectGroup,
                                          rect.x() + offset.x(),
                                          rect.y() + offset.y())
                else:
                    pass

    ##
    # This goes through all the positions of the mMapWork and checks if
    # there fits the rule given by the region in mMapRuleSet.
    # if there is a match all Layers are copied to mMapWork.
    # @param ruleIndex: the region which should be compared to all positions
    #              of mMapWork will be looked up in mRulesInput and mRulesOutput
    # @return where: an rectangle where the rule actually got applied
    ##
    def applyRule(self, ruleIndex, where):
        ret = QRect()
        if (self.mLayerList.isEmpty()):
            return ret
        ruleInput = self.mRulesInput.at(ruleIndex)
        ruleOutput = self.mRulesOutput.at(ruleIndex)
        rbr = ruleInput.boundingRect()
        # Since the rule itself is translated, we need to adjust the borders of the
        # loops. Decrease the size at all sides by one: There must be at least one
        # tile overlap to the rule.
        minX = where.left() - rbr.left() - rbr.width() + 1
        minY = where.top() - rbr.top() - rbr.height() + 1
        maxX = where.right() - rbr.left() + rbr.width() - 1
        maxY = where.bottom() - rbr.top() + rbr.height() - 1
        # In this list of regions it is stored which parts or the map have already
        # been altered by exactly this rule. We store all the altered parts to
        # make sure there are no overlaps of the same rule applied to
        # (neighbouring) places
        appliedRegions = QList()
        if (self.mNoOverlappingRules):
            for i in range(self.mMapWork.layerCount()):
                appliedRegions.append(QRegion())
        for y in range(minY, maxY + 1):
            for x in range(minX, maxX + 1):
                anymatch = False
                for index in self.mInputRules.indexes:
                    ii = self.mInputRules[index]
                    allLayerNamesMatch = True
                    for name in ii.names:
                        i = self.mMapWork.indexOfLayer(name,
                                                       Layer.TileLayerType)
                        if (i == -1):
                            allLayerNamesMatch = False
                        else:
                            setLayer = self.mMapWork.layerAt(i).asTileLayer()
                            allLayerNamesMatch &= compareLayerTo(
                                setLayer, ii[name].listYes, ii[name].listNo,
                                ruleInput, QPoint(x, y))

                    if (allLayerNamesMatch):
                        anymatch = True
                        break

                if (anymatch):
                    r = 0
                    # choose by chance which group of rule_layers should be used:
                    if (self.mLayerList.size() > 1):
                        r = qrand() % self.mLayerList.size()
                    if (not self.mNoOverlappingRules):
                        self.copyMapRegion(ruleOutput, QPoint(x, y),
                                           self.mLayerList.at(r))
                        ret = ret.united(rbr.translated(QPoint(x, y)))
                        continue

                    missmatch = False
                    translationTable = self.mLayerList.at(r)
                    layers = translationTable.keys()
                    # check if there are no overlaps within this rule.
                    ruleRegionInLayer = QVector()
                    for i in range(layers.size()):
                        layer = layers.at(i)
                        appliedPlace = QRegion()
                        tileLayer = layer.asTileLayer()
                        if (tileLayer):
                            appliedPlace = tileLayer.region()
                        else:
                            appliedPlace = tileRegionOfObjectGroup(
                                layer.asObjectGroup())
                        ruleRegionInLayer.append(
                            appliedPlace.intersected(ruleOutput))
                        if (appliedRegions.at(i).intersects(
                                ruleRegionInLayer[i].translated(x, y))):
                            missmatch = True
                            break

                    if (missmatch):
                        continue
                    self.copyMapRegion(ruleOutput, QPoint(x, y),
                                       self.mLayerList.at(r))
                    ret = ret.united(rbr.translated(QPoint(x, y)))
                    for i in range(translationTable.size()):
                        appliedRegions[i] += ruleRegionInLayer[i].translated(
                            x, y)

        return ret

    ##
    # Cleans up the data structes filled by setupRuleMapLayers(),
    # so the next rule can be processed.
    ##
    def cleanUpRuleMapLayers(self):
        self.cleanTileLayers()
        it = QList.const_iterator()
        for it in self.mLayerList:
            del it
        self.mLayerList.clear()
        # do not delete mLayerRuleRegions, it is owned by the rulesmap
        self.mLayerInputRegions = None
        self.mLayerOutputRegions = None
        self.mInputRules.clear()

    ##
    # Cleans up the data structes filled by setupTilesets(),
    # so the next rule can be processed.
    ##
    def cleanTilesets(self):
        for tileset in self.mAddedTilesets:
            if (self.mMapWork.isTilesetUsed(tileset)):
                continue
            index = self.mMapWork.indexOfTileset(tileset)
            if (index == -1):
                continue
            undo = self.mMapDocument.undoStack()
            undo.push(RemoveTileset(self.mMapDocument, index))

        self.mAddedTilesets.clear()

    ##
    # Cleans up the added tile layers setup by setupMissingLayers(),
    # so we have a minimal addition of tile layers by the automapping.
    ##
    def cleanTileLayers(self):
        for tilelayerName in self.mAddedTileLayers:
            layerIndex = self.mMapWork.indexOfLayer(tilelayerName,
                                                    Layer.TileLayerType)
            if (layerIndex == -1):
                continue
            layer = self.mMapWork.layerAt(layerIndex)
            if (not layer.isEmpty()):
                continue
            undo = self.mMapDocument.undoStack()
            undo.push(RemoveLayer(self.mMapDocument, layerIndex))

        self.mAddedTileLayers.clear()
Exemple #29
0
class AutoMapper(QObject):
    ##
    # Constructs an AutoMapper.
    # All data structures, which only rely on the rules map are setup
    # here.
    #
    # @param workingDocument: the map to work on.
    # @param rules: The rule map which should be used for automapping
    # @param rulePath: The filepath to the rule map.
    ##
    def __init__(self, workingDocument, rules, rulePath):
        ##
        # where to work in
        ##
        self.mMapDocument = workingDocument

        ##
        # the same as mMapDocument.map()
        ##
        self.mMapWork = None
        if workingDocument:
            self.mMapWork = workingDocument.map()

        ##
        # map containing the rules, usually different than mMapWork
        ##
        self.mMapRules = rules

        ##
        # This contains all added tilesets as pointers.
        # if rules use Tilesets which are not in the mMapWork they are added.
        # keep track of them, because we need to delete them afterwards,
        # when they still are unused
        # they will be added while setupTilesets().
        ##
        self.mAddedTilesets = QVector()

        ##
        # description see: mAddedTilesets, just described by Strings
        ##
        self.mAddedTileLayers = QList()

        ##
        # Points to the tilelayer, which defines the inputregions.
        ##
        self.mLayerInputRegions = None

        ##
        # Points to the tilelayer, which defines the outputregions.
        ##
        self.mLayerOutputRegions = None

        ##
        # Contains all tilelayer pointers, which names begin with input*
        # It is sorted by index and name
        ##
        self.mInputRules = InputLayers()

        ##
        # List of Regions in mMapRules to know where the input rules are
        ##
        self.mRulesInput = QList()

        ##
        # List of regions in mMapRules to know where the output of a
        # rule is.
        # mRulesOutput[i] is the output of that rule,
        # which has the input at mRulesInput[i], meaning that mRulesInput
        # and mRulesOutput must match with the indexes.
        ##
        self.mRulesOutput = QList()

        ##
        # The inner set with layers to indexes is needed for translating
        # tile layers from mMapRules to mMapWork.
        #
        # The key is the pointer to the layer in the rulemap. The
        # pointer to the layer within the working map is not hardwired, but the
        # position in the layerlist, where it was found the last time.
        # This loosely bound pointer ensures we will get the right layer, since we
        # need to check before anyway, and it is still fast.
        #
        # The list is used to hold different translation tables
        # => one of the tables is chosen by chance, so randomness is available
        ##
        self.mLayerList = QList()
        ##
        # store the name of the processed rules file, to have detailed
        # error messages available
        ##
        self.mRulePath = rulePath

        ##
        # determines if all tiles in all touched layers should be deleted first.
        ##
        self.mDeleteTiles = False

        ##
        # This variable determines, how many overlapping tiles should be used.
        # The bigger the more area is remapped at an automapping operation.
        # This can lead to higher latency, but provides a better behavior on
        # interactive automapping.
        # It defaults to zero.
        ##
        self.mAutoMappingRadius = 0

        ##
        # Determines if a rule is allowed to overlap it
        ##
        self.mNoOverlappingRules = False

        self.mTouchedObjectGroups = QSet()
        self.mWarning = QString()
        self.mTouchedTileLayers = QSet()
        self.mError = ''

        if (not self.setupRuleMapProperties()):
            return
        if (not self.setupRuleMapTileLayers()):
            return
        if (not self.setupRuleList()):
            return

    def __del__(self):
        self.cleanUpRulesMap()

    ##
    # Checks if the passed \a ruleLayerName is used in this instance
    # of Automapper.
    ##
    def ruleLayerNameUsed(self, ruleLayerName):
        return self.mInputRules.names.contains(ruleLayerName)

    ##
    # Call prepareLoad first! Returns a set of strings describing the tile
    # layers, which could be touched considering the given layers of the
    # rule map.
    ##
    def getTouchedTileLayers(self):
        return self.mTouchedTileLayers

    ##
    # This needs to be called directly before the autoMap call.
    # It sets up some data structures which change rapidly, so it is quite
    # painful to keep these datastructures up to date all time. (indices of
    # layers of the working map)
    ##
    def prepareAutoMap(self):
        self.mError = ''
        self.mWarning = ''
        if (not self.setupMissingLayers()):
            return False
        if (not self.setupCorrectIndexes()):
            return False
        if (not self.setupTilesets(self.mMapRules, self.mMapWork)):
            return False
        return True

    ##
    # Here is done all the automapping.
    ##
    def autoMap(self, where):
        # first resize the active area
        if (self.mAutoMappingRadius):
            region = QRegion()
            for r in where.rects():
                region += r.adjusted(- self.mAutoMappingRadius,
                                     - self.mAutoMappingRadius,
                                     + self.mAutoMappingRadius,
                                     + self.mAutoMappingRadius)

           #where += region

        # delete all the relevant area, if the property "DeleteTiles" is set
        if (self.mDeleteTiles):
            setLayersRegion = self.getSetLayersRegion()
            for i in range(self.mLayerList.size()):
                translationTable = self.mLayerList.at(i)
                for layer in translationTable.keys():
                    index = self.mLayerList.at(i).value(layer)
                    dstLayer = self.mMapWork.layerAt(index)
                    region = setLayersRegion.intersected(where)
                    dstTileLayer = dstLayer.asTileLayer()
                    if (dstTileLayer):
                        dstTileLayer.erase(region)
                    else:
                        self.eraseRegionObjectGroup(self.mMapDocument,
                                               dstLayer.asObjectGroup(),
                                               region)

        # Increase the given region where the next automapper should work.
        # This needs to be done, so you can rely on the order of the rules at all
        # locations
        ret = QRegion()
        for rect in where.rects():
            for i in range(self.mRulesInput.size()):
                # at the moment the parallel execution does not work yet
                # TODO: make multithreading available!
                # either by dividing the rules or the region to multiple threads
                ret = ret.united(self.applyRule(i, rect))

        #where = where.united(ret)

    ##
    # This cleans all datastructures, which are setup via prepareAutoMap,
    # so the auto mapper becomes ready for its next automatic mapping.
    ##
    def cleanAll(self):
        self.cleanTilesets()
        self.cleanTileLayers()

    ##
    # Contains all errors until operation was canceled.
    # The errorlist is cleared within prepareLoad and prepareAutoMap.
    ##
    def errorString(self):
        return self.mError

    ##
    # Contains all warnings which occur at loading a rules map or while
    # automapping.
    # The errorlist is cleared within prepareLoad and prepareAutoMap.
    ##
    def warningString(self):
        return self.mWarning

    ##
    # Reads the map properties of the rulesmap.
    # @return returns True when anything is ok, False when errors occured.
    ##
    def setupRuleMapProperties(self):
        properties = self.mMapRules.properties()
        for key in properties.keys():
            value = properties.value(key)
            raiseWarning = True
            if (key.toLower() == "deletetiles"):
                if (value.canConvert(QVariant.Bool)):
                    self.mDeleteTiles = value.toBool()
                    raiseWarning = False
            elif (key.toLower() == "automappingradius"):
                if (value.canConvert(QVariant.Int)):
                    self.mAutoMappingRadius = value
                    raiseWarning = False
            elif (key.toLower() == "nooverlappingrules"):
                if (value.canConvert(QVariant.Bool)):
                    self.mNoOverlappingRules = value.toBool()
                    raiseWarning = False

            if (raiseWarning):
                self.mWarning += self.tr("'%s': Property '%s' = '%s' does not make sense. \nIgnoring this property."%(self.mRulePath, key, value.toString()) + '\n')

        return True

    def cleanUpRulesMap(self):
        self.cleanTilesets()
        # mMapRules can be empty, when in prepareLoad the very first stages fail.
        if (not self.mMapRules):
            return
        tilesetManager = TilesetManager.instance()
        tilesetManager.removeReferences(self.mMapRules.tilesets())
        del self.mMapRules
        self.mMapRules = None
        self.cleanUpRuleMapLayers()
        self.mRulesInput.clear()
        self.mRulesOutput.clear()

    ##
    # Searches the rules layer for regions and stores these in \a rules.
    # @return returns True when anything is ok, False when errors occured.
    ##
    def setupRuleList(self):
        combinedRegions = coherentRegions(
                self.mLayerInputRegions.region() +
                self.mLayerOutputRegions.region())
        combinedRegions = QList(sorted(combinedRegions, key=lambda x:x.y(), reverse=True))
        rulesInput = coherentRegions(
                self.mLayerInputRegions.region())
        rulesOutput = coherentRegions(
                self.mLayerOutputRegions.region())
        for i in range(combinedRegions.size()):
            self.mRulesInput.append(QRegion())
            self.mRulesOutput.append(QRegion())

        for reg in rulesInput:
            for i in range(combinedRegions.size()):
                if (reg.intersects(combinedRegions[i])):
                    self.mRulesInput[i] += reg
                    break

        for reg in rulesOutput:
            for i in range(combinedRegions.size()):
                if (reg.intersects(combinedRegions[i])):
                    self.mRulesOutput[i] += reg
                    break

        for i in range(self.mRulesInput.size()):
            checkCoherent = self.mRulesInput.at(i).united(self.mRulesOutput.at(i))
            coherentRegions(checkCoherent).length() == 1
        return True

    ##
    # Sets up the layers in the rules map, which are used for automapping.
    # The layers are detected and put in the internal data structures
    # @return returns True when anything is ok, False when errors occured.
    ##
    def setupRuleMapTileLayers(self):
        error = QString()
        for layer in self.mMapRules.layers():
            layerName = layer.name()
            if (layerName.lower().startswith("regions")):
                treatAsBoth = layerName.toLower() == "regions"
                if (layerName.lower().endswith("input") or treatAsBoth):
                    if (self.mLayerInputRegions):
                        error += self.tr("'regions_input' layer must not occur more than once.\n")

                    if (layer.isTileLayer()):
                        self.mLayerInputRegions = layer.asTileLayer()
                    else:
                        error += self.tr("'regions_*' layers must be tile layers.\n")

                if (layerName.lower().endswith("output") or treatAsBoth):
                    if (self.mLayerOutputRegions):
                        error += self.tr("'regions_output' layer must not occur more than once.\n")

                    if (layer.isTileLayer()):
                        self.mLayerOutputRegions = layer.asTileLayer()
                    else:
                        error += self.tr("'regions_*' layers must be tile layers.\n")

                continue

            nameStartPosition = layerName.indexOf('_') + 1
            # name is all characters behind the underscore (excluded)
            name = layerName.right(layerName.size() - nameStartPosition)
            # group is all before the underscore (included)
            index = layerName.left(nameStartPosition)
            if (index.lower().startswith("output")):
                index.remove(0, 6)
            elif (index.lower().startswith("inputnot")):
                index.remove(0, 8)
            elif (index.lower().startswith("input")):
                index.remove(0, 5)
            # both 'rule' and 'output' layers will require and underscore and 
            # rely on the correct position detected of the underscore
            if (nameStartPosition == 0):
                error += self.tr("Did you forget an underscore in layer '%d'?\n"%layerName)
                continue

            if (layerName.startsWith("input", Qt.CaseInsensitive)):
                isNotList = layerName.lower().startswith("inputnot")
                if (not layer.isTileLayer()):
                    error += self.tr("'input_*' and 'inputnot_*' layers must be tile layers.\n")
                    continue

                self.mInputRules.names.insert(name)
                if (not self.mInputRules.indexes.contains(index)):
                    self.mInputRules.indexes.insert(index)
                    self.mInputRules.insert(index, InputIndex())

                if (not self.mInputRules[index].names.contains(name)):
                    self.mInputRules[index].names.insert(name)
                    self.mInputRules[index].insert(name, InputIndexName())

                if (isNotList):
                    self.mInputRules[index][name].listNo.append(layer.asTileLayer())
                else:
                    self.mInputRules[index][name].listYes.append(layer.asTileLayer())
                continue

            if layerName.lower().startswith("output"):
                if (layer.isTileLayer()):
                    self.mTouchedTileLayers.insert(name)
                else:
                    self.mTouchedObjectGroups.insert(name)
                type = layer.layerType()
                layerIndex = self.mMapWork.indexOfLayer(name, type)
                found = False
                for translationTable in self.mLayerList:
                    if (translationTable.index == index):
                        translationTable.insert(layer, layerIndex)
                        found = True
                        break

                if (not found):
                    self.mLayerList.append(RuleOutput())
                    self.mLayerList.last().insert(layer, layerIndex)
                    self.mLayerList.last().index = index

                continue

            error += self.tr("Layer '%s' is not recognized as a valid layer for Automapping.\n"%layerName)

        if (not self.mLayerInputRegions):
            error += self.tr("No 'regions' or 'regions_input' layer found.\n")
        if (not self.mLayerOutputRegions):
            error += self.tr("No 'regions' or 'regions_output' layer found.\n")
        if (self.mInputRules.isEmpty()):
            error += self.tr("No input_<name> layer found!\n")
        # no need to check for mInputNotRules.size() == 0 here.
        # these layers are not necessary.
        if error != '':
            error = self.mRulePath + '\n' + error
            self.mError += error
            return False

        return True

    ##
    # Checks if all needed layers in the working map are there.
    # If not, add them in the correct order.
    ##
    def setupMissingLayers(self):
        # make sure all needed layers are there:
        for name in self.mTouchedTileLayers:
            if (self.mMapWork.indexOfLayer(name, Layer.TileLayerType) != -1):
                continue
            index = self.mMapWork.layerCount()
            tilelayer = TileLayer(name, 0, 0, self.mMapWork.width(), self.mMapWork.height())
            self.mMapDocument.undoStack().push(AddLayer(self.mMapDocument, index, tilelayer))
            self.mAddedTileLayers.append(name)

        for name in self.mTouchedObjectGroups:
            if (self.mMapWork.indexOfLayer(name, Layer.ObjectGroupType) != -1):
                continue
            index = self.mMapWork.layerCount()
            objectGroup = ObjectGroup(name, 0, 0,
                                                       self.mMapWork.width(),
                                                       self.mMapWork.height())
            self.mMapDocument.undoStack().push(AddLayer(self.mMapDocument, index, objectGroup))
            self.mAddedTileLayers.append(name)

        return True

    ##
    # Checks if the layers setup as in setupRuleMapLayers are still right.
    # If it's not right, correct them.
    # @return returns True if everything went fine. False is returned when
    #         no set layer was found
    ##
    def setupCorrectIndexes(self):
        # make sure all indexes of the layer translationtables are correct.
        for i in range(self.mLayerList.size()):
            translationTable = self.mLayerList.at(i)
            for layerKey in translationTable.keys():
                name = layerKey.name()
                pos = name.indexOf('_') + 1
                name = name.right(name.length() - pos)
                index = translationTable.value(layerKey, -1)
                if (index >= self.mMapWork.layerCount() or index == -1 or
                        name != self.mMapWork.layerAt(index).name()):
                    newIndex = self.mMapWork.indexOfLayer(name, layerKey.layerType())
                    translationTable.insert(layerKey, newIndex)

        return True

    ##
    # sets up the tilesets which are used in automapping.
    # @return returns True when anything is ok, False when errors occured.
    #        (in that case will be a msg box anyway)
    ##
    # This cannot just be replaced by MapDocument::unifyTileset(Map),
    # because here mAddedTileset is modified.
    def setupTilesets(self, src, dst):
        existingTilesets = dst.tilesets()
        tilesetManager = TilesetManager.instance()
        # Add tilesets that are not yet part of dst map
        for tileset in src.tilesets():
            if (existingTilesets.contains(tileset)):
                continue
            undoStack = self.mMapDocument.undoStack()
            replacement = tileset.findSimilarTileset(existingTilesets)
            if (not replacement):
                self.mAddedTilesets.append(tileset)
                undoStack.push(AddTileset(self.mMapDocument, 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())
                undoStack.push(ChangeProperties(self.mMapDocument,
                                                     self.tr("Tile"),
                                                     replacementTile,
                                                     properties))

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

        return True

    ##
    # Returns the conjunction of of all regions of all setlayers
    ##
    def getSetLayersRegion(self):
        result = QRegion()
        for name in self.mInputRules.names:
            index = self.mMapWork.indexOfLayer(name, Layer.TileLayerType)
            if (index == -1):
                continue
            setLayer = self.mMapWork.layerAt(index).asTileLayer()
            result |= setLayer.region()

        return result

    ##
    # This copies all Tiles from TileLayer src to TileLayer dst
    #
    # In src the Tiles are taken from the rectangle given by
    # src_x, src_y, width and height.
    # In dst they get copied to a rectangle given by
    # dst_x, dst_y, width, height .
    # if there is no tile in src TileLayer, there will nothing be copied,
    # so the maybe existing tile in dst will not be overwritten.
    #
    ##
    def copyTileRegion(self, srcLayer, srcX, srcY, width, height, dstLayer, dstX, dstY):
        startX = max(dstX, 0)
        startY = max(dstY, 0)
        endX = min(dstX + width, dstLayer.width())
        endY = min(dstY + height, dstLayer.height())
        offsetX = srcX - dstX
        offsetY = srcY - dstY
        for x in range(startX, endX):
            for y in range(startY, endY):
                cell = srcLayer.cellAt(x + offsetX, y + offsetY)
                if (not cell.isEmpty()):
                    # this is without graphics update, it's done afterwards for all
                    dstLayer.setCell(x, y, cell)

    ##
    # This copies all objects from the \a src_lr ObjectGroup to the \a dst_lr
    # in the given rectangle.
    #
    # The rectangle is described by the upper left corner \a src_x \a src_y
    # and its \a width and \a height. The parameter \a dst_x and \a dst_y
    # offset the copied objects in the destination object group.
    ##
    def copyObjectRegion(self, srcLayer, srcX, srcY, width, height, dstLayer, dstX, dstY):
        undo = self.mMapDocument.undoStack()
        rect = QRectF(srcX, srcY, width, height)
        pixelRect = self.mMapDocument.renderer().tileToPixelCoords_(rect)
        objects = objectsInRegion(srcLayer, pixelRect.toAlignedRect())
        pixelOffset = self.mMapDocument.renderer().tileToPixelCoords(dstX, dstY)
        pixelOffset -= pixelRect.topLeft()
        clones = QList()
        for obj in objects:
            clone = obj.clone()
            clones.append(clone)
            clone.setX(clone.x() + pixelOffset.x())
            clone.setY(clone.y() + pixelOffset.y())
            undo.push(AddMapObject(self.mMapDocument, dstLayer, clone))

    ##
    # This copies multiple TileLayers from one map to another.
    # Only the region \a region is considered for copying.
    # In the destination it will come to the region translated by Offset.
    # The parameter \a LayerTranslation is a map of which layers of the rulesmap
    # should get copied into which layers of the working map.
    ##
    def copyMapRegion(self, region, offset, layerTranslation):
        for i in range(layerTranslation.keys().size()):
            _from = layerTranslation.keys().at(i)
            to = self.mMapWork.layerAt(layerTranslation.value(_from))
            for rect in region.rects():
                fromTileLayer = _from.asTileLayer()
                fromObjectGroup = _from.asObjectGroup()
                if (fromTileLayer):
                    toTileLayer = to.asTileLayer()
                    self.copyTileRegion(fromTileLayer, rect.x(), rect.y(),
                                   rect.width(), rect.height(),
                                   toTileLayer,
                                   rect.x() + offset.x(), rect.y() + offset.y())
                elif (fromObjectGroup):
                    toObjectGroup = to.asObjectGroup()
                    self.copyObjectRegion(fromObjectGroup, rect.x(), rect.y(),
                                     rect.width(), rect.height(),
                                     toObjectGroup,
                                     rect.x() + offset.x(), rect.y() + offset.y())
                else:
                    pass

    ##
    # This goes through all the positions of the mMapWork and checks if
    # there fits the rule given by the region in mMapRuleSet.
    # if there is a match all Layers are copied to mMapWork.
    # @param ruleIndex: the region which should be compared to all positions
    #              of mMapWork will be looked up in mRulesInput and mRulesOutput
    # @return where: an rectangle where the rule actually got applied
    ##
    def applyRule(self, ruleIndex, where):
        ret = QRect()
        if (self.mLayerList.isEmpty()):
            return ret
        ruleInput = self.mRulesInput.at(ruleIndex)
        ruleOutput = self.mRulesOutput.at(ruleIndex)
        rbr = ruleInput.boundingRect()
        # Since the rule itself is translated, we need to adjust the borders of the
        # loops. Decrease the size at all sides by one: There must be at least one
        # tile overlap to the rule.
        minX = where.left() - rbr.left() - rbr.width() + 1
        minY = where.top() - rbr.top() - rbr.height() + 1
        maxX = where.right() - rbr.left() + rbr.width() - 1
        maxY = where.bottom() - rbr.top() + rbr.height() - 1
        # In this list of regions it is stored which parts or the map have already
        # been altered by exactly this rule. We store all the altered parts to
        # make sure there are no overlaps of the same rule applied to
        # (neighbouring) places
        appliedRegions = QList()
        if (self.mNoOverlappingRules):
            for i in range(self.mMapWork.layerCount()):
                appliedRegions.append(QRegion())
        for y in range(minY, maxY+1):
            for x in range(minX, maxX+1):
                anymatch = False
                for index in self.mInputRules.indexes:
                    ii = self.mInputRules[index]
                    allLayerNamesMatch = True
                    for name in ii.names:
                        i = self.mMapWork.indexOfLayer(name, Layer.TileLayerType)
                        if (i == -1):
                            allLayerNamesMatch = False
                        else:
                            setLayer = self.mMapWork.layerAt(i).asTileLayer()
                            allLayerNamesMatch &= compareLayerTo(setLayer,
                                                                 ii[name].listYes,
                                                                 ii[name].listNo,
                                                                 ruleInput,
                                                                 QPoint(x, y))

                    if (allLayerNamesMatch):
                        anymatch = True
                        break

                if (anymatch):
                    r = 0
                    # choose by chance which group of rule_layers should be used:
                    if (self.mLayerList.size() > 1):
                        r = qrand() % self.mLayerList.size()
                    if (not self.mNoOverlappingRules):
                        self.copyMapRegion(ruleOutput, QPoint(x, y), self.mLayerList.at(r))
                        ret = ret.united(rbr.translated(QPoint(x, y)))
                        continue

                    missmatch = False
                    translationTable = self.mLayerList.at(r)
                    layers = translationTable.keys()
                    # check if there are no overlaps within this rule.
                    ruleRegionInLayer = QVector()
                    for i in range(layers.size()):
                        layer = layers.at(i)
                        appliedPlace = QRegion()
                        tileLayer = layer.asTileLayer()
                        if (tileLayer):
                            appliedPlace = tileLayer.region()
                        else:
                            appliedPlace = tileRegionOfObjectGroup(layer.asObjectGroup())
                        ruleRegionInLayer.append(appliedPlace.intersected(ruleOutput))
                        if (appliedRegions.at(i).intersects(
                                    ruleRegionInLayer[i].translated(x, y))):
                            missmatch = True
                            break

                    if (missmatch):
                        continue
                    self.copyMapRegion(ruleOutput, QPoint(x, y), self.mLayerList.at(r))
                    ret = ret.united(rbr.translated(QPoint(x, y)))
                    for i in range(translationTable.size()):
                        appliedRegions[i] += ruleRegionInLayer[i].translated(x, y)

        return ret

    ##
    # Cleans up the data structes filled by setupRuleMapLayers(),
    # so the next rule can be processed.
    ##
    def cleanUpRuleMapLayers(self):
        self.cleanTileLayers()
        it = QList.const_iterator()
        for it in self.mLayerList:
            del it
        self.mLayerList.clear()
        # do not delete mLayerRuleRegions, it is owned by the rulesmap
        self.mLayerInputRegions = None
        self.mLayerOutputRegions = None
        self.mInputRules.clear()

    ##
    # Cleans up the data structes filled by setupTilesets(),
    # so the next rule can be processed.
    ##
    def cleanTilesets(self):
        for tileset in self.mAddedTilesets:
            if (self.mMapWork.isTilesetUsed(tileset)):
                continue
            index = self.mMapWork.indexOfTileset(tileset)
            if (index == -1):
                continue
            undo = self.mMapDocument.undoStack()
            undo.push(RemoveTileset(self.mMapDocument, index))

        self.mAddedTilesets.clear()

    ##
    # Cleans up the added tile layers setup by setupMissingLayers(),
    # so we have a minimal addition of tile layers by the automapping.
    ##
    def cleanTileLayers(self):
        for tilelayerName in self.mAddedTileLayers:
            layerIndex = self.mMapWork.indexOfLayer(tilelayerName,
                                                          Layer.TileLayerType)
            if (layerIndex == -1):
                continue
            layer = self.mMapWork.layerAt(layerIndex)
            if (not layer.isEmpty()):
                continue
            undo = self.mMapDocument.undoStack()
            undo.push(RemoveLayer(self.mMapDocument, layerIndex))

        self.mAddedTileLayers.clear()
Exemple #30
0
    def __init__(self, workingDocument, rules, rulePath):
        ##
        # where to work in
        ##
        self.mMapDocument = workingDocument

        ##
        # the same as mMapDocument.map()
        ##
        self.mMapWork = None
        if workingDocument:
            self.mMapWork = workingDocument.map()

        ##
        # map containing the rules, usually different than mMapWork
        ##
        self.mMapRules = rules

        ##
        # This contains all added tilesets as pointers.
        # if rules use Tilesets which are not in the mMapWork they are added.
        # keep track of them, because we need to delete them afterwards,
        # when they still are unused
        # they will be added while setupTilesets().
        ##
        self.mAddedTilesets = QVector()

        ##
        # description see: mAddedTilesets, just described by Strings
        ##
        self.mAddedTileLayers = QList()

        ##
        # Points to the tilelayer, which defines the inputregions.
        ##
        self.mLayerInputRegions = None

        ##
        # Points to the tilelayer, which defines the outputregions.
        ##
        self.mLayerOutputRegions = None

        ##
        # Contains all tilelayer pointers, which names begin with input*
        # It is sorted by index and name
        ##
        self.mInputRules = InputLayers()

        ##
        # List of Regions in mMapRules to know where the input rules are
        ##
        self.mRulesInput = QList()

        ##
        # List of regions in mMapRules to know where the output of a
        # rule is.
        # mRulesOutput[i] is the output of that rule,
        # which has the input at mRulesInput[i], meaning that mRulesInput
        # and mRulesOutput must match with the indexes.
        ##
        self.mRulesOutput = QList()

        ##
        # The inner set with layers to indexes is needed for translating
        # tile layers from mMapRules to mMapWork.
        #
        # The key is the pointer to the layer in the rulemap. The
        # pointer to the layer within the working map is not hardwired, but the
        # position in the layerlist, where it was found the last time.
        # This loosely bound pointer ensures we will get the right layer, since we
        # need to check before anyway, and it is still fast.
        #
        # The list is used to hold different translation tables
        # => one of the tables is chosen by chance, so randomness is available
        ##
        self.mLayerList = QList()
        ##
        # store the name of the processed rules file, to have detailed
        # error messages available
        ##
        self.mRulePath = rulePath

        ##
        # determines if all tiles in all touched layers should be deleted first.
        ##
        self.mDeleteTiles = False

        ##
        # This variable determines, how many overlapping tiles should be used.
        # The bigger the more area is remapped at an automapping operation.
        # This can lead to higher latency, but provides a better behavior on
        # interactive automapping.
        # It defaults to zero.
        ##
        self.mAutoMappingRadius = 0

        ##
        # Determines if a rule is allowed to overlap it
        ##
        self.mNoOverlappingRules = False

        self.mTouchedObjectGroups = QSet()
        self.mWarning = QString()
        self.mTouchedTileLayers = QSet()
        self.mError = ''

        if (not self.setupRuleMapProperties()):
            return
        if (not self.setupRuleMapTileLayers()):
            return
        if (not self.setupRuleList()):
            return
Exemple #31
0
 def usedTilesets(self):
     return QSet()