Esempio n. 1
0
    def drawGrid(self, painter, rect, gridColor):
        tileWidth = self.map().tileWidth()
        tileHeight = self.map().tileHeight()
        r = rect.toAlignedRect()
        r.adjust(-tileWidth / 2, -tileHeight / 2, tileWidth / 2, tileHeight / 2)
        startX = int(max(0.0, self.screenToTileCoords_(r.topLeft()).x()))
        startY = int(max(0.0, self.screenToTileCoords_(r.topRight()).y()))
        endX = int(min(self.map().width(), self.screenToTileCoords_(r.bottomRight()).x()))
        endY = int(min(self.map().height(), self.screenToTileCoords_(r.bottomLeft()).y()))
        gridColor.setAlpha(128)
        gridPen = QPen(gridColor)
        gridPen.setCosmetic(True)
        _x = QVector()
        _x.append(2)
        _x.append(2)
        gridPen.setDashPattern(_x)
        painter.setPen(gridPen)
        for y in range(startY, endY+1):
            start = self.tileToScreenCoords(startX, y)
            end = self.tileToScreenCoords(endX, y)
            painter.drawLine(start, end)

        for x in range(startX, endX+1):
            start = self.tileToScreenCoords(x, startY)
            end = self.tileToScreenCoords(x, endY)
            painter.drawLine(start, end)
Esempio n. 2
0
    def readObjectTypes(self, fileName):
        self.mError = ''
        objectTypes = QVector()
        file = QFile(fileName)
        if (not file.open(QIODevice.ReadOnly | QIODevice.Text)):
            self.mError = QCoreApplication.translate("ObjectTypes",
                                                     "Could not open file.")
            return objectTypes

        reader = QXmlStreamReader(file)
        if (not reader.readNextStartElement()
                or reader.name() != "objecttypes"):
            self.mError = QCoreApplication.translate(
                "ObjectTypes", "File doesn't contain object types.")
            return objectTypes

        while (reader.readNextStartElement()):
            if (reader.name() == "objecttype"):
                atts = reader.attributes()
                name = QString(atts.value("name"))
                color = QColor(atts.value("color"))
                objectTypes.append(ObjectType(name, color))

            reader.skipCurrentElement()

        if (reader.hasError()):
            self.mError = QCoreApplication.translate(
                "ObjectTypes", "%s\n\nLine %d, column %d" %
                (reader.errorString(), reader.lineNumber(),
                 reader.columnNumber()))
            return objectTypes

        return objectTypes
Esempio n. 3
0
    def drawGrid(self, painter, rect, gridColor):
        tileWidth = self.map().tileWidth()
        tileHeight = self.map().tileHeight()
        r = rect.toAlignedRect()
        r.adjust(-tileWidth / 2, -tileHeight / 2, tileWidth / 2,
                 tileHeight / 2)
        startX = int(max(0.0, self.screenToTileCoords_(r.topLeft()).x()))
        startY = int(max(0.0, self.screenToTileCoords_(r.topRight()).y()))
        endX = int(
            min(self.map().width(),
                self.screenToTileCoords_(r.bottomRight()).x()))
        endY = int(
            min(self.map().height(),
                self.screenToTileCoords_(r.bottomLeft()).y()))
        gridColor.setAlpha(128)
        gridPen = QPen(gridColor)
        gridPen.setCosmetic(True)
        _x = QVector()
        _x.append(2)
        _x.append(2)
        gridPen.setDashPattern(_x)
        painter.setPen(gridPen)
        for y in range(startY, endY + 1):
            start = self.tileToScreenCoords(startX, y)
            end = self.tileToScreenCoords(endX, y)
            painter.drawLine(start, end)

        for x in range(startX, endX + 1):
            start = self.tileToScreenCoords(x, startY)
            end = self.tileToScreenCoords(x, endY)
            painter.drawLine(start, end)
Esempio n. 4
0
    def offsetTiles(self, offset, bounds, wrapX, wrapY):
        newGrid = QVector()
        for i in range(self.mWidth * self.mHeight):
            newGrid.append(Cell())
        for y in range(self.mHeight):
            for x in range(self.mWidth):
                # Skip out of bounds tiles
                if (not bounds.contains(x, y)):
                    newGrid[x + y * self.mWidth] = self.cellAt(x, y)
                    continue

                # Get position to pull tile value from
                oldX = x - offset.x()
                oldY = y - offset.y()
                # Wrap x value that will be pulled from
                if (wrapX and bounds.width() > 0):
                    while oldX < bounds.left():
                        oldX += bounds.width()
                    while oldX > bounds.right():
                        oldX -= bounds.width()

                # Wrap y value that will be pulled from
                if (wrapY and bounds.height() > 0):
                    while oldY < bounds.top():
                        oldY += bounds.height()
                    while oldY > bounds.bottom():
                        oldY -= bounds.height()

                # Set the new tile
                if (self.contains(oldX, oldY) and bounds.contains(oldX, oldY)):
                    newGrid[x + y * self.mWidth] = self.cellAt(oldX, oldY)
                else:
                    newGrid[x + y * self.mWidth] = Cell()

        self.mGrid = newGrid
Esempio n. 5
0
    def offsetTiles(self, offset, bounds, wrapX, wrapY):
        newGrid = QVector()
        for i in range(self.mWidth * self.mHeight):
            newGrid.append(Cell())
        for y in range(self.mHeight):
            for x in range(self.mWidth):
                # Skip out of bounds tiles
                if (not bounds.contains(x, y)):
                    newGrid[x + y * self.mWidth] = self.cellAt(x, y)
                    continue

                # Get position to pull tile value from
                oldX = x - offset.x()
                oldY = y - offset.y()
                # Wrap x value that will be pulled from
                if (wrapX and bounds.width() > 0):
                    while oldX < bounds.left():
                        oldX += bounds.width()
                    while oldX > bounds.right():
                        oldX -= bounds.width()

                # Wrap y value that will be pulled from
                if (wrapY and bounds.height() > 0):
                    while oldY < bounds.top():
                        oldY += bounds.height()
                    while oldY > bounds.bottom():
                        oldY -= bounds.height()

                # Set the new tile
                if (self.contains(oldX, oldY) and bounds.contains(oldX, oldY)):
                    newGrid[x + y * self.mWidth] = self.cellAt(oldX, oldY)
                else:
                    newGrid[x + y * self.mWidth] = Cell()

        self.mGrid = newGrid
Esempio n. 6
0
    def drawGrid(self, painter, rect, gridColor):
        tileWidth = self.map().tileWidth()
        tileHeight = self.map().tileHeight()
        if (tileWidth <= 0 or tileHeight <= 0):
            return
        startX = max(0,  int(rect.x() / tileWidth) * tileWidth)
        startY = max(0,  int(rect.y() / tileHeight) * tileHeight)
        endX = min(math.ceil(rect.right()), self.map().width() * tileWidth + 1)
        endY = min(math.ceil(rect.bottom()), self.map().height() * tileHeight + 1)
        gridColor.setAlpha(128)
        gridPen = QPen(gridColor)
        gridPen.setCosmetic(True)
        _x = QVector()
        _x.append(2)
        _x.append(2)
        gridPen.setDashPattern(_x)
        if (startY < endY):
            gridPen.setDashOffset(startY)
            painter.setPen(gridPen)
            for x in range(startX, endX, tileWidth):
                painter.drawLine(x, startY, x, endY - 1)

        if (startX < endX):
            gridPen.setDashOffset(startX)
            painter.setPen(gridPen)
            for y in range(startY, endY, tileHeight):
                painter.drawLine(startX, y, endX - 1, y)
Esempio n. 7
0
    def readObjectTypes(self, fileName):
        self.mError = ''
        objectTypes = QVector()
        file = QFile(fileName)
        if (not file.open(QIODevice.ReadOnly | QIODevice.Text)):
            self.mError = QCoreApplication.translate(
                        "ObjectTypes", "Could not open file.")
            return objectTypes

        reader = QXmlStreamReader(file)
        if (not reader.readNextStartElement() or reader.name() != "objecttypes"):
            self.mError = QCoreApplication.translate(
                        "ObjectTypes", "File doesn't contain object types.")
            return objectTypes

        while (reader.readNextStartElement()):
            if (reader.name() == "objecttype"):
                atts = reader.attributes()
                name = QString(atts.value("name"))
                color = QColor(atts.value("color"))
                objectTypes.append(ObjectType(name, color))

            reader.skipCurrentElement()

        if (reader.hasError()):
            self.mError = QCoreApplication.translate("ObjectTypes", "%s\n\nLine %d, column %d"%(reader.errorString(), reader.lineNumber(), reader.columnNumber()))
            return objectTypes

        return objectTypes
Esempio n. 8
0
    def drawGrid(self, painter, rect, gridColor):
        tileWidth = self.map().tileWidth()
        tileHeight = self.map().tileHeight()
        if (tileWidth <= 0 or tileHeight <= 0):
            return
        startX = max(0, int(rect.x() / tileWidth) * tileWidth)
        startY = max(0, int(rect.y() / tileHeight) * tileHeight)
        endX = min(math.ceil(rect.right()), self.map().width() * tileWidth + 1)
        endY = min(math.ceil(rect.bottom()),
                   self.map().height() * tileHeight + 1)
        gridColor.setAlpha(128)
        gridPen = QPen(gridColor)
        gridPen.setCosmetic(True)
        _x = QVector()
        _x.append(2)
        _x.append(2)
        gridPen.setDashPattern(_x)
        if (startY < endY):
            gridPen.setDashOffset(startY)
            painter.setPen(gridPen)
            for x in range(startX, endX, tileWidth):
                painter.drawLine(x, startY, x, endY - 1)

        if (startX < endX):
            gridPen.setDashOffset(startX)
            painter.setPen(gridPen)
            for y in range(startY, endY, tileHeight):
                painter.drawLine(startX, y, endX - 1, y)
Esempio n. 9
0
 def removeObjectTypes(self, indexes):
     rows = QVector()
     for index in indexes:
         rows.append(index.row())
     rows = sorted(rows)
     for i in range(len(rows) - 1, -1, -1):
         row = rows[i]
         self.beginRemoveRows(QModelIndex(), row, row)
         self.mObjectTypes.remove(row)
         self.endRemoveRows()
Esempio n. 10
0
 def removeObjectTypes(self, indexes):
     rows = QVector()
     for index in indexes:
         rows.append(index.row())
     rows = sorted(rows)
     for i in range(len(rows) - 1, -1, -1):
         row = rows[i]
         self.beginRemoveRows(QModelIndex(), row, row)
         self.mObjectTypes.remove(row)
         self.endRemoveRows()
Esempio n. 11
0
def cellsInRegion(list, r):
    cells = QVector()
    for tilelayer in list:
        for rect in r.rects():
            for x in range(rect.left(), rect.right() + 1):
                for y in range(rect.top(), rect.bottom() + 1):
                    cell = tilelayer.cellAt(x, y)
                    if (not cells.contains(cell)):
                        cells.append(cell)

    return cells
Esempio n. 12
0
def cellsInRegion(list, r):
    cells = QVector()
    for tilelayer in list:
        for rect in r.rects():
            for x in range(rect.left(), rect.right()+1):
                for y in range(rect.top(), rect.bottom()+1):
                    cell = tilelayer.cellAt(x, y)
                    if (not cells.contains(cell)):
                        cells.append(cell)

    return cells
Esempio n. 13
0
    def __readAnimationFrames(self):
        frames = QVector()
        while (self.xml.readNextStartElement()):
            if (self.xml.name() == "frame"):
                atts = self.xml.attributes()
                frame = Frame()
                frame.tileId = Int(atts.value("tileid"))
                frame.duration = Int(atts.value("duration"))
                frames.append(frame)
                self.xml.skipCurrentElement()
            else:
                self.__readUnknownElement()

        return frames
Esempio n. 14
0
    def __readAnimationFrames(self):
        frames = QVector()
        while (self.xml.readNextStartElement()):
            if (self.xml.name() == "frame"):
                atts = self.xml.attributes()
                frame = Frame()
                frame.tileId = Int(atts.value("tileid"))
                frame.duration = Int(atts.value("duration"))
                frames.append(frame)
                self.xml.skipCurrentElement()
            else:
                self.__readUnknownElement()

        return frames
Esempio n. 15
0
    def flip(self, direction):
        newGrid = QVector()
        for i in range(self.mWidth * self.mHeight):
            newGrid.append(Cell())
        for y in range(self.mHeight):
            for x in range(self.mWidth):
                dest = newGrid[x + y * self.mWidth]
                if (direction == FlipDirection.FlipHorizontally):
                    source = self.cellAt(self.mWidth - x - 1, y)
                    dest = source
                    dest.flippedHorizontally = not source.flippedHorizontally
                elif (direction == FlipDirection.FlipVertically):
                    source = self.cellAt(x, self.mHeight - y - 1)
                    dest = source
                    dest.flippedVertically = not source.flippedVertically

        self.mGrid = newGrid
Esempio n. 16
0
    def flip(self, direction):
        newGrid = QVector()
        for i in range(self.mWidth * self.mHeight):
            newGrid.append(Cell())
        for y in range(self.mHeight):
            for x in range(self.mWidth):
                dest = newGrid[x + y * self.mWidth]
                if (direction == FlipDirection.FlipHorizontally):
                    source = self.cellAt(self.mWidth - x - 1, y)
                    dest = source
                    dest.flippedHorizontally = not source.flippedHorizontally
                elif (direction == FlipDirection.FlipVertically):
                    source = self.cellAt(x, self.mHeight - y - 1)
                    dest = source
                    dest.flippedVertically = not source.flippedVertically

        self.mGrid = newGrid
Esempio n. 17
0
    def resize(self, size, offset):
        if (self.size() == size and offset.isNull()):
            return
        newGrid = QVector()
        for i in range(size.width() * size.height()):
            newGrid.append(Cell())
        # Copy over the preserved part
        startX = max(0, -offset.x())
        startY = max(0, -offset.y())
        endX = min(self.mWidth, size.width() - offset.x())
        endY = min(self.mHeight, size.height() - offset.y())
        for y in range(startY, endY):
            for x in range(startX, endX):
                index = x + offset.x() + (y + offset.y()) * size.width()
                newGrid[index] = self.cellAt(x, y)

        self.mGrid = newGrid
        self.setSize(size)
Esempio n. 18
0
    def resize(self, size, offset):
        if (self.size() == size and offset.isNull()):
            return
        newGrid = QVector()
        for i in range(size.width() * size.height()):
            newGrid.append(Cell())
        # Copy over the preserved part
        startX = max(0, -offset.x())
        startY = max(0, -offset.y())
        endX = min(self.mWidth, size.width() - offset.x())
        endY = min(self.mHeight, size.height() - offset.y())
        for y in range(startY, endY):
            for x in range(startX, endX):
                index = x + offset.x() + (y + offset.y()) * size.width()
                newGrid[index] = self.cellAt(x, y)

        self.mGrid = newGrid
        self.setSize(size)
Esempio n. 19
0
class SetProperty(QUndoCommand):
    ##
    # Constructs a new 'Set Property' command.
    #
    # @param mapDocument  the map document of the object's map
    # @param objects      the objects of which the property should be changed
    # @param name         the name of the property to be changed
    # @param value        the new value of the property
    ##
    def __init__(self, mapDocument, objects, name, value, parent=None):
        super().__init__(parent)

        self.mProperties = QVector()
        self.mMapDocument = mapDocument
        self.mObjects = objects
        self.mName = name
        self.mValue = value

        for obj in self.mObjects:
            prop = ObjectProperty()
            prop.existed = obj.hasProperty(self.mName)
            prop.previousValue = obj.property(self.mName)
            self.mProperties.append(prop)

        if (self.mObjects.size() > 1
                or self.mObjects[0].hasProperty(self.mName)):
            self.setText(
                QCoreApplication.translate("Undo Commands", "Set Property"))
        else:
            self.setText(
                QCoreApplication.translate("Undo Commands", "Add Property"))

    def undo(self):
        for i in range(self.mObjects.size()):
            if (self.mProperties[i].existed):
                self.mMapDocument.setProperty(
                    self.mObjects[i], self.mName,
                    self.mProperties[i].previousValue)
            else:
                self.mMapDocument.removeProperty(self.mObjects[i], self.mName)

    def redo(self):
        for obj in self.mObjects:
            self.mMapDocument.setProperty(obj, self.mName, self.mValue)
Esempio n. 20
0
    def autoMapInternal(self, where, touchedLayer):
        self.mError = ''
        self.mWarning = ''
        if (not self.mMapDocument):
            return
        automatic = touchedLayer != None
        if (not self.mLoaded):
            mapPath = QFileInfo(self.mMapDocument.fileName()).path()
            rulesFileName = mapPath + "/rules.txt"
            if (self.loadFile(rulesFileName)):
                self.mLoaded = True
            else:
                self.errorsOccurred.emit(automatic)
                return

        passedAutoMappers = QVector()
        if (touchedLayer):
            for a in self.mAutoMappers:
                if (a.ruleLayerNameUsed(touchedLayer.name())):
                    passedAutoMappers.append(a)
        else:
            passedAutoMappers = self.mAutoMappers

        if (not passedAutoMappers.isEmpty()):
            # use a pointer to the region, so each automapper can manipulate it and the
            # following automappers do see the impact
            region = QRegion(where)

            undoStack = self.mMapDocument.undoStack()
            undoStack.beginMacro(self.tr("Apply AutoMap rules"))
            aw = AutoMapperWrapper(self.mMapDocument, passedAutoMappers,
                                   region)
            undoStack.push(aw)
            undoStack.endMacro()

        for automapper in self.mAutoMappers:
            self.mWarning += automapper.warningString()
            self.mError += automapper.errorString()

        if self.mWarning != '':
            self.warningsOccurred.emit(automatic)
        if self.mError != '':
            self.errorsOccurred.emit(automatic)
Esempio n. 21
0
    def autoMapInternal(self, where, touchedLayer):
        self.mError = ''
        self.mWarning = ''
        if (not self.mMapDocument):
            return
        automatic = touchedLayer != None
        if (not self.mLoaded):
            mapPath = QFileInfo(self.mMapDocument.fileName()).path()
            rulesFileName = mapPath + "/rules.txt"
            if (self.loadFile(rulesFileName)):
                self.mLoaded = True
            else:
                self.errorsOccurred.emit(automatic)
                return
                
        passedAutoMappers = QVector()
        if (touchedLayer):
            for a in self.mAutoMappers:
                if (a.ruleLayerNameUsed(touchedLayer.name())):
                    passedAutoMappers.append(a)
        else:
            passedAutoMappers = self.mAutoMappers

        if (not passedAutoMappers.isEmpty()):
            # use a pointer to the region, so each automapper can manipulate it and the
            # following automappers do see the impact
            region = QRegion(where)
        
            undoStack = self.mMapDocument.undoStack()
            undoStack.beginMacro(self.tr("Apply AutoMap rules"))
            aw = AutoMapperWrapper(self.mMapDocument, passedAutoMappers, region)
            undoStack.push(aw)
            undoStack.endMacro()

        for automapper in self.mAutoMappers:
            self.mWarning += automapper.warningString()
            self.mError += automapper.errorString()

        if self.mWarning != '':
            self.warningsOccurred.emit(automatic)
        if self.mError != '':
            self.errorsOccurred.emit(automatic)
Esempio n. 22
0
def pointsOnLine(*args):
    l = len(args)
    if l==2:
        a, b = args
        return pointsOnLine(a.x(), a.y(), b.x(), b.y())
    else:
        x0, y0, x1, y1 = args
        ret = QVector()
        steep = abs(y1 - y0) > abs(x1 - x0)
        if steep:
            x0, y0 = y0, x0
            x1, y1 = y1, x1

        reverse = x0 > x1
        if reverse:
            x0, x1 = x1, x0
            y0, y1 = y1, y0

        deltax = x1 - x0
        deltay = abs(y1 - y0)
        error= int(deltax / 2)
        ystep = 0
        y = y0
        if (y0 < y1):
            ystep = 1
        else:
            ystep = -1
        for x in range(x0, x1+1):
            if (steep):
                ret.append(QPoint(y, x))
            else:
                ret.append(QPoint(x, y))
            error = error - deltay
            if (error < 0):
                 y = y + ystep
                 error = error + deltax

        if reverse:
            ret.reverse()
            
        return ret
Esempio n. 23
0
    def dropMimeData(self, data, action, row, column, parent):
        if (action == Qt.IgnoreAction):
            return True
        if (column > 0):
            return False
        beginRow = 0
        if (row != -1):
            beginRow = row
        elif parent.isValid():
            beginRow = parent.row()
        else:
            beginRow = self.mFrames.size()
        newFrames = QVector()
        if (data.hasFormat(FRAMES_MIMETYPE)):
            encodedData = data.data(FRAMES_MIMETYPE)
            stream = QDataStream(encodedData, QIODevice.ReadOnly)
            while (not stream.atEnd()):
                frame = Frame()
                frame.tileId = stream.readInt()
                frame.duration = stream.readInt()
                newFrames.append(frame)
        elif (data.hasFormat(TILES_MIMETYPE)):
            encodedData = data.data(TILES_MIMETYPE)
            stream = QDataStream(encodedData, QIODevice.ReadOnly)
            while (not stream.atEnd()):
                frame = Frame()
                frame.tileId = stream.readInt()
                frame.duration = FrameListModel.DEFAULT_DURATION
                newFrames.append(frame)

        if (newFrames.isEmpty()):
            return False
        self.beginInsertRows(QModelIndex(), beginRow,
                             beginRow + newFrames.size() - 1)
        self.mFrames.insert(beginRow, newFrames.size(), Frame())
        for i in range(newFrames.size()):
            self.mFrames[i + beginRow] = newFrames[i]
        self.endInsertRows()
        return True
Esempio n. 24
0
class SetProperty(QUndoCommand):
    ##
    # Constructs a new 'Set Property' command.
    #
    # @param mapDocument  the map document of the object's map
    # @param objects      the objects of which the property should be changed
    # @param name         the name of the property to be changed
    # @param value        the new value of the property
    ##
    def __init__(self, mapDocument, objects, name, value, parent = None):
        super().__init__(parent)

        self.mProperties = QVector()
        self.mMapDocument = mapDocument
        self.mObjects = objects
        self.mName = name
        self.mValue = value

        for obj in self.mObjects:
            prop = ObjectProperty()
            prop.existed = obj.hasProperty(self.mName)
            prop.previousValue = obj.property(self.mName)
            self.mProperties.append(prop)

        if (self.mObjects.size() > 1 or self.mObjects[0].hasProperty(self.mName)):
            self.setText(QCoreApplication.translate("Undo Commands", "Set Property"))
        else:
            self.setText(QCoreApplication.translate("Undo Commands", "Add Property"))

    def undo(self):
        for i in range(self.mObjects.size()):
            if (self.mProperties[i].existed):
                self.mMapDocument.setProperty(self.mObjects[i], self.mName, self.mProperties[i].previousValue)
            else:
                self.mMapDocument.removeProperty(self.mObjects[i], self.mName)

    def redo(self):
        for obj in self.mObjects:
            self.mMapDocument.setProperty(obj, self.mName, self.mValue)
Esempio n. 25
0
    def __readTilesetTile(self, tileset):
        atts = self.xml.attributes()
        id = Int(atts.value("id"))
        if (id < 0):
            self.xml.raiseError(self.tr("Invalid tile ID: %d" % id))
            return

        hasImage = tileset.imageSource() != ''
        if (hasImage and id >= tileset.tileCount()):
            self.xml.raiseError(
                self.tr("Tile ID does not exist in tileset image: %d" % id))
            return

        if (id > tileset.tileCount()):
            self.xml.raiseError(
                self.tr("Invalid (nonconsecutive) tile ID: %d" % id))
            return

        # For tilesets without image source, consecutive tile IDs are allowed (for
        # tiles with individual images)
        if (id == tileset.tileCount()):
            tileset.addTile(QPixmap())
        tile = tileset.tileAt(id)
        # Read tile quadrant terrain ids
        terrain = atts.value("terrain")
        if terrain != '':
            quadrants = terrain.split(",")
            if (len(quadrants) == 4):
                for i in range(4):
                    if quadrants[i] == '':
                        t = -1
                    else:
                        t = Int(quadrants[i])
                    tile.setCornerTerrainId(i, t)

        # Read tile probability
        probability = atts.value("probability")
        if probability != '':
            tile.setProbability(Float(probability))
        while (self.xml.readNextStartElement()):
            if (self.xml.name() == "properties"):
                tile.mergeProperties(self.__readProperties())
            elif (self.xml.name() == "image"):
                source = self.xml.attributes().value("source")
                if source != '':
                    source = self.p.resolveReference(source, self.mPath)
                tileset.setTileImage(id, QPixmap.fromImage(self.__readImage()),
                                     source)
            elif (self.xml.name() == "objectgroup"):
                tile.setObjectGroup(self.__readObjectGroup())
            elif (self.xml.name() == "animation"):
                tile.setFrames(self.__readAnimationFrames())
            else:
                self.__readUnknownElement()

        # Temporary code to support TMW-style animation frame properties
        if (not tile.isAnimated() and tile.hasProperty("animation-frame0")):
            frames = QVector()
            i = 0
            while (i >= 0):
                frameName = "animation-frame" + str(i)
                delayName = "animation-delay" + str(i)
                if (tile.hasProperty(frameName)
                        and tile.hasProperty(delayName)):
                    frame = Frame()
                    frame.tileId = tile.property(frameName)
                    frame.duration = tile.property(delayName) * 10
                    frames.append(frame)
                else:
                    break
                i += 1

            tile.setFrames(frames)
Esempio n. 26
0
    def drawPreviewLayer(self, _list):
        self.mPreviewLayer = None

        if self.mStamp.isEmpty():
            return

        if self.mIsRandom:
            if self.mRandomCellPicker.isEmpty():
                return

            paintedRegion = QRegion()
            for p in _list:
                paintedRegion += QRect(p, QSize(1, 1))

            bounds = paintedRegion.boundingRect()
            preview = TileLayer(QString(),
                                  bounds.x(), bounds.y(),
                                  bounds.width(), bounds.height())

            for p in _list:
                cell = self.mRandomCellPicker.pick()
                preview.setCell(p.x() - bounds.left(),
                                 p.y() - bounds.top(),
                                 cell)

            self.mPreviewLayer = preview
        else:
            self.mMissingTilesets.clear()

            paintedRegion = QRegion()
            operations = QVector()
            regionCache = QHash()

            for p in _list:
                variation = self.mStamp.randomVariation()
                self.mapDocument().unifyTilesets(variation.map, self.mMissingTilesets)

                stamp = variation.tileLayer()

                stampRegion = QRegion()
                if regionCache.contains(stamp):
                    stampRegion = regionCache.value(stamp)
                else:
                    stampRegion = stamp.region()
                    regionCache.insert(stamp, stampRegion)

                centered = QPoint(p.x() - int(stamp.width() / 2), p.y() - int(stamp.height() / 2))

                region = stampRegion.translated(centered.x(), centered.y())
                if not paintedRegion.intersects(region):
                    paintedRegion += region

                    op = PaintOperation(centered, stamp)
                    operations.append(op)

            bounds = paintedRegion.boundingRect()
            preview = TileLayer(QString(),
                                      bounds.x(), bounds.y(),
                                      bounds.width(), bounds.height())

            for op in operations:
                preview.merge(op.pos - bounds.topLeft(), op.stamp)
            
            self.mPreviewLayer = preview
Esempio n. 27
0
    def __toTileset(self, variant):
        variantMap = variant
        firstGid = variantMap.get("firstgid",0)
        
        # Handle external tilesets
        sourceVariant = variantMap.get("source", '')
        if sourceVariant != '':
            source = resolvePath(self.mMapDir, sourceVariant)
            tileset, error = readTileset(source)
            if not tileset:
                self.mError = self.tr("Error while loading tileset '%s': %s"%(source, error))
            else:
                self.mGidMapper.insert(firstGid, tileset)
            return tileset

        name = variantMap.get("name",'')
        tileWidth = variantMap.get("tilewidth",0)
        tileHeight = variantMap.get("tileheight",0)
        spacing = variantMap.get("spacing",0)
        margin = variantMap.get("margin",0)
        tileOffset = variantMap.get("tileoffset", {})
        tileOffsetX = tileOffset.get("x",0)
        tileOffsetY = tileOffset.get("y",0)
        if (tileWidth <= 0 or tileHeight <= 0 or (firstGid == 0 and not self.mReadingExternalTileset)):
            self.mError = self.tr("Invalid tileset parameters for tileset '%s'"%name)
            return None
        
        tileset = Tileset.create(name, tileWidth, tileHeight, spacing, margin)
        tileset.setTileOffset(QPoint(tileOffsetX, tileOffsetY))
        trans = variantMap.get("transparentcolor", '')
        if (trans!='' and QColor.isValidColor(trans)):
            tileset.setTransparentColor(QColor(trans))
        imageVariant = variantMap.get("image",'')
        if imageVariant != '':
            imagePath = resolvePath(self.mMapDir, imageVariant)
            if (not tileset.loadFromImage(imagePath)):
                self.mError = self.tr("Error loading tileset image:\n'%s'"%imagePath)
                return None

        tileset.setProperties(self.toProperties(variantMap.get("properties", {})))
        
        # Read terrains
        terrainsVariantList = variantMap.get("terrains", [])
        for terrainMap in terrainsVariantList:
            tileset.addTerrain(terrainMap.get("name", ''), terrainMap.get("tile", 0))

        # Read tile terrain and external image information
        tilesVariantMap = variantMap.get("tiles", {})
        for it in tilesVariantMap.items():
            ok = False
            tileIndex = Int(it[0])
            if (tileIndex < 0):
                self.mError = self.tr("Tileset tile index negative:\n'%d'"%tileIndex)
            
            if (tileIndex >= tileset.tileCount()):
                # Extend the tileset to fit the tile
                if (tileIndex >= len(tilesVariantMap)):
                    # If tiles are  defined this way, there should be an entry
                    # for each tile.
                    # Limit the index to number of entries to prevent running out
                    # of memory on malicious input.f
                    self.mError = self.tr("Tileset tile index too high:\n'%d'"%tileIndex)
                    return None
                
                for i in range(tileset.tileCount(), tileIndex+1):
                    tileset.addTile(QPixmap())
            
            tile = tileset.tileAt(tileIndex)
            if (tile):
                tileVar = it[1]
                terrains = tileVar.get("terrain", [])
                if len(terrains) == 4:
                    for i in range(0, 4):
                        terrainId, ok = Int2(terrains[i])
                        if (ok and terrainId >= 0 and terrainId < tileset.terrainCount()):
                            tile.setCornerTerrainId(i, terrainId)

                probability, ok = Float2(tileVar.get("probability", 0.0))
                if (ok):
                    tile.setProbability(probability)
                imageVariant = tileVar.get("image",'')
                if imageVariant != '':
                    imagePath = resolvePath(self.mMapDir, imageVariant)
                    tileset.setTileImage(tileIndex, QPixmap(imagePath), imagePath)
                
                objectGroupVariant = tileVar.get("objectgroup", {})
                if len(objectGroupVariant) > 0:
                    tile.setObjectGroup(self.toObjectGroup(objectGroupVariant))
                frameList = tileVar.get("animation", [])
                lenFrames = len(frameList)
                if lenFrames > 0:
                    frames = QVector()
                    for i in range(lenFrames):
                        frames.append(Frame())
                    for i in range(lenFrames - 1, -1, -1):
                        frameVariantMap = frameList[i]
                        frame = frames[i]
                        frame.tileId = frameVariantMap.get("tileid", 0)
                        frame.duration = frameVariantMap.get("duration", 0)
                    
                    tile.setFrames(frames)

        
        # Read tile properties
        propertiesVariantMap = variantMap.get("tileproperties", {})
        
        for it in propertiesVariantMap.items():
            tileIndex = Int(it[0])
            propertiesVar = it[1]
            if (tileIndex >= 0 and tileIndex < tileset.tileCount()):
                properties = self.toProperties(propertiesVar)
                tileset.tileAt(tileIndex).setProperties(properties)

        if not self.mReadingExternalTileset:        
            self.mGidMapper.insert(firstGid, tileset)

        return tileset
Esempio n. 28
0
    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
Esempio n. 29
0
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)
Esempio n. 30
0
class ObjectTypesModel(QAbstractTableModel):
    ColorRole = Qt.UserRole

    def __init__(self, parent):
        super().__init__(parent)

        self.mObjectTypes = QVector()

    def setObjectTypes(self, objectTypes):
        self.beginResetModel()
        self.mObjectTypes = objectTypes
        self.endResetModel()

    def objectTypes(self):
        return self.mObjectTypes

    def rowCount(self, parent):
        if parent.isValid():
            _x = 0
        else:
            _x = self.mObjectTypes.size()
        return _x

    def columnCount(self, parent):
        if parent.isValid():
            _x = 0
        else:
            _x = 2
        return _x

    def headerData(self, section, orientation, role):
        if (orientation == Qt.Horizontal):
            if (role == Qt.DisplayRole):
                x = section
                if x == 0:
                    return self.tr("Type")
                elif x == 1:
                    return self.tr("Color")
            elif (role == Qt.TextAlignmentRole):
                return Qt.AlignLeft

        return QVariant()

    def data(self, index, role):
        # QComboBox requests data for an invalid index when the model is empty
        if (not index.isValid()):
            return QVariant()
        objectType = self.mObjectTypes.at(index.row())
        if (role == Qt.DisplayRole or role == Qt.EditRole):
            if (index.column() == 0):
                return objectType.name
        if (role == ObjectTypesModel.ColorRole and index.column() == 1):
            return objectType.color
        return QVariant()

    def setData(self, index, value, role):
        if (role == Qt.EditRole and index.column() == 0):
            self.mObjectTypes[index.row()].name = value.strip()
            self.dataChanged.emit(index, index)
            return True

        return False

    def flags(self, index):
        f = super().flags(index)
        if (index.column() == 0):
            f |= Qt.ItemIsEditable
        return f

    def setObjectTypeColor(self, objectIndex, color):
        self.mObjectTypes[objectIndex].color = color
        mi = self.index(objectIndex, 1)
        self.dataChanged.emit(mi, mi)

    def removeObjectTypes(self, indexes):
        rows = QVector()
        for index in indexes:
            rows.append(index.row())
        rows = sorted(rows)
        for i in range(len(rows) - 1, -1, -1):
            row = rows[i]
            self.beginRemoveRows(QModelIndex(), row, row)
            self.mObjectTypes.remove(row)
            self.endRemoveRows()

    def appendNewObjectType(self):
        self.beginInsertRows(QModelIndex(), self.mObjectTypes.size(),
                             self.mObjectTypes.size())
        self.mObjectTypes.append(ObjectType())
        self.endInsertRows()
Esempio n. 31
0
    def drawPreviewLayer(self, _list):
        self.mPreviewLayer = None

        if self.mStamp.isEmpty():
            return

        if self.mIsRandom:
            if self.mRandomCellPicker.isEmpty():
                return

            paintedRegion = QRegion()
            for p in _list:
                paintedRegion += QRect(p, QSize(1, 1))

            bounds = paintedRegion.boundingRect()
            preview = TileLayer(QString(), bounds.x(), bounds.y(),
                                bounds.width(), bounds.height())

            for p in _list:
                cell = self.mRandomCellPicker.pick()
                preview.setCell(p.x() - bounds.left(),
                                p.y() - bounds.top(), cell)

            self.mPreviewLayer = preview
        else:
            self.mMissingTilesets.clear()

            paintedRegion = QRegion()
            operations = QVector()
            regionCache = QHash()

            for p in _list:
                variation = self.mStamp.randomVariation()
                self.mapDocument().unifyTilesets(variation.map,
                                                 self.mMissingTilesets)

                stamp = variation.tileLayer()

                stampRegion = QRegion()
                if regionCache.contains(stamp):
                    stampRegion = regionCache.value(stamp)
                else:
                    stampRegion = stamp.region()
                    regionCache.insert(stamp, stampRegion)

                centered = QPoint(p.x() - int(stamp.width() / 2),
                                  p.y() - int(stamp.height() / 2))

                region = stampRegion.translated(centered.x(), centered.y())
                if not paintedRegion.intersects(region):
                    paintedRegion += region

                    op = PaintOperation(centered, stamp)
                    operations.append(op)

            bounds = paintedRegion.boundingRect()
            preview = TileLayer(QString(), bounds.x(), bounds.y(),
                                bounds.width(), bounds.height())

            for op in operations:
                preview.merge(op.pos - bounds.topLeft(), op.stamp)

            self.mPreviewLayer = preview
Esempio n. 32
0
class TileStampManager(QObject):
    setStamp = pyqtSignal(TileStamp)

    def __init__(self, toolManager, parent=None):
        super().__init__(parent)

        self.mStampsByName = QMap()
        self.mQuickStamps = QVector()
        for i in range(TileStampManager.quickStampKeys().__len__()):
            self.mQuickStamps.append(0)

        self.mTileStampModel = TileStampModel(self)
        self.mToolManager = toolManager

        prefs = preferences.Preferences.instance()
        prefs.stampsDirectoryChanged.connect(self.stampsDirectoryChanged)
        self.mTileStampModel.stampAdded.connect(self.stampAdded)
        self.mTileStampModel.stampRenamed.connect(self.stampRenamed)
        self.mTileStampModel.stampChanged.connect(self.saveStamp)
        self.mTileStampModel.stampRemoved.connect(self.deleteStamp)
        self.loadStamps()

    def __del__(self):
        # needs to be over here where the TileStamp type is complete
        pass

    ##
    # Returns the keys used for quickly accessible tile stamps.
    # Note: To store a tile layer <Ctrl> is added. The given keys will work
    # for recalling the stored values.
    ##
    def quickStampKeys():
        keys = [
            Qt.Key_1, Qt.Key_2, Qt.Key_3, Qt.Key_4, Qt.Key_5, Qt.Key_6,
            Qt.Key_7, Qt.Key_8, Qt.Key_9
        ]
        return keys

    def tileStampModel(self):
        return self.mTileStampModel

    def createStamp(self):
        stamp = self.tampFromContext(self.mToolManager.selectedTool())
        if (not stamp.isEmpty()):
            self.mTileStampModel.addStamp(stamp)
        return stamp

    def addVariation(self, targetStamp):
        stamp = stampFromContext(self.mToolManager.selectedTool())
        if (stamp.isEmpty()):
            return
        if (stamp == targetStamp):  # avoid easy mistake of adding duplicates
            return
        for variation in stamp.variations():
            self.mTileStampModel.addVariation(targetStamp, variation)

    def selectQuickStamp(self, index):
        stamp = self.mQuickStamps.at(index)
        if (not stamp.isEmpty()):
            self.setStamp.emit(stamp)

    def createQuickStamp(self, index):
        stamp = stampFromContext(self.mToolManager.selectedTool())
        if (stamp.isEmpty()):
            return
        self.setQuickStamp(index, stamp)

    def extendQuickStamp(self, index):
        quickStamp = self.mQuickStamps[index]
        if (quickStamp.isEmpty()):
            self.createQuickStamp(index)
        else:
            self.addVariation(quickStamp)

    def stampsDirectoryChanged(self):
        # erase current stamps
        self.mQuickStamps.fill(TileStamp())
        self.mStampsByName.clear()
        self.mTileStampModel.clear()
        self.loadStamps()

    def eraseQuickStamp(self, index):
        stamp = self.mQuickStamps.at(index)
        if (not stamp.isEmpty()):
            self.mQuickStamps[index] = TileStamp()
            if (not self.mQuickStamps.contains(stamp)):
                self.mTileStampModel.removeStamp(stamp)

    def setQuickStamp(self, index, stamp):
        stamp.setQuickStampIndex(index)
        # make sure existing quickstamp is removed from stamp model
        self.eraseQuickStamp(index)
        self.mTileStampModel.addStamp(stamp)
        self.mQuickStamps[index] = stamp

    def loadStamps(self):
        prefs = preferences.Preferences.instance()
        stampsDirectory = prefs.stampsDirectory()
        stampsDir = QDir(stampsDirectory)
        iterator = QDirIterator(stampsDirectory, ["*.stamp"],
                                QDir.Files | QDir.Readable)
        while (iterator.hasNext()):
            stampFileName = iterator.next()
            stampFile = QFile(stampFileName)
            if (not stampFile.open(QIODevice.ReadOnly)):
                continue
            data = stampFile.readAll()
            document = QJsonDocument.fromBinaryData(data)
            if (document.isNull()):
                # document not valid binary data, maybe it's an JSON text file
                error = QJsonParseError()
                document = QJsonDocument.fromJson(data, error)
                if (error.error != QJsonParseError.NoError):
                    qDebug("Failed to parse stamp file:" + error.errorString())
                    continue

            stamp = TileStamp.fromJson(document.object(), stampsDir)
            if (stamp.isEmpty()):
                continue
            stamp.setFileName(iterator.fileInfo().fileName())
            self.mTileStampModel.addStamp(stamp)
            index = stamp.quickStampIndex()
            if (index >= 0 and index < self.mQuickStamps.size()):
                self.mQuickStamps[index] = stamp

    def stampAdded(self, stamp):
        if (stamp.name().isEmpty()
                or self.mStampsByName.contains(stamp.name())):
            # pick the first available stamp name
            name = QString()
            index = self.mTileStampModel.stamps().size()
            while (self.mStampsByName.contains(name)):
                name = str(index)
                index += 1

            stamp.setName(name)

        self.mStampsByName.insert(stamp.name(), stamp)
        if (stamp.fileName().isEmpty()):
            stamp.setFileName(findStampFileName(stamp.name()))
            self.saveStamp(stamp)

    def stampRenamed(self, stamp):
        existingName = self.mStampsByName.key(stamp)
        self.mStampsByName.remove(existingName)
        self.mStampsByName.insert(stamp.name(), stamp)
        existingFileName = stamp.fileName()
        newFileName = findStampFileName(stamp.name(), existingFileName)
        if (existingFileName != newFileName):
            if (QFile.rename(stampFilePath(existingFileName),
                             stampFilePath(newFileName))):
                stamp.setFileName(newFileName)

    def saveStamp(self, stamp):
        # make sure we have a stamps directory
        prefs = preferences.Preferences.instance()
        stampsDirectory = prefs.stampsDirectory()
        stampsDir = QDir(stampsDirectory)
        if (not stampsDir.exists() and not stampsDir.mkpath(".")):
            qDebug("Failed to create stamps directory" + stampsDirectory)
            return

        filePath = stampsDir.filePath(stamp.fileName())
        file = QSaveFile(filePath)
        if (not file.open(QIODevice.WriteOnly)):
            qDebug("Failed to open stamp file for writing" + filePath)
            return

        stampJson = stamp.toJson(QFileInfo(filePath).dir())
        file.write(QJsonDocument(stampJson).toJson(QJsonDocument.Compact))
        if (not file.commit()):
            qDebug() << "Failed to write stamp" << filePath

    def deleteStamp(self, stamp):
        self.mStampsByName.remove(stamp.name())
        QFile.remove(stampFilePath(stamp.fileName()))
Esempio n. 33
0
    def drawGrid(self, painter, exposed, gridColor):
        rect = exposed.toAlignedRect()
        if (rect.isNull()):
            return
        p = RenderParams(self.map())
        # Determine the tile and pixel coordinates to start at
        startTile = self.screenToTileCoords_(rect.topLeft()).toPoint()
        startPos = self.tileToScreenCoords_(startTile).toPoint()
        ## Determine in which half of the tile the top-left corner of the area we
        # need to draw is. If we're in the upper half, we need to start one row
        # up due to those tiles being visible as well. How we go up one row
        # depends on whether we're in the left or right half of the tile.
        ##
        inUpperHalf = rect.y() - startPos.y() < p.sideOffsetY
        inLeftHalf = rect.x() - startPos.x() < p.sideOffsetX
        if (inUpperHalf):
            startTile.setY(startTile.y() - 1)
        if (inLeftHalf):
            startTile.setX(startTile.x() - 1)
        startTile.setX(max(0, startTile.x()))
        startTile.setY(max(0, startTile.y()))
        startPos = self.tileToScreenCoords_(startTile).toPoint()
        oct = [
            QPoint(0, p.tileHeight - p.sideOffsetY),
            QPoint(0, p.sideOffsetY),
            QPoint(p.sideOffsetX, 0),
            QPoint(p.tileWidth - p.sideOffsetX, 0),
            QPoint(p.tileWidth, p.sideOffsetY),
            QPoint(p.tileWidth, p.tileHeight - p.sideOffsetY),
            QPoint(p.tileWidth - p.sideOffsetX, p.tileHeight),
            QPoint(p.sideOffsetX, p.tileHeight)
        ]

        lines = QVector()
        #lines.reserve(8)
        gridColor.setAlpha(128)
        gridPen = QPen(gridColor)
        gridPen.setCosmetic(True)
        _x = QVector()
        _x.append(2)
        _x.append(2)
        gridPen.setDashPattern(_x)
        painter.setPen(gridPen)
        if (p.staggerX):
            # Odd row shifting is applied in the rendering loop, so un-apply it here
            if (p.doStaggerX(startTile.x())):
                startPos.setY(startPos.y() - p.rowHeight)
            while (startPos.x() <= rect.right()
                   and startTile.x() < self.map().width()):
                rowTile = QPoint(startTile)
                rowPos = QPoint(startPos)
                if (p.doStaggerX(startTile.x())):
                    rowPos.setY(rowPos.y() + p.rowHeight)
                while (rowPos.y() <= rect.bottom()
                       and rowTile.y() < self.map().height()):
                    lines.append(QLineF(rowPos + oct[1], rowPos + oct[2]))
                    lines.append(QLineF(rowPos + oct[2], rowPos + oct[3]))
                    lines.append(QLineF(rowPos + oct[3], rowPos + oct[4]))
                    isStaggered = p.doStaggerX(startTile.x())
                    lastRow = rowTile.y() == self.map().height() - 1
                    lastColumn = rowTile.x() == self.map().width() - 1
                    bottomLeft = rowTile.x() == 0 or (lastRow and isStaggered)
                    bottomRight = lastColumn or (lastRow and isStaggered)
                    if (bottomRight):
                        lines.append(QLineF(rowPos + oct[5], rowPos + oct[6]))
                    if (lastRow):
                        lines.append(QLineF(rowPos + oct[6], rowPos + oct[7]))
                    if (bottomLeft):
                        lines.append(QLineF(rowPos + oct[7], rowPos + oct[0]))
                    painter.drawLines(lines)
                    lines.resize(0)
                    rowPos.setY(rowPos.y() + p.tileHeight + p.sideLengthY)
                    rowTile.setY(rowTile.y() + 1)

                startPos.setX(startPos.x() + p.columnWidth)
                startTile.setX(startTile.x() + 1)
        else:
            # Odd row shifting is applied in the rendering loop, so un-apply it here
            if (p.doStaggerY(startTile.y())):
                startPos.setX(startPos.x() - p.columnWidth)
            while (startPos.y() <= rect.bottom()
                   and startTile.y() < self.map().height()):
                rowTile = QPoint(startTile)
                rowPos = QPoint(startPos)
                if (p.doStaggerY(startTile.y())):
                    rowPos.setX(rowPos.x() + p.columnWidth)
                while (rowPos.x() <= rect.right()
                       and rowTile.x() < self.map().width()):
                    lines.append(QLineF(rowPos + oct[0], rowPos + oct[1]))
                    lines.append(QLineF(rowPos + oct[1], rowPos + oct[2]))
                    lines.append(QLineF(rowPos + oct[3], rowPos + oct[4]))
                    isStaggered = p.doStaggerY(startTile.y())
                    lastRow = rowTile.y() == self.map().height() - 1
                    lastColumn = rowTile.x() == self.map().width() - 1
                    bottomLeft = lastRow or (rowTile.x() == 0
                                             and not isStaggered)
                    bottomRight = lastRow or (lastColumn and isStaggered)
                    if (lastColumn):
                        lines.append(QLineF(rowPos + oct[4], rowPos + oct[5]))
                    if (bottomRight):
                        lines.append(QLineF(rowPos + oct[5], rowPos + oct[6]))
                    if (bottomLeft):
                        lines.append(QLineF(rowPos + oct[7], rowPos + oct[0]))
                    painter.drawLines(lines)
                    lines.resize(0)
                    rowPos.setX(rowPos.x() + p.tileWidth + p.sideLengthX)
                    rowTile.setX(rowTile.x() + 1)

                startPos.setY(startPos.y() + p.rowHeight)
                startTile.setY(startTile.y() + 1)
Esempio n. 34
0
class FrameListModel(QAbstractListModel):
    DEFAULT_DURATION = 100

    def __init__(self, parent):
        super().__init__(parent)

        self.mFrames = QVector()
        self.mTileset = None

    def rowCount(self, parent):
        if parent.isValid():
            _x = 0
        else:
            _x = self.mFrames.size()
        return _x

    def data(self, index, role):
        x = role
        if x == Qt.EditRole or x == Qt.DisplayRole:
            return self.mFrames.at(index.row()).duration
        elif x == Qt.DecorationRole:
            tileId = self.mFrames.at(index.row()).tileId
            tile = self.mTileset.tileAt(tileId)
            if tile:
                return tile.image()

        return QVariant()

    def setData(self, index, value, role):
        if (role == Qt.EditRole):
            duration = value
            if (duration >= 0):
                self.mFrames[index.row()].duration = duration
                self.dataChanged.emit(index, index)
                return True

        return False

    def flags(self, index):
        defaultFlags = super().flags(index)
        if (index.isValid()):
            return Qt.ItemIsDragEnabled | Qt.ItemIsEditable | defaultFlags
        else:
            return Qt.ItemIsDropEnabled | defaultFlags

    def removeRows(self, row, count, parent):
        if (not parent.isValid()):
            if (count > 0):
                self.beginRemoveRows(parent, row, row + count - 1)
                self.mFrames.remove(row, count)
                self.endRemoveRows()

            return True

        return False

    def mimeTypes(self):
        types = QStringList()
        types.append(TILES_MIMETYPE)
        types.append(FRAMES_MIMETYPE)
        return types

    def mimeData(self, indexes):
        mimeData = QMimeData()
        encodedData = QByteArray()
        stream = QDataStream(encodedData, QIODevice.WriteOnly)
        for index in indexes:
            if (index.isValid()):
                frame = self.mFrames.at(index.row())
                stream.writeInt(frame.tileId)
                stream.writeInt(frame.duration)

        mimeData.setData(FRAMES_MIMETYPE, encodedData)
        return mimeData

    def dropMimeData(self, data, action, row, column, parent):
        if (action == Qt.IgnoreAction):
            return True
        if (column > 0):
            return False
        beginRow = 0
        if (row != -1):
            beginRow = row
        elif parent.isValid():
            beginRow = parent.row()
        else:
            beginRow = self.mFrames.size()
        newFrames = QVector()
        if (data.hasFormat(FRAMES_MIMETYPE)):
            encodedData = data.data(FRAMES_MIMETYPE)
            stream = QDataStream(encodedData, QIODevice.ReadOnly)
            while (not stream.atEnd()):
                frame = Frame()
                frame.tileId = stream.readInt()
                frame.duration = stream.readInt()
                newFrames.append(frame)
        elif (data.hasFormat(TILES_MIMETYPE)):
            encodedData = data.data(TILES_MIMETYPE)
            stream = QDataStream(encodedData, QIODevice.ReadOnly)
            while (not stream.atEnd()):
                frame = Frame()
                frame.tileId = stream.readInt()
                frame.duration = FrameListModel.DEFAULT_DURATION
                newFrames.append(frame)

        if (newFrames.isEmpty()):
            return False
        self.beginInsertRows(QModelIndex(), beginRow,
                             beginRow + newFrames.size() - 1)
        self.mFrames.insert(beginRow, newFrames.size(), Frame())
        for i in range(newFrames.size()):
            self.mFrames[i + beginRow] = newFrames[i]
        self.endInsertRows()
        return True

    def supportedDropActions(self):
        return Qt.CopyAction | Qt.MoveAction

    def setFrames(self, tileset, frames):
        self.beginResetModel()
        self.mTileset = tileset
        self.mFrames = frames
        self.endResetModel()

    def addTileIdAsFrame(self, id):
        frame = Frame()
        frame.tileId = id
        frame.duration = FrameListModel.DEFAULT_DURATION
        self.addFrame(frame)

    def frames(self):
        return self.mFrames

    def addFrame(self, frame):
        self.beginInsertRows(QModelIndex(), self.mFrames.size(),
                             self.mFrames.size())
        self.mFrames.append(frame)
        self.endInsertRows()
Esempio n. 35
0
class CellRenderer():
    BottomLeft, BottomCenter, TopLeft = range(3)

    def __init__(self, painter):
        self.mPainter = painter
        self.mTile = None
        self.mIsOpenGL = hasOpenGLEngine(painter)

        self.mFragments = QVector()

    def __del__(self):
        self.flush()

    ##
    # Renders a \a cell with the given \a origin at \a pos, taking into account
    # the flipping and tile offset.
    #
    # For performance reasons, the actual drawing is delayed until a different
    # kind of tile has to be drawn. For this reason it is necessary to call
    # flush when finished doing drawCell calls. This function is also called by
    # the destructor so usually an explicit call is not needed.
    ##
    def render(self, cell, pos, cellSize, origin):
        if (self.mTile != cell.tile):
            self.flush()
        image = cell.tile.currentFrameImage()
        size = image.size()
        if cellSize == QSizeF(0,0):
            objectSize = size
        else:
            objectSize = cellSize
        scale = QSizeF(objectSize.width() / size.width(), objectSize.height() / size.height())
        offset = cell.tile.offset()
        sizeHalf = QPointF(objectSize.width() / 2, objectSize.height() / 2)
        fragment = QPainter.PixmapFragment()
        fragment.x = pos.x() + (offset.x() * scale.width()) + sizeHalf.x()
        fragment.y = pos.y() + (offset.y() * scale.height()) + sizeHalf.y() - objectSize.height()
        fragment.sourceLeft = 0
        fragment.sourceTop = 0
        fragment.width = size.width()
        fragment.height = size.height()
        
        if cell.flippedHorizontally:
            fragment.scaleX = -1
        else:
            fragment.scaleX = 1
        if cell.flippedVertically:
            fragment.scaleY = -1
        else:
            fragment.scaleY = 1

        fragment.rotation = 0
        fragment.opacity = 1
        flippedHorizontally = cell.flippedHorizontally
        flippedVertically = cell.flippedVertically
        if (origin == CellRenderer.BottomCenter):
            fragment.x -= sizeHalf.x()
        if (cell.flippedAntiDiagonally):
            fragment.rotation = 90
            flippedHorizontally = cell.flippedVertically
            flippedVertically = not cell.flippedHorizontally
            # Compensate for the swap of image dimensions
            halfDiff = sizeHalf.y() - sizeHalf.x()
            fragment.y += halfDiff
            if (origin != CellRenderer.BottomCenter):
                fragment.x += halfDiff

        if flippedHorizontally:
            x = -1
        else:
            x = 1
        fragment.scaleX = scale.width() * x

        if flippedVertically:
            x = -1
        else:
            x = 1
        fragment.scaleY = scale.height() * x
        if (self.mIsOpenGL or (fragment.scaleX > 0 and fragment.scaleY > 0)):
            self.mTile = cell.tile
            self.mFragments.append(fragment)
            return

        # The Raster paint engine as of Qt 4.8.4 / 5.0.2 does not support
        # drawing fragments with a negative scaling factor.
        self.flush() # make sure we drew all tiles so far
        oldTransform = self.mPainter.transform()
        transform = oldTransform
        transform.translate(fragment.x, fragment.y)
        transform.rotate(fragment.rotation)
        transform.scale(fragment.scaleX, fragment.scaleY)
        target = QRectF(fragment.width * -0.5, fragment.height * -0.5, fragment.width, fragment.height)
        source = QRectF(0, 0, fragment.width, fragment.height)
        self.mPainter.setTransform(transform)
        self.mPainter.drawPixmap(target, image, source)
        self.mPainter.setTransform(oldTransform)

    def flush(self):
        if (not self.mTile):
            return
        self.mPainter.drawPixmapFragments(self.mFragments, self.mTile.currentFrameImage())
        self.mTile = None
        self.mFragments.resize(0)
Esempio n. 36
0
class Preferences(QObject):
    showGridChanged = pyqtSignal(bool)
    showTileObjectOutlinesChanged = pyqtSignal(bool)
    showTileAnimationsChanged = pyqtSignal(bool)
    snapToGridChanged = pyqtSignal(bool)
    snapToFineGridChanged = pyqtSignal(bool)
    gridColorChanged = pyqtSignal(QColor)
    gridFineChanged = pyqtSignal(int)
    objectLineWidthChanged = pyqtSignal(float)
    highlightCurrentLayerChanged = pyqtSignal(bool)
    showTilesetGridChanged = pyqtSignal(bool)
    objectLabelVisibilityChanged = pyqtSignal(int)
    useOpenGLChanged = pyqtSignal(bool)
    objectTypesChanged = pyqtSignal()
    mapsDirectoryChanged = pyqtSignal()
    stampsDirectoryChanged = pyqtSignal(str)
    isPatronChanged = pyqtSignal()

    mInstance = None

    ObjectTypesFile, ImageFile, ExportedFile, ExternalTileset = range(4)

    def __init__(self):
        super().__init__()

        self.mSettings = QSettings(self)

        self.mObjectTypes = QVector()

        # Retrieve storage settings
        self.mSettings.beginGroup("Storage")
        self.mLayerDataFormat = Map.LayerDataFormat(self.intValue("LayerDataFormat", Map.LayerDataFormat.Base64Zlib.value))
        self.mMapRenderOrder = Map.RenderOrder(self.intValue("MapRenderOrder", Map.RenderOrder.RightDown.value))
        self.mDtdEnabled = self.boolValue("DtdEnabled")
        self.mReloadTilesetsOnChange = self.boolValue("ReloadTilesets", True)
        self.mStampsDirectory = self.stringValue("StampsDirectory")
        self.mSettings.endGroup()
        # Retrieve interface settings
        self.mSettings.beginGroup("Interface")
        self.mShowGrid = self.boolValue("ShowGrid")
        self.mShowTileObjectOutlines = self.boolValue("ShowTileObjectOutlines")
        self.mShowTileAnimations = self.boolValue("ShowTileAnimations", True)
        self.mSnapToGrid = self.boolValue("SnapToGrid")
        self.mSnapToFineGrid = self.boolValue("SnapToFineGrid")
        self.mGridColor = self.colorValue("GridColor", Qt.black)
        self.mGridFine = self.intValue("GridFine", 4)
        self.mObjectLineWidth = self.realValue("ObjectLineWidth", 2)
        self.mHighlightCurrentLayer = self.boolValue("HighlightCurrentLayer")
        self.mShowTilesetGrid = self.boolValue("ShowTilesetGrid", True)
        self.mLanguage = self.stringValue("Language")
        self.mUseOpenGL = self.boolValue("OpenGL")
        self.mObjectLabelVisibility = self.intValue("ObjectLabelVisibility", ObjectLabelVisiblity.AllObjectLabels)
        self.mSettings.endGroup()
        # Retrieve defined object types
        self.mSettings.beginGroup("ObjectTypes")
        names = self.mSettings.value("Names", QStringList())
        colors = self.mSettings.value("Colors", QStringList())
        self.mSettings.endGroup()
        count = min(len(names), len(colors))
        for i in range(count):
            self.mObjectTypes.append(ObjectType(names[i], QColor(colors[i])))
        self.mSettings.beginGroup("Automapping")
        self.mAutoMapDrawing = self.boolValue("WhileDrawing")
        self.mSettings.endGroup()
        self.mSettings.beginGroup("MapsDirectory")
        self.mMapsDirectory = self.stringValue("Current")
        self.mSettings.endGroup()
        tilesetManager = TilesetManager.instance()
        tilesetManager.setReloadTilesetsOnChange(self.mReloadTilesetsOnChange)
        tilesetManager.setAnimateTiles(self.mShowTileAnimations)
        # Keeping track of some usage information
        self.mSettings.beginGroup("Install")
        self.mFirstRun = QDate.fromString(self.mSettings.value("FirstRun"))
        self.mRunCount = self.intValue("RunCount", 0) + 1
        self.mIsPatron = self.boolValue("IsPatron")
        if (not self.mFirstRun.isValid()):
            self.mFirstRun = QDate.currentDate()
            self.mSettings.setValue("FirstRun", self.mFirstRun.toString(Qt.ISODate))

        self.mSettings.setValue("RunCount", self.mRunCount)
        self.mSettings.endGroup()
        
        # Retrieve startup settings
        self.mSettings.beginGroup("Startup")
        self.mOpenLastFilesOnStartup = self.boolValue("OpenLastFiles", True)
        self.mSettings.endGroup()

    def __del__(self):
        pass

    def setObjectLabelVisibility(self, visibility):
        if self.mObjectLabelVisibility == visibility:
            return

        self.mObjectLabelVisibility = visibility
        self.mSettings.setValue("Interface/ObjectLabelVisibility", visibility)
        self.objectLabelVisibilityChanged.emit(visibility)
        
    def instance():
        if (not Preferences.mInstance):
            Preferences.mInstance = Preferences()
        return Preferences.mInstance

    def deleteInstance():
        del Preferences.mInstance
        Preferences.mInstance = None

    def showGrid(self):
        return self.mShowGrid

    def showTileObjectOutlines(self):
        return self.mShowTileObjectOutlines

    def showTileAnimations(self):
        return self.mShowTileAnimations

    def snapToGrid(self):
        return self.mSnapToGrid

    def snapToFineGrid(self):
        return self.mSnapToFineGrid

    def gridColor(self):
        return self.mGridColor

    def gridFine(self):
        return self.mGridFine

    def objectLineWidth(self):
        return self.mObjectLineWidth

    def highlightCurrentLayer(self):
        return self.mHighlightCurrentLayer

    def showTilesetGrid(self):
        return self.mShowTilesetGrid

    def useOpenGL(self):
        return self.mUseOpenGL

    def objectTypes(self):
        return self.mObjectTypes

    def automappingDrawing(self):
        return self.mAutoMapDrawing

    ##
    # Provides access to the QSettings instance to allow storing/retrieving
    # arbitrary values. The naming style for groups and keys is CamelCase.
    ##
    def settings(self):
        return self.mSettings

    def layerDataFormat(self):
        return self.mLayerDataFormat

    def setLayerDataFormat(self, layerDataFormat):
        if (self.mLayerDataFormat == layerDataFormat):
            return
        self.mLayerDataFormat = layerDataFormat
        self.mSettings.setValue("Storage/LayerDataFormat",
                            self.mLayerDataFormat)

    def mapRenderOrder(self):
        return self.mMapRenderOrder

    def setMapRenderOrder(self, mapRenderOrder):
        if (self.mMapRenderOrder == mapRenderOrder):
            return
        self.mMapRenderOrder = mapRenderOrder
        self.mSettings.setValue("Storage/MapRenderOrder",
                            self.mMapRenderOrder)

    def dtdEnabled(self):
        return self.mDtdEnabled

    def setDtdEnabled(self, enabled):
        self.mDtdEnabled = enabled
        self.mSettings.setValue("Storage/DtdEnabled", enabled)

    def language(self):
        return self.mLanguage

    def setLanguage(self, language):
        if (self.mLanguage == language):
            return
        self.mLanguage = language
        self.mSettings.setValue("Interface/Language", self.mLanguage)
        languagemanager.LanguageManager.instance().installTranslators()

    def reloadTilesetsOnChange(self):
        return self.mReloadTilesetsOnChange

    def setReloadTilesetsOnChanged(self, value):
        if (self.mReloadTilesetsOnChange == value):
            return
        self.mReloadTilesetsOnChange = value
        self.mSettings.setValue("Storage/ReloadTilesets",
                            self.mReloadTilesetsOnChange)
        tilesetManager = TilesetManager.instance()
        tilesetManager.setReloadTilesetsOnChange(self.mReloadTilesetsOnChange)

    def setUseOpenGL(self, useOpenGL):
        if (self.mUseOpenGL == useOpenGL):
            return
        self.mUseOpenGL = useOpenGL
        self.mSettings.setValue("Interface/OpenGL", self.mUseOpenGL)
        self.useOpenGLChanged.emit(self.mUseOpenGL)

    def setObjectTypes(self, objectTypes):
        self.mObjectTypes = objectTypes
        names = QStringList()
        colors = QStringList()
        for objectType in objectTypes:
            names.append(objectType.name)
            colors.append(objectType.color.name())

        self.mSettings.beginGroup("ObjectTypes")
        self.mSettings.setValue("Names", names)
        self.mSettings.setValue("Colors", colors)
        self.mSettings.endGroup()
        self.objectTypesChanged.emit()

    def lastPath(self, fileType):
        path = self.mSettings.value(lastPathKey(fileType))
        if path==None or path=='':
            documentManager = DocumentManager.instance()
            mapDocument = documentManager.currentDocument()
            if mapDocument:
                path = QFileInfo(mapDocument.fileName()).path()

        if path==None or path=='':
            path = QStandardPaths.writableLocation(QStandardPaths.DocumentsLocation)

        return path

    ##
    # \see lastPath()
    ##
    def setLastPath(self, fileType, path):
        self.mSettings.setValue(lastPathKey(fileType), path)

    def setAutomappingDrawing(self, enabled):
        self.mAutoMapDrawing = enabled
        self.mSettings.setValue("Automapping/WhileDrawing", enabled)

    def mapsDirectory(self):
        return self.mMapsDirectory

    def setMapsDirectory(self, path):
        if (self.mMapsDirectory == path):
            return
        self.mMapsDirectory = path
        self.mSettings.setValue("MapsDirectory/Current", path)
        self.mapsDirectoryChanged.emit()

    def objectLabelVisibility(self):
        return self.mObjectLabelVisibility

    def firstRun(self):
        return self.mFirstRun

    def runCount(self):
        return self.mRunCount

    def isPatron(self):
        return self.mIsPatron

    def openLastFilesOnStartup(self):
        return self.mOpenLastFilesOnStartup
        
    def setPatron(self, isPatron):
        if (self.mIsPatron == isPatron):
            return
        self.mIsPatron = isPatron
        self.mSettings.setValue("Install/IsPatron", isPatron)
        self.isPatronChanged.emit()

    def setShowGrid(self, showGrid):
        if (self.mShowGrid == showGrid):
            return
        self.mShowGrid = showGrid
        self.mSettings.setValue("Interface/ShowGrid", self.mShowGrid)
        self.showGridChanged.emit(self.mShowGrid)

    def setShowTileObjectOutlines(self, enabled):
        if (self.mShowTileObjectOutlines == enabled):
            return
        self.mShowTileObjectOutlines = enabled
        self.mSettings.setValue("Interface/ShowTileObjectOutlines",
                            self.mShowTileObjectOutlines)
        self.showTileObjectOutlinesChanged.emit(self.mShowTileObjectOutlines)

    def setShowTileAnimations(self, enabled):
        if (self.mShowTileAnimations == enabled):
            return
        self.mShowTileAnimations = enabled
        self.mSettings.setValue("Interface/ShowTileAnimations",
                            self.mShowTileAnimations)
        tilesetManager = TilesetManager.instance()
        tilesetManager.setAnimateTiles(self.mShowTileAnimations)
        self.showTileAnimationsChanged.emit(self.mShowTileAnimations)

    def setSnapToGrid(self, snapToGrid):
        if (self.mSnapToGrid == snapToGrid):
            return
        self.mSnapToGrid = snapToGrid
        self.mSettings.setValue("Interface/SnapToGrid", self.mSnapToGrid)
        self.snapToGridChanged.emit(self.mSnapToGrid)

    def setSnapToFineGrid(self, snapToFineGrid):
        if (self.mSnapToFineGrid == snapToFineGrid):
            return
        self.mSnapToFineGrid = snapToFineGrid
        self.mSettings.setValue("Interface/SnapToFineGrid", self.mSnapToFineGrid)
        self.snapToFineGridChanged.emit(self.mSnapToFineGrid)

    def setGridColor(self, gridColor):
        if (self.mGridColor == gridColor):
            return
        self.mGridColor = gridColor
        self.mSettings.setValue("Interface/GridColor", self.mGridColor.name())
        self.gridColorChanged.emit(self.mGridColor)

    def setGridFine(self, gridFine):
        if (self.mGridFine == gridFine):
            return
        self.mGridFine = gridFine
        self.mSettings.setValue("Interface/GridFine", self.mGridFine)
        self.gridFineChanged.emit(self.mGridFine)

    def setObjectLineWidth(self, lineWidth):
        if (self.mObjectLineWidth == lineWidth):
            return
        self.mObjectLineWidth = lineWidth
        self.mSettings.setValue("Interface/ObjectLineWidth", self.mObjectLineWidth)
        self.objectLineWidthChanged.emit(self.mObjectLineWidth)

    def setHighlightCurrentLayer(self, highlight):
        if (self.mHighlightCurrentLayer == highlight):
            return
        self.mHighlightCurrentLayer = highlight
        self.mSettings.setValue("Interface/HighlightCurrentLayer",
                            self.mHighlightCurrentLayer)
        self.highlightCurrentLayerChanged.emit(self.mHighlightCurrentLayer)

    def setShowTilesetGrid(self, showTilesetGrid):
        if (self.mShowTilesetGrid == showTilesetGrid):
            return
        self.mShowTilesetGrid = showTilesetGrid
        self.mSettings.setValue("Interface/ShowTilesetGrid",
                            self.mShowTilesetGrid)
        self.showTilesetGridChanged.emit(self.mShowTilesetGrid)

    def setOpenLastFilesOnStartup(self, open):
        if self.mOpenLastFilesOnStartup == open:
            return

        self.mOpenLastFilesOnStartup = open
        self.mSettings.setValue("Startup/OpenLastFiles", open)

    def boolValue(self, key, defaultValue = False):
        b = self.mSettings.value(key, defaultValue)
        tp = type(b)
        if tp==bool:
            return b
        elif tp==str:
            return b.lower()=='true'
        return bool(b)

    def colorValue(self, key, default = QColor()):
        if type(default) != QColor:
            default = QColor(default)
        name = self.mSettings.value(key, default.name())
        if (not QColor.isValidColor(name)):
            return QColor()
        return QColor(name)

    def stringValue(self, key, default = ''):
        return self.mSettings.value(key, default)

    def intValue(self, key, defaultValue):
        return Int(self.mSettings.value(key, defaultValue))

    def realValue(self, key, defaultValue):
        return Float(self.mSettings.value(key, defaultValue))

    def stampsDirectory(self):
        if self.mStampsDirectory == '':
            appData = QStandardPaths.writableLocation(QStandardPaths.AppDataLocation)
            return appData + "/stamps"
        return self.mStampsDirectory

    def setStampsDirectory(self, stampsDirectory):
        if self.mStampsDirectory == stampsDirectory:
            return

        self.mStampsDirectory = stampsDirectory
        self.mSettings.setValue("Storage/StampsDirectory", stampsDirectory)

        self.stampsDirectoryChanged.emit(stampsDirectory)
Esempio n. 37
0
class TileLayer(Layer):
    ##
    # Constructor.
    ##
    def __init__(self, name, x, y, width, height):
        super().__init__(Layer.TileLayerType, name, x, y, width, height)
        self.mMaxTileSize = QSize(0, 0)
        self.mGrid = QVector()
        for i in range(width * height):
            self.mGrid.append(Cell())
        self.mOffsetMargins = QMargins()

    def __iter__(self):
        return self.mGrid.__iter__()

    ##
    # Returns the maximum tile size of this layer.
    ##
    def maxTileSize(self):
        return self.mMaxTileSize

    ##
    # Returns the margins that have to be taken into account while drawing
    # this tile layer. The margins depend on the maximum tile size and the
    # offset applied to the tiles.
    ##
    def drawMargins(self):
        return QMargins(
            self.mOffsetMargins.left(),
            self.mOffsetMargins.top() + self.mMaxTileSize.height(),
            self.mOffsetMargins.right() + self.mMaxTileSize.width(),
            self.mOffsetMargins.bottom())

    ##
    # Recomputes the draw margins. Needed after the tile offset of a tileset
    # has changed for example.
    #
    # Generally you want to call Map.recomputeDrawMargins instead.
    ##
    def recomputeDrawMargins(self):
        maxTileSize = QSize(0, 0)
        offsetMargins = QMargins()
        i = 0
        while (i < self.mGrid.size()):
            cell = self.mGrid.at(i)
            tile = cell.tile
            if tile:
                size = tile.size()
                if (cell.flippedAntiDiagonally):
                    size.transpose()
                offset = tile.offset()
                maxTileSize = maxSize(size, maxTileSize)
                offsetMargins = maxMargins(
                    QMargins(-offset.x(), -offset.y(), offset.x(), offset.y()),
                    offsetMargins)
            i += 1

        self.mMaxTileSize = maxTileSize
        self.mOffsetMargins = offsetMargins
        if (self.mMap):
            self.mMap.adjustDrawMargins(self.drawMargins())

    ##
    # Returns whether (x, y) is inside this map layer.
    ##
    def contains(self, *args):
        l = len(args)
        if l == 2:
            x, y = args
            return x >= 0 and y >= 0 and x < self.mWidth and y < self.mHeight
        elif l == 1:
            point = args[0]
            return self.contains(point.x(), point.y())

    ##
    # Calculates the region of cells in this tile layer for which the given
    # \a condition returns True.
    ##
    def region(self, *args):
        l = len(args)
        if l == 1:
            condition = args[0]
            region = QRegion()
            for y in range(self.mHeight):
                for x in range(self.mWidth):
                    if (condition(self.cellAt(x, y))):
                        rangeStart = x
                        x += 1
                        while (x <= self.mWidth):
                            if (x == self.mWidth
                                    or not condition(self.cellAt(x, y))):
                                rangeEnd = x
                                region += QRect(rangeStart + self.mX,
                                                y + self.mY,
                                                rangeEnd - rangeStart, 1)
                                break
                            x += 1

            return region
        elif l == 0:
            ##
            # Calculates the region occupied by the tiles of this layer. Similar to
            # Layer.bounds(), but leaves out the regions without tiles.
            ##
            return self.region(lambda cell: not cell.isEmpty())

    ##
    # Returns a read-only reference to the cell at the given coordinates. The
    # coordinates have to be within this layer.
    ##
    def cellAt(self, *args):
        l = len(args)
        if l == 2:
            x, y = args
            return self.mGrid.at(x + y * self.mWidth)
        elif l == 1:
            point = args[0]
            return self.cellAt(point.x(), point.y())

    ##
    # Sets the cell at the given coordinates.
    ##
    def setCell(self, x, y, cell):
        if (cell.tile):
            size = cell.tile.size()
            if (cell.flippedAntiDiagonally):
                size.transpose()
            offset = cell.tile.offset()
            self.mMaxTileSize = maxSize(size, self.mMaxTileSize)
            self.mOffsetMargins = maxMargins(
                QMargins(-offset.x(), -offset.y(), offset.x(), offset.y()),
                self.mOffsetMargins)
            if (self.mMap):
                self.mMap.adjustDrawMargins(self.drawMargins())

        self.mGrid[x + y * self.mWidth] = cell

    ##
    # Returns a copy of the area specified by the given \a region. The
    # caller is responsible for the returned tile layer.
    ##
    def copy(self, *args):
        l = len(args)
        if l == 1:
            region = args[0]
            if type(region) != QRegion:
                region = QRegion(region)
            area = region.intersected(QRect(0, 0, self.width(), self.height()))
            bounds = region.boundingRect()
            areaBounds = area.boundingRect()
            offsetX = max(0, areaBounds.x() - bounds.x())
            offsetY = max(0, areaBounds.y() - bounds.y())
            copied = TileLayer(QString(), 0, 0, bounds.width(),
                               bounds.height())
            for rect in area.rects():
                for x in range(rect.left(), rect.right() + 1):
                    for y in range(rect.top(), rect.bottom() + 1):
                        copied.setCell(x - areaBounds.x() + offsetX,
                                       y - areaBounds.y() + offsetY,
                                       self.cellAt(x, y))
            return copied
        elif l == 4:
            x, y, width, height = args

            return self.copy(QRegion(x, y, width, height))

    ##
    # Merges the given \a layer onto this layer at position \a pos. Parts that
    # fall outside of this layer will be lost and empty tiles in the given
    # layer will have no effect.
    ##
    def merge(self, pos, layer):
        # Determine the overlapping area
        area = QRect(pos, QSize(layer.width(), layer.height()))
        area &= QRect(0, 0, self.width(), self.height())
        for y in range(area.top(), area.bottom() + 1):
            for x in range(area.left(), area.right() + 1):
                cell = layer.cellAt(x - pos.x(), y - pos.y())
                if (not cell.isEmpty()):
                    self.setCell(x, y, cell)

    ##
    # Removes all cells in the specified region.
    ##
    def erase(self, area):
        emptyCell = Cell()
        for rect in area.rects():
            for x in range(rect.left(), rect.right() + 1):
                for y in range(rect.top(), rect.bottom() + 1):
                    self.setCell(x, y, emptyCell)

    ##
    # Sets the cells starting at the given position to the cells in the given
    # \a tileLayer. Parts that fall outside of this layer will be ignored.
    #
    # When a \a mask is given, only cells that fall within this mask are set.
    # The mask is applied in local coordinates.
    ##
    def setCells(self, x, y, layer, mask=QRegion()):
        # Determine the overlapping area
        area = QRegion(QRect(x, y, layer.width(), layer.height()))
        area &= QRect(0, 0, self.width(), self.height())
        if (not mask.isEmpty()):
            area &= mask
        for rect in area.rects():
            for _x in range(rect.left(), rect.right() + 1):
                for _y in range(rect.top(), rect.bottom() + 1):
                    self.setCell(_x, _y, layer.cellAt(_x - x, _y - y))

    ##
    # Flip this tile layer in the given \a direction. Direction must be
    # horizontal or vertical. This doesn't change the dimensions of the
    # tile layer.
    ##
    def flip(self, direction):
        newGrid = QVector()
        for i in range(self.mWidth * self.mHeight):
            newGrid.append(Cell())
        for y in range(self.mHeight):
            for x in range(self.mWidth):
                dest = newGrid[x + y * self.mWidth]
                if (direction == FlipDirection.FlipHorizontally):
                    source = self.cellAt(self.mWidth - x - 1, y)
                    dest = source
                    dest.flippedHorizontally = not source.flippedHorizontally
                elif (direction == FlipDirection.FlipVertically):
                    source = self.cellAt(x, self.mHeight - y - 1)
                    dest = source
                    dest.flippedVertically = not source.flippedVertically

        self.mGrid = newGrid

    ##
    # Rotate this tile layer by 90 degrees left or right. The tile positions
    # are rotated within the layer, and the tiles themselves are rotated. The
    # dimensions of the tile layer are swapped.
    ##
    def rotate(self, direction):
        rotateRightMask = [5, 4, 1, 0, 7, 6, 3, 2]
        rotateLeftMask = [3, 2, 7, 6, 1, 0, 5, 4]
        if direction == RotateDirection.RotateRight:
            rotateMask = rotateRightMask
        else:
            rotateMask = rotateLeftMask
        newWidth = self.mHeight
        newHeight = self.mWidth
        newGrid = QVector(newWidth * newHeight)
        for y in range(self.mHeight):
            for x in range(self.mWidth):
                source = self.cellAt(x, y)
                dest = source
                mask = (dest.flippedHorizontally << 2) | (
                    dest.flippedVertically << 1) | (
                        dest.flippedAntiDiagonally << 0)
                mask = rotateMask[mask]
                dest.flippedHorizontally = (mask & 4) != 0
                dest.flippedVertically = (mask & 2) != 0
                dest.flippedAntiDiagonally = (mask & 1) != 0
                if (direction == RotateDirection.RotateRight):
                    newGrid[x * newWidth + (self.mHeight - y - 1)] = dest
                else:
                    newGrid[(self.mWidth - x - 1) * newWidth + y] = dest

        t = self.mMaxTileSize.width()
        self.mMaxTileSize.setWidth(self.mMaxTileSize.height())
        self.mMaxTileSize.setHeight(t)
        self.mWidth = newWidth
        self.mHeight = newHeight
        self.mGrid = newGrid

    ##
    # Computes and returns the set of tilesets used by this tile layer.
    ##
    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

    ##
    # Returns whether this tile layer has any cell for which the given
    # \a condition returns True.
    ##
    def hasCell(self, condition):
        i = 0
        for cell in self.mGrid:
            if (condition(cell)):
                return True
            i += 1
        return False

    ##
    # Returns whether this tile layer is referencing the given tileset.
    ##
    def referencesTileset(self, tileset):
        i = 0
        while (i < self.mGrid.size()):
            tile = self.mGrid.at(i).tile
            if (tile and tile.tileset() == tileset):
                return True
            i += 1
        return False

    ##
    # Removes all references to the given tileset. This sets all tiles on this
    # layer that are from the given tileset to null.
    ##
    def removeReferencesToTileset(self, tileset):
        i = 0
        while (i < self.mGrid.size()):
            tile = self.mGrid.at(i).tile
            if (tile and tile.tileset() == tileset):
                self.mGrid.replace(i, Cell())
            i += 1

    ##
    # Replaces all tiles from \a oldTileset with tiles from \a newTileset.
    ##
    def replaceReferencesToTileset(self, oldTileset, newTileset):
        i = 0
        while (i < self.mGrid.size()):
            tile = self.mGrid.at(i).tile
            if (tile and tile.tileset() == oldTileset):
                self.mGrid[i].tile = newTileset.tileAt(tile.id())
            i += 1

    ##
    # Resizes this tile layer to \a size, while shifting all tiles by
    # \a offset.
    ##
    def resize(self, size, offset):
        if (self.size() == size and offset.isNull()):
            return
        newGrid = QVector()
        for i in range(size.width() * size.height()):
            newGrid.append(Cell())
        # Copy over the preserved part
        startX = max(0, -offset.x())
        startY = max(0, -offset.y())
        endX = min(self.mWidth, size.width() - offset.x())
        endY = min(self.mHeight, size.height() - offset.y())
        for y in range(startY, endY):
            for x in range(startX, endX):
                index = x + offset.x() + (y + offset.y()) * size.width()
                newGrid[index] = self.cellAt(x, y)

        self.mGrid = newGrid
        self.setSize(size)

    ##
    # Offsets the tiles in this layer within \a bounds by \a offset,
    # and optionally wraps them.
    #
    # \sa ObjectGroup.offset()
    ##
    def offsetTiles(self, offset, bounds, wrapX, wrapY):
        newGrid = QVector()
        for i in range(self.mWidth * self.mHeight):
            newGrid.append(Cell())
        for y in range(self.mHeight):
            for x in range(self.mWidth):
                # Skip out of bounds tiles
                if (not bounds.contains(x, y)):
                    newGrid[x + y * self.mWidth] = self.cellAt(x, y)
                    continue

                # Get position to pull tile value from
                oldX = x - offset.x()
                oldY = y - offset.y()
                # Wrap x value that will be pulled from
                if (wrapX and bounds.width() > 0):
                    while oldX < bounds.left():
                        oldX += bounds.width()
                    while oldX > bounds.right():
                        oldX -= bounds.width()

                # Wrap y value that will be pulled from
                if (wrapY and bounds.height() > 0):
                    while oldY < bounds.top():
                        oldY += bounds.height()
                    while oldY > bounds.bottom():
                        oldY -= bounds.height()

                # Set the new tile
                if (self.contains(oldX, oldY) and bounds.contains(oldX, oldY)):
                    newGrid[x + y * self.mWidth] = self.cellAt(oldX, oldY)
                else:
                    newGrid[x + y * self.mWidth] = Cell()

        self.mGrid = newGrid

    def canMergeWith(self, other):
        return other.isTileLayer()

    def mergedWith(self, other):
        o = other
        unitedBounds = self.bounds().united(o.bounds())
        offset = self.position() - unitedBounds.topLeft()
        merged = self.clone()
        merged.resize(unitedBounds.size(), offset)
        merged.merge(o.position() - unitedBounds.topLeft(), o)
        return merged

    ##
    # Returns the region where this tile layer and the given tile layer
    # are different. The relative positions of the layers are taken into
    # account. The returned region is relative to this tile layer.
    ##
    def computeDiffRegion(self, other):
        ret = QRegion()
        dx = other.x() - self.mX
        dy = other.y() - self.mY
        r = QRect(0, 0, self.width(), self.height())
        r &= QRect(dx, dy, other.width(), other.height())
        for y in range(r.top(), r.bottom() + 1):
            for x in range(r.left(), r.right() + 1):
                if (self.cellAt(x, y) != other.cellAt(x - dx, y - dy)):
                    rangeStart = x
                    while (x <= r.right() and
                           self.cellAt(x, y) != other.cellAt(x - dx, y - dy)):
                        x += 1

                    rangeEnd = x
                    ret += QRect(rangeStart, y, rangeEnd - rangeStart, 1)

        return ret

    ##
    # Returns True if all tiles in the layer are empty.
    ##
    def isEmpty(self):
        i = 0
        while (i < self.mGrid.size()):
            if (not self.mGrid.at(i).isEmpty()):
                return False
            i += 1
        return True

    ##
    # Returns a duplicate of this TileLayer.
    #
    # \sa Layer.clone()
    ##
    def clone(self):
        return self.initializeClone(
            TileLayer(self.mName, self.mX, self.mY, self.mWidth, self.mHeight))

    def begin(self):
        return self.mGrid.begin()

    def end(self):
        return self.mGrid.end()

    def initializeClone(self, clone):
        super().initializeClone(clone)
        clone.mGrid = self.mGrid
        clone.mMaxTileSize = self.mMaxTileSize
        clone.mOffsetMargins = self.mOffsetMargins
        return clone
Esempio n. 38
0
class AutomappingManager(QObject):
    ##
    # This signal is emited after automapping was done and an error occurred.
    ##
    errorsOccurred = pyqtSignal(bool)
    ##
    # This signal is emited after automapping was done and a warning occurred.
    ##
    warningsOccurred = pyqtSignal(bool)

    ##
    # Constructor.
    ##
    def __init__(self, parent=None):
        super().__init__(parent)

        ##
        # The current map document.
        ##
        self.mMapDocument = None
        ##
        # For each new file of rules a new AutoMapper is setup. In this vector we
        # can store all of the AutoMappers in order.
        ##
        self.mAutoMappers = QVector()
        ##
        # This tells you if the rules for the current map document were already
        # loaded.
        ##
        self.mLoaded = False
        ##
        # Contains all errors which occurred until canceling.
        # If mError is not empty, no serious result can be expected.
        ##
        self.mError = ''
        ##
        # Contains all strings, which try to explain unusual and unexpected
        # behavior.
        ##
        self.mWarning = QString()

    def __del__(self):
        self.cleanUp()

    def setMapDocument(self, mapDocument):
        self.cleanUp()
        if (self.mMapDocument):
            self.mMapDocument.disconnect()
        self.mMapDocument = mapDocument
        if (self.mMapDocument):
            self.mMapDocument.regionEdited.connect(self.autoMap)

        self.mLoaded = False

    def errorString(self):
        return self.mError

    def warningString(self):
        return self.mWarning

    ##
    # This triggers an automapping on the whole current map document.
    ##
    def autoMap(self, *args):
        l = len(args)
        if l == 0:
            if (not self.mMapDocument):
                return
            map = self.mMapDocument.Map()
            w = map.width()
            h = map.height()
            self.autoMapInternal(QRect(0, 0, w, h), None)
        elif l == 2:
            where, touchedLayer = args
            if (preferences.Preferences.instance().automappingDrawing()):
                self.autoMapInternal(where, touchedLayer)

    ##
    # This function parses a rules file.
    # For each path which is a rule, (fileextension is tmx) an AutoMapper
    # object is setup.
    #
    # If a fileextension is txt, this file will be opened and searched for
    # rules again.
    #
    # @return if the loading was successful: return True if it suceeded.
    ##
    def loadFile(self, filePath):
        ret = True
        absPath = QFileInfo(filePath).path()
        rulesFile = QFile(filePath)
        if (not rulesFile.exists()):
            self.mError += self.tr("No rules file found at:\n%s\n" % filePath)
            return False

        if (not rulesFile.open(QIODevice.ReadOnly | QIODevice.Text)):
            self.mError += self.tr("Error opening rules file:\n%s\n" %
                                   filePath)
            return False

        i = QTextStream(rulesFile)
        line = ' '
        while line != '':
            line = i.readLine()
            rulePath = line.strip()
            if (rulePath == '' or rulePath.startswith('#')
                    or rulePath.startswith("//")):
                continue
            if (QFileInfo(rulePath).isRelative()):
                rulePath = absPath + '/' + rulePath
            if (not QFileInfo(rulePath).exists()):
                self.mError += self.tr("File not found:\n%s" % rulePath) + '\n'
                ret = False
                continue

            if (rulePath.lower().endswith(".tmx")):
                tmxFormat = TmxMapFormat()
                rules = tmxFormat.read(rulePath)
                if (not rules):
                    self.mError += self.tr("Opening rules map failed:\n%s" %
                                           tmxFormat.errorString()) + '\n'
                    ret = False
                    continue

                tilesetManager = TilesetManager.instance()
                tilesetManager.addReferences(rules.tilesets())
                autoMapper = None
                autoMapper = AutoMapper(self.mMapDocument, rules, rulePath)
                self.mWarning += autoMapper.warningString()
                error = autoMapper.errorString()
                if error != '':
                    self.mAutoMappers.append(autoMapper)
                else:
                    self.mError += error
                    del autoMapper

            if (rulePath.lower().endswith(".txt")):
                if (not self.loadFile(rulePath)):
                    ret = False
        return ret

    ##
    # Applies automapping to the Region \a where, considering only layer
    # \a touchedLayer has changed.
    # There will only those Automappers be used which have a rule layer
    # touching the \a touchedLayer
    # If layer is 0, all Automappers are used.
    ##
    def autoMapInternal(self, where, touchedLayer):
        self.mError = ''
        self.mWarning = ''
        if (not self.mMapDocument):
            return
        automatic = touchedLayer != None
        if (not self.mLoaded):
            mapPath = QFileInfo(self.mMapDocument.fileName()).path()
            rulesFileName = mapPath + "/rules.txt"
            if (self.loadFile(rulesFileName)):
                self.mLoaded = True
            else:
                self.errorsOccurred.emit(automatic)
                return

        passedAutoMappers = QVector()
        if (touchedLayer):
            for a in self.mAutoMappers:
                if (a.ruleLayerNameUsed(touchedLayer.name())):
                    passedAutoMappers.append(a)
        else:
            passedAutoMappers = self.mAutoMappers

        if (not passedAutoMappers.isEmpty()):
            # use a pointer to the region, so each automapper can manipulate it and the
            # following automappers do see the impact
            region = QRegion(where)

            undoStack = self.mMapDocument.undoStack()
            undoStack.beginMacro(self.tr("Apply AutoMap rules"))
            aw = AutoMapperWrapper(self.mMapDocument, passedAutoMappers,
                                   region)
            undoStack.push(aw)
            undoStack.endMacro()

        for automapper in self.mAutoMappers:
            self.mWarning += automapper.warningString()
            self.mError += automapper.errorString()

        if self.mWarning != '':
            self.warningsOccurred.emit(automatic)
        if self.mError != '':
            self.errorsOccurred.emit(automatic)

    ##
    # deletes all its data structures
    ##
    def cleanUp(self):
        self.mAutoMappers.clear()
Esempio n. 39
0
class CellRenderer():
    BottomLeft, BottomCenter, TopLeft = range(3)

    def __init__(self, painter):
        self.mPainter = painter
        self.mTile = None
        self.mIsOpenGL = hasOpenGLEngine(painter)

        self.mFragments = QVector()

    def __del__(self):
        self.flush()

    ##
    # Renders a \a cell with the given \a origin at \a pos, taking into account
    # the flipping and tile offset.
    #
    # For performance reasons, the actual drawing is delayed until a different
    # kind of tile has to be drawn. For this reason it is necessary to call
    # flush when finished doing drawCell calls. This function is also called by
    # the destructor so usually an explicit call is not needed.
    ##
    def render(self, cell, pos, cellSize, origin):
        if (self.mTile != cell.tile):
            self.flush()
        image = cell.tile.currentFrameImage()
        size = image.size()
        if cellSize == QSizeF(0, 0):
            objectSize = size
        else:
            objectSize = cellSize
        scale = QSizeF(objectSize.width() / size.width(),
                       objectSize.height() / size.height())
        offset = cell.tile.offset()
        sizeHalf = QPointF(objectSize.width() / 2, objectSize.height() / 2)
        fragment = QPainter.PixmapFragment()
        fragment.x = pos.x() + (offset.x() * scale.width()) + sizeHalf.x()
        fragment.y = pos.y() + (
            offset.y() * scale.height()) + sizeHalf.y() - objectSize.height()
        fragment.sourceLeft = 0
        fragment.sourceTop = 0
        fragment.width = size.width()
        fragment.height = size.height()

        if cell.flippedHorizontally:
            fragment.scaleX = -1
        else:
            fragment.scaleX = 1
        if cell.flippedVertically:
            fragment.scaleY = -1
        else:
            fragment.scaleY = 1

        fragment.rotation = 0
        fragment.opacity = 1
        flippedHorizontally = cell.flippedHorizontally
        flippedVertically = cell.flippedVertically
        if (origin == CellRenderer.BottomCenter):
            fragment.x -= sizeHalf.x()
        if (cell.flippedAntiDiagonally):
            fragment.rotation = 90
            flippedHorizontally = cell.flippedVertically
            flippedVertically = not cell.flippedHorizontally
            # Compensate for the swap of image dimensions
            halfDiff = sizeHalf.y() - sizeHalf.x()
            fragment.y += halfDiff
            if (origin != CellRenderer.BottomCenter):
                fragment.x += halfDiff

        if flippedHorizontally:
            x = -1
        else:
            x = 1
        fragment.scaleX = scale.width() * x

        if flippedVertically:
            x = -1
        else:
            x = 1
        fragment.scaleY = scale.height() * x
        if (self.mIsOpenGL or (fragment.scaleX > 0 and fragment.scaleY > 0)):
            self.mTile = cell.tile
            self.mFragments.append(fragment)
            return

        # The Raster paint engine as of Qt 4.8.4 / 5.0.2 does not support
        # drawing fragments with a negative scaling factor.
        self.flush()  # make sure we drew all tiles so far
        oldTransform = self.mPainter.transform()
        transform = oldTransform
        transform.translate(fragment.x, fragment.y)
        transform.rotate(fragment.rotation)
        transform.scale(fragment.scaleX, fragment.scaleY)
        target = QRectF(fragment.width * -0.5, fragment.height * -0.5,
                        fragment.width, fragment.height)
        source = QRectF(0, 0, fragment.width, fragment.height)
        self.mPainter.setTransform(transform)
        self.mPainter.drawPixmap(target, image, source)
        self.mPainter.setTransform(oldTransform)

    def flush(self):
        if (not self.mTile):
            return
        self.mPainter.drawPixmapFragments(self.mFragments,
                                          self.mTile.currentFrameImage())
        self.mTile = None
        self.mFragments.resize(0)
Esempio n. 40
0
    def __toTileset(self, variant):
        variantMap = variant
        firstGid = variantMap.get("firstgid", 0)

        # Handle external tilesets
        sourceVariant = variantMap.get("source", '')
        if sourceVariant != '':
            source = resolvePath(self.mMapDir, sourceVariant)
            tileset, error = readTileset(source)
            if not tileset:
                self.mError = self.tr("Error while loading tileset '%s': %s" %
                                      (source, error))
            else:
                self.mGidMapper.insert(firstGid, tileset)
            return tileset

        name = variantMap.get("name", '')
        tileWidth = variantMap.get("tilewidth", 0)
        tileHeight = variantMap.get("tileheight", 0)
        spacing = variantMap.get("spacing", 0)
        margin = variantMap.get("margin", 0)
        tileOffset = variantMap.get("tileoffset", {})
        tileOffsetX = tileOffset.get("x", 0)
        tileOffsetY = tileOffset.get("y", 0)
        if (tileWidth <= 0 or tileHeight <= 0
                or (firstGid == 0 and not self.mReadingExternalTileset)):
            self.mError = self.tr(
                "Invalid tileset parameters for tileset '%s'" % name)
            return None

        tileset = Tileset.create(name, tileWidth, tileHeight, spacing, margin)
        tileset.setTileOffset(QPoint(tileOffsetX, tileOffsetY))
        trans = variantMap.get("transparentcolor", '')
        if (trans != '' and QColor.isValidColor(trans)):
            tileset.setTransparentColor(QColor(trans))
        imageVariant = variantMap.get("image", '')
        if imageVariant != '':
            imagePath = resolvePath(self.mMapDir, imageVariant)
            if (not tileset.loadFromImage(imagePath)):
                self.mError = self.tr("Error loading tileset image:\n'%s'" %
                                      imagePath)
                return None

        tileset.setProperties(
            self.toProperties(variantMap.get("properties", {})))

        # Read terrains
        terrainsVariantList = variantMap.get("terrains", [])
        for terrainMap in terrainsVariantList:
            tileset.addTerrain(terrainMap.get("name", ''),
                               terrainMap.get("tile", 0))

        # Read tile terrain and external image information
        tilesVariantMap = variantMap.get("tiles", {})
        for it in tilesVariantMap.items():
            ok = False
            tileIndex = Int(it[0])
            if (tileIndex < 0):
                self.mError = self.tr("Tileset tile index negative:\n'%d'" %
                                      tileIndex)

            if (tileIndex >= tileset.tileCount()):
                # Extend the tileset to fit the tile
                if (tileIndex >= len(tilesVariantMap)):
                    # If tiles are  defined this way, there should be an entry
                    # for each tile.
                    # Limit the index to number of entries to prevent running out
                    # of memory on malicious input.f
                    self.mError = self.tr(
                        "Tileset tile index too high:\n'%d'" % tileIndex)
                    return None

                for i in range(tileset.tileCount(), tileIndex + 1):
                    tileset.addTile(QPixmap())

            tile = tileset.tileAt(tileIndex)
            if (tile):
                tileVar = it[1]
                terrains = tileVar.get("terrain", [])
                if len(terrains) == 4:
                    for i in range(0, 4):
                        terrainId, ok = Int2(terrains[i])
                        if (ok and terrainId >= 0
                                and terrainId < tileset.terrainCount()):
                            tile.setCornerTerrainId(i, terrainId)

                probability, ok = Float2(tileVar.get("probability", 0.0))
                if (ok):
                    tile.setProbability(probability)
                imageVariant = tileVar.get("image", '')
                if imageVariant != '':
                    imagePath = resolvePath(self.mMapDir, imageVariant)
                    tileset.setTileImage(tileIndex, QPixmap(imagePath),
                                         imagePath)

                objectGroupVariant = tileVar.get("objectgroup", {})
                if len(objectGroupVariant) > 0:
                    tile.setObjectGroup(self.toObjectGroup(objectGroupVariant))
                frameList = tileVar.get("animation", [])
                lenFrames = len(frameList)
                if lenFrames > 0:
                    frames = QVector()
                    for i in range(lenFrames):
                        frames.append(Frame())
                    for i in range(lenFrames - 1, -1, -1):
                        frameVariantMap = frameList[i]
                        frame = frames[i]
                        frame.tileId = frameVariantMap.get("tileid", 0)
                        frame.duration = frameVariantMap.get("duration", 0)

                    tile.setFrames(frames)

        # Read tile properties
        propertiesVariantMap = variantMap.get("tileproperties", {})

        for it in propertiesVariantMap.items():
            tileIndex = Int(it[0])
            propertiesVar = it[1]
            if (tileIndex >= 0 and tileIndex < tileset.tileCount()):
                properties = self.toProperties(propertiesVar)
                tileset.tileAt(tileIndex).setProperties(properties)

        if not self.mReadingExternalTileset:
            self.mGidMapper.insert(firstGid, tileset)

        return tileset
Esempio n. 41
0
def fillRegion(layer, fillOrigin):
    # Create that region that will hold the fill
    fillRegion = QRegion()
    # Silently quit if parameters are unsatisfactory
    if (not layer.contains(fillOrigin)):
        return fillRegion
    # Cache cell that we will match other cells against
    matchCell = layer.cellAt(fillOrigin)
    # Grab map dimensions for later use.
    layerWidth = layer.width()
    layerHeight = layer.height()
    layerSize = layerWidth * layerHeight
    # Create a queue to hold cells that need filling
    fillPositions = QList()
    fillPositions.append(fillOrigin)
    # Create an array that will store which cells have been processed
    # This is faster than checking if a given cell is in the region/list
    processedCellsVec = QVector()
    for i in range(layerSize):
        processedCellsVec.append(0xff)
    processedCells = processedCellsVec
    # Loop through queued positions and fill them, while at the same time
    # checking adjacent positions to see if they should be added
    while (not fillPositions.empty()):
        currentPoint = fillPositions.takeFirst()
        startOfLine = currentPoint.y() * layerWidth
        # Seek as far left as we can
        left = currentPoint.x()
        while (left > 0 and layer.cellAt(left - 1, currentPoint.y()) == matchCell):
            left -= 1
        # Seek as far right as we can
        right = currentPoint.x()
        while (right + 1 < layerWidth and layer.cellAt(right + 1, currentPoint.y()) == matchCell):
            right += 1
        # Add cells between left and right to the region
        fillRegion += QRegion(left, currentPoint.y(), right - left + 1, 1)
        # Add cell strip to processed cells
        for i in range(startOfLine + left, right + startOfLine, 1):
            processedCells[i] = 1
        # These variables cache whether the last cell was added to the queue
        # or not as an optimization, since adjacent cells on the x axis
        # do not need to be added to the queue.
        lastAboveCell = False
        lastBelowCell = False
        # Loop between left and right and check if cells above or
        # below need to be added to the queue
        for x in range(left, right+1):
            fillPoint = QPoint(x, currentPoint.y())
            # Check cell above
            if (fillPoint.y() > 0):
                aboveCell = QPoint(fillPoint.x(), fillPoint.y() - 1)
                if (not processedCells[aboveCell.y() * layerWidth + aboveCell.x()] and layer.cellAt(aboveCell) == matchCell):

                    # Do not add the above cell to the queue if its
                    # x-adjacent cell was added.
                    if (not lastAboveCell):
                        fillPositions.append(aboveCell)
                    lastAboveCell = True
                else:
                    lastAboveCell = False

                processedCells[aboveCell.y() * layerWidth + aboveCell.x()] = 1

            # Check cell below
            if (fillPoint.y() + 1 < layerHeight):
                belowCell = QPoint(fillPoint.x(), fillPoint.y() + 1)
                if (not processedCells[belowCell.y() * layerWidth + belowCell.x()] and layer.cellAt(belowCell) == matchCell):

                    # Do not add the below cell to the queue if its
                    # x-adjacent cell was added.
                    if (not lastBelowCell):
                        fillPositions.append(belowCell)
                    lastBelowCell = True
                else:
                    lastBelowCell = False

                processedCells[belowCell.y() * layerWidth + belowCell.x()] = 1

    return fillRegion
Esempio n. 42
0
    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
Esempio n. 43
0
class ObjectTypesModel(QAbstractTableModel):
    ColorRole = Qt.UserRole

    def __init__(self, parent):
        super().__init__(parent)
        
        self.mObjectTypes = QVector()
        
    def setObjectTypes(self, objectTypes):
        self.beginResetModel()
        self.mObjectTypes = objectTypes
        self.endResetModel()

    def objectTypes(self):
        return self.mObjectTypes
        
    def rowCount(self, parent):
        if parent.isValid():
            _x = 0
        else:
            _x = self.mObjectTypes.size()
        return _x

    def columnCount(self, parent):
        if parent.isValid():
            _x = 0
        else:
            _x = 2
        return _x

    def headerData(self, section, orientation, role):
        if (orientation == Qt.Horizontal):
            if (role == Qt.DisplayRole):
                x = section
                if x==0:
                    return self.tr("Type")
                elif x==1:
                    return self.tr("Color")
            elif (role == Qt.TextAlignmentRole):
                return Qt.AlignLeft

        return QVariant()

    def data(self, index, role):
        # QComboBox requests data for an invalid index when the model is empty
        if (not index.isValid()):
            return QVariant()
        objectType = self.mObjectTypes.at(index.row())
        if (role == Qt.DisplayRole or role == Qt.EditRole):
            if (index.column() == 0):
                return objectType.name
        if (role == ObjectTypesModel.ColorRole and index.column() == 1):
            return objectType.color
        return QVariant()

    def setData(self, index, value, role):
        if (role == Qt.EditRole and index.column() == 0):
            self.mObjectTypes[index.row()].name = value.strip()
            self.dataChanged.emit(index, index)
            return True

        return False

    def flags(self, index):
        f = super().flags(index)
        if (index.column() == 0):
            f |= Qt.ItemIsEditable
        return f

    def setObjectTypeColor(self, objectIndex, color):
        self.mObjectTypes[objectIndex].color = color
        mi = self.index(objectIndex, 1)
        self.dataChanged.emit(mi, mi)

    def removeObjectTypes(self, indexes):
        rows = QVector()
        for index in indexes:
            rows.append(index.row())
        rows = sorted(rows)
        for i in range(len(rows) - 1, -1, -1):
            row = rows[i]
            self.beginRemoveRows(QModelIndex(), row, row)
            self.mObjectTypes.remove(row)
            self.endRemoveRows()

    def appendNewObjectType(self):
        self.beginInsertRows(QModelIndex(), self.mObjectTypes.size(), self.mObjectTypes.size())
        self.mObjectTypes.append(ObjectType())
        self.endInsertRows()
Esempio n. 44
0
    def __readTilesetTile(self, tileset):
        atts = self.xml.attributes()
        id = Int(atts.value("id"))
        if (id < 0):
            self.xml.raiseError(self.tr("Invalid tile ID: %d"%id))
            return

        hasImage = tileset.imageSource()!=''
        if (hasImage and id >= tileset.tileCount()):
            self.xml.raiseError(self.tr("Tile ID does not exist in tileset image: %d"%id))
            return

        if (id > tileset.tileCount()):
            self.xml.raiseError(self.tr("Invalid (nonconsecutive) tile ID: %d"%id))
            return

        # For tilesets without image source, consecutive tile IDs are allowed (for
        # tiles with individual images)
        if (id == tileset.tileCount()):
            tileset.addTile(QPixmap())
        tile = tileset.tileAt(id)
        # Read tile quadrant terrain ids
        terrain = atts.value("terrain")
        if terrain != '':
            quadrants = terrain.split(",")
            if (len(quadrants) == 4):
                for i in range(4):
                    if quadrants[i]=='':
                        t = -1
                    else:
                        t = Int(quadrants[i])
                    tile.setCornerTerrainId(i, t)

        # Read tile probability
        probability = atts.value("probability")
        if probability != '':
            tile.setProbability(Float(probability))
        while (self.xml.readNextStartElement()):
            if (self.xml.name() == "properties"):
                tile.mergeProperties(self.__readProperties())
            elif (self.xml.name() == "image"):
                source = self.xml.attributes().value("source")
                if source != '':
                    source = self.p.resolveReference(source, self.mPath)
                tileset.setTileImage(id, QPixmap.fromImage(self.__readImage()), source)
            elif (self.xml.name() == "objectgroup"):
                tile.setObjectGroup(self.__readObjectGroup())
            elif (self.xml.name() == "animation"):
                tile.setFrames(self.__readAnimationFrames())
            else:
                self.__readUnknownElement()

        # Temporary code to support TMW-style animation frame properties
        if (not tile.isAnimated() and tile.hasProperty("animation-frame0")):
            frames = QVector()
            i = 0
            while(i>=0):
                frameName = "animation-frame" + str(i)
                delayName = "animation-delay" + str(i)
                if (tile.hasProperty(frameName) and tile.hasProperty(delayName)):
                    frame = Frame()
                    frame.tileId = tile.property(frameName)
                    frame.duration = tile.property(delayName) * 10
                    frames.append(frame)
                else:
                    break
                i += 1

            tile.setFrames(frames)
Esempio n. 45
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()
Esempio n. 46
0
class CommandLineParser():
    def __init__(self):
        self.mCurrentProgramName = QString()
        self.mOptions = QVector()
        self.mShowHelp = False
        self.mLongestArgument = 0
        self.mFilesToOpen = QStringList()

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

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

    ##
    # Registers an option with the parser. When an option with the given
    # \a shortName or \a longName is encountered, \a callback is called with
    # \a data as its only parameter.
    ##
    def registerOption(self, *args):
        l = len(args)
        if l==4:
            ##
            # Convenience overload that allows registering an option with a callback
            # as a member function of a class. The class type and the member function
            # are given as template parameters, while the instance is passed in as
            # \a handler.
            #
            # \overload
            ##
            handler, shortName, longName, help = args
            self.registerOption(MemberFunctionCall, handler, shortName, longName, help)
        elif l==5:
            callback, data, shortName, longName, help = args
            self.mOptions.append(CommandLineParser.Option(callback, data, shortName, longName, help))
            length = longName.length()
            if (self.mLongestArgument < length):
                self.mLongestArgument = length

    ##
    # Parses the given \a arguments. Returns False when the application is not
    # expected to run (either there was a parsing error, or the help was
    # requested).
    ##
    def parse(self, arguments):
        self.mFilesToOpen.clear()
        self.mShowHelp = False
        todo = QStringList(arguments)
        self.mCurrentProgramName = QFileInfo(todo.takeFirst()).fileName()
        index = 0
        noMoreArguments = False
        while (not todo.isEmpty()):
            index += 1
            arg = todo.takeFirst()
            if (arg.isEmpty()):
                continue
            if (noMoreArguments or arg.at(0) != '-'):
                self.mFilesToOpen.append(arg)
                continue

            if (arg.length() == 1):
                # Traditionally a single hyphen means read file from stdin,
                # write file to stdout. This isn't supported right now.
                qWarning(self.tr("Bad argument %d: lonely hyphen"%index))
                self.showHelp()
                return False

            # Long options
            if (arg.at(1) == '-'):
                # Double hypen "--" means no more options will follow
                if (arg.length() == 2):
                    noMoreArguments = True
                    continue

                if (not self.handleLongOption(arg)):
                    qWarning(self.tr("Unknown long argument %d: %s"%(index, arg)))
                    self.mShowHelp = True
                    break

                continue

            # Short options
            for i in range(1, arg.length()):
                c = arg.at(i)
                if (not self.handleShortOption(c)):
                    qWarning(self.tr("Unknown short argument %d.%d: %s"%(index, i, c)))
                    self.mShowHelp = True
                    break

        if (self.mShowHelp):
            self.showHelp()
            return False

        return True

    ##
    # Returns the files to open that were found among the arguments.
    ##
    def filesToOpen(self):
        return QList(self.mFilesToOpen)

    def showHelp(self):
        qWarning(self.tr("Usage:\n  %s [options] [files...]"%self.mCurrentProgramName) + "\n\n" + self.tr("Options:"))
        qWarning("  -h %-*s : %s", self.mLongestArgument, "--help", self.tr("Display this help"))
        for option in self.mOptions:
            if (not option.shortName.isNull()):
                qWarning("  -%c %-*s : %s",
                         option.shortName.toLatin1(),
                         self.mLongestArgument, option.longName,
                         option.help)
            else:
                qWarning("     %-*s : %s",
                         self.mLongestArgument, option.longName,
                         option.help)

        qWarning()

    def handleLongOption(self, longName):
        if (longName == "--help"):
            self.mShowHelp = True
            return True

        for option in self.mOptions:
            if (longName == option.longName):
                option.callback(option.data)
                return True

        return False

    def handleShortOption(self, c):
        if (c == 'h'):
            self.mShowHelp = True
            return True

        for option in self.mOptions:
            if (c == option.shortName):
                option.callback(option.data)
                return True

        return False

    ##
    # Internal definition of a command line option.
    ##
    class Option():
        def __init__(self, *args):
            l = len(args)
            callback = Callback()
            shortName = QChar()
            longName = QString()
            help = QString()
            if l==0:
                self.callback = 0
                self.data = 0
            elif l==5:
                callback = args[0]
                data = args[1]
                shortName = args[2]
                longName = args[3]
                help = args[4]
                self.callback = callback
                self.data = data
                self.shortName = shortName
                self.longName = longName
                self.help = help
Esempio n. 47
0
class ObjectSelectionTool(AbstractObjectTool):
    def __init__(self, parent=None):
        super().__init__(self.tr("Select Objects"),
                         QIcon(":images/22x22/tool-select-objects.png"),
                         QKeySequence(self.tr("S")), parent)
        self.mSelectionRectangle = SelectionRectangle()
        self.mOriginIndicator = OriginIndicator()
        self.mMousePressed = False
        self.mHoveredObjectItem = None
        self.mClickedObjectItem = None
        self.mClickedRotateHandle = None
        self.mClickedResizeHandle = None
        self.mResizingLimitHorizontal = False
        self.mResizingLimitVertical = False
        self.mMode = Mode.Resize
        self.mAction = Action.NoAction
        self.mRotateHandles = [0, 0, 0, 0]
        self.mResizeHandles = [0, 0, 0, 0, 0, 0, 0, 0]
        self.mAlignPosition = QPointF()
        self.mMovingObjects = QVector()
        self.mScreenStart = QPoint()
        self.mStart = QPointF()
        self.mModifiers = 0
        self.mOrigin = QPointF()

        for i in range(AnchorPosition.CornerAnchorCount):
            self.mRotateHandles[i] = RotateHandle(i)
        for i in range(AnchorPosition.AnchorCount):
            self.mResizeHandles[i] = ResizeHandle(i)

    def __del__(self):
        if self.mSelectionRectangle.scene():
            self.mSelectionRectangle.scene().removeItem(
                self.mSelectionRectangle)
        if self.mOriginIndicator.scene():
            self.mOriginIndicator.scene().removeItem(self.mOriginIndicator)
        for i in range(AnchorPosition.CornerAnchorCount):
            handle = self.mRotateHandles[i]
            scene = handle.scene()
            if scene:
                scene.removeItem(handle)
        self.mRotateHandles.clear()
        for i in range(AnchorPosition.AnchorCount):
            handle = self.mResizeHandles[i]
            scene = handle.scene()
            if scene:
                scene.removeItem(handle)
        self.mResizeHandles.clear()

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

    def activate(self, scene):
        super().activate(scene)
        self.updateHandles()
        self.mapDocument().objectsChanged.connect(self.updateHandles)
        self.mapDocument().mapChanged.connect(self.updateHandles)
        scene.selectedObjectItemsChanged.connect(self.updateHandles)
        self.mapDocument().objectsRemoved.connect(self.objectsRemoved)
        if self.mOriginIndicator.scene() != scene:
            scene.addItem(self.mOriginIndicator)
        for i in range(AnchorPosition.CornerAnchorCount):
            handle = self.mRotateHandles[i]
            if handle.scene() != scene:
                scene.addItem(handle)
        for i in range(AnchorPosition.AnchorCount):
            handle = self.mResizeHandles[i]
            if handle.scene() != scene:
                scene.addItem(handle)

    def deactivate(self, scene):
        if self.mOriginIndicator.scene() == scene:
            scene.removeItem(self.mOriginIndicator)
        for i in range(AnchorPosition.CornerAnchorCount):
            handle = self.mRotateHandles[i]
            if handle.scene() == scene:
                scene.removeItem(handle)
        for i in range(AnchorPosition.AnchorCount):
            handle = self.mResizeHandles[i]
            if handle.scene() == scene:
                scene.removeItem(handle)
        self.mapDocument().objectsChanged.disconnect(self.updateHandles)
        self.mapDocument().mapChanged.disconnect(self.updateHandles)
        scene.selectedObjectItemsChanged.disconnect(self.updateHandles)
        super().deactivate(scene)

    def keyPressed(self, event):
        if (self.mAction != Action.NoAction):
            event.ignore()
            return

        moveBy = QPointF()
        x = event.key()
        if x == Qt.Key_Up:
            moveBy = QPointF(0, -1)
        elif x == Qt.Key_Down:
            moveBy = QPointF(0, 1)
        elif x == Qt.Key_Left:
            moveBy = QPointF(-1, 0)
        elif x == Qt.Key_Right:
            moveBy = QPointF(1, 0)
        else:
            super().keyPressed(event)
            return

        items = self.mapScene().selectedObjectItems()
        modifiers = event.modifiers()
        if (moveBy.isNull() or items.isEmpty()
                or (modifiers & Qt.ControlModifier)):
            event.ignore()
            return

        moveFast = modifiers & Qt.ShiftModifier
        snapToFineGrid = preferences.Preferences.instance().snapToFineGrid()
        if (moveFast):
            # TODO: This only makes sense for orthogonal maps
            moveBy.setX(moveBy.x() * self.mapDocument().map().tileWidth())
            moveBy.setX(moveBy.y() * self.mapDocument().map().tileHeight())
            if (snapToFineGrid):
                moveBy /= preferences.Preferences.instance().gridFine()

        undoStack = self.mapDocument().undoStack()
        undoStack.beginMacro(self.tr("Move %n Object(s)", "", items.size()))
        i = 0
        for objectItem in items:
            object = objectItem.mapObject()
            oldPos = object.position()
            newPos = oldPos + moveBy
            undoStack.push(
                MoveMapObject(self.mapDocument(), object, newPos, oldPos))
            i += 1

        undoStack.endMacro()

    def mouseEntered(self):
        pass

    def mouseMoved(self, pos, modifiers):
        super().mouseMoved(pos, modifiers)

        # Update the hovered item (for mouse cursor)
        hoveredRotateHandle = None
        hoveredResizeHandle = None
        hoveredObjectItem = None

        view = self.mapScene().views()[0]
        if view:
            hoveredItem = self.mapScene().itemAt(pos, view.transform())
            hoveredRotateHandle = None
            hoveredResizeHandle = None
            tp = type(hoveredItem)
            if tp == RotateHandle:
                hoveredRotateHandle = hoveredItem
            elif tp == ResizeHandle:
                hoveredResizeHandle = hoveredItem

        if (not hoveredRotateHandle and not hoveredResizeHandle):
            hoveredObjectItem = self.topMostObjectItemAt(pos)

        self.mHoveredObjectItem = hoveredObjectItem

        if (self.mAction == Action.NoAction and self.mMousePressed):
            screenPos = QCursor.pos()
            dragDistance = (self.mScreenStart - screenPos).manhattanLength()
            if (dragDistance >= QApplication.startDragDistance()):
                hasSelection = not self.mapScene().selectedObjectItems(
                ).isEmpty()
                # Holding Alt forces moving current selection
                # Holding Shift forces selection rectangle
                if ((self.mClickedObjectItem or
                     (modifiers & Qt.AltModifier) and hasSelection)
                        and not (modifiers & Qt.ShiftModifier)):
                    self.startMoving(modifiers)
                elif (self.mClickedRotateHandle):
                    self.startRotating()
                elif (self.mClickedResizeHandle):
                    self.startResizing()
                else:
                    self.startSelecting()

        x = self.mAction
        if x == Action.Selecting:
            self.mSelectionRectangle.setRectangle(
                QRectF(self.mStart, pos).normalized())
        elif x == Action.Moving:
            self.updateMovingItems(pos, modifiers)
        elif x == Action.Rotating:
            self.updateRotatingItems(pos, modifiers)
        elif x == Action.Resizing:
            self.updateResizingItems(pos, modifiers)
        elif x == Action.NoAction:
            pass
        self.refreshCursor()

    def mousePressed(self, event):
        if (self.mAction != Action.NoAction
            ):  # 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()
            clickedRotateHandle = 0
            clickedResizeHandle = 0
            view = findView(event)
            if view:
                clickedItem = self.mapScene().itemAt(event.scenePos(),
                                                     view.transform())
                clickedRotateHandle = None
                clickedResizeHandle = None
                tp = type(clickedItem)
                if tp == RotateHandle:
                    clickedRotateHandle = clickedItem
                elif tp == ResizeHandle:
                    clickedResizeHandle = clickedItem
            self.mClickedRotateHandle = clickedRotateHandle
            self.mClickedResizeHandle = clickedResizeHandle
            if (not clickedRotateHandle and not clickedResizeHandle):
                self.mClickedObjectItem = self.topMostObjectItemAt(self.mStart)
        else:
            super().mousePressed(event)

    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()

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

    def languageChanged(self):
        self.setName(self.tr("Select Objects"))
        self.setShortcut(QKeySequence(self.tr("S")))

    def updateHandles(self):
        if (self.mAction == Action.Moving or self.mAction == Action.Rotating
                or self.mAction == Action.Resizing):
            return
        objects = self.mapDocument().selectedObjects()
        showHandles = objects.size() > 0
        if (showHandles):
            renderer = self.mapDocument().renderer()
            boundingRect = objectBounds(
                objects.first(), renderer,
                objectTransform(objects.first(), renderer))
            for i in range(1, objects.size()):
                object = objects.at(i)
                boundingRect |= objectBounds(object, renderer,
                                             objectTransform(object, renderer))

            topLeft = boundingRect.topLeft()
            topRight = boundingRect.topRight()
            bottomLeft = boundingRect.bottomLeft()
            bottomRight = boundingRect.bottomRight()
            center = boundingRect.center()
            handleRotation = 0
            # If there is only one object selected, align to its orientation.
            if (objects.size() == 1):
                object = objects.first()
                handleRotation = object.rotation()
                if (resizeInPixelSpace(object)):
                    bounds = pixelBounds(object)
                    transform = QTransform(objectTransform(object, renderer))
                    topLeft = transform.map(
                        renderer.pixelToScreenCoords_(bounds.topLeft()))
                    topRight = transform.map(
                        renderer.pixelToScreenCoords_(bounds.topRight()))
                    bottomLeft = transform.map(
                        renderer.pixelToScreenCoords_(bounds.bottomLeft()))
                    bottomRight = transform.map(
                        renderer.pixelToScreenCoords_(bounds.bottomRight()))
                    center = transform.map(
                        renderer.pixelToScreenCoords_(bounds.center()))
                    # Ugly hack to make handles appear nicer in this case
                    if (self.mapDocument().map().orientation() ==
                            Map.Orientation.Isometric):
                        handleRotation += 45
                else:
                    bounds = objectBounds(object, renderer, QTransform())
                    transform = QTransform(objectTransform(object, renderer))
                    topLeft = transform.map(bounds.topLeft())
                    topRight = transform.map(bounds.topRight())
                    bottomLeft = transform.map(bounds.bottomLeft())
                    bottomRight = transform.map(bounds.bottomRight())
                    center = transform.map(bounds.center())

            self.mOriginIndicator.setPos(center)
            self.mRotateHandles[AnchorPosition.TopLeftAnchor].setPos(topLeft)
            self.mRotateHandles[AnchorPosition.TopRightAnchor].setPos(topRight)
            self.mRotateHandles[AnchorPosition.BottomLeftAnchor].setPos(
                bottomLeft)
            self.mRotateHandles[AnchorPosition.BottomRightAnchor].setPos(
                bottomRight)
            top = (topLeft + topRight) / 2
            left = (topLeft + bottomLeft) / 2
            right = (topRight + bottomRight) / 2
            bottom = (bottomLeft + bottomRight) / 2
            self.mResizeHandles[AnchorPosition.TopAnchor].setPos(top)
            self.mResizeHandles[AnchorPosition.TopAnchor].setResizingOrigin(
                bottom)
            self.mResizeHandles[AnchorPosition.LeftAnchor].setPos(left)
            self.mResizeHandles[AnchorPosition.LeftAnchor].setResizingOrigin(
                right)
            self.mResizeHandles[AnchorPosition.RightAnchor].setPos(right)
            self.mResizeHandles[AnchorPosition.RightAnchor].setResizingOrigin(
                left)
            self.mResizeHandles[AnchorPosition.BottomAnchor].setPos(bottom)
            self.mResizeHandles[AnchorPosition.BottomAnchor].setResizingOrigin(
                top)
            self.mResizeHandles[AnchorPosition.TopLeftAnchor].setPos(topLeft)
            self.mResizeHandles[
                AnchorPosition.TopLeftAnchor].setResizingOrigin(bottomRight)
            self.mResizeHandles[AnchorPosition.TopRightAnchor].setPos(topRight)
            self.mResizeHandles[
                AnchorPosition.TopRightAnchor].setResizingOrigin(bottomLeft)
            self.mResizeHandles[AnchorPosition.BottomLeftAnchor].setPos(
                bottomLeft)
            self.mResizeHandles[
                AnchorPosition.BottomLeftAnchor].setResizingOrigin(topRight)
            self.mResizeHandles[AnchorPosition.BottomRightAnchor].setPos(
                bottomRight)
            self.mResizeHandles[
                AnchorPosition.BottomRightAnchor].setResizingOrigin(topLeft)
            for i in range(AnchorPosition.CornerAnchorCount):
                self.mRotateHandles[i].setRotation(handleRotation)
            for i in range(AnchorPosition.AnchorCount):
                self.mResizeHandles[i].setRotation(handleRotation)

        self.updateHandleVisibility()

    def updateHandleVisibility(self):
        hasSelection = not self.mapDocument().selectedObjects().isEmpty()
        showHandles = hasSelection and (self.mAction == Action.NoAction
                                        or self.mAction == Action.Selecting)
        showOrigin = hasSelection and self.mAction != Action.Moving and (
            self.mMode == Mode.Rotate or self.mAction == Action.Resizing)
        for i in range(AnchorPosition.CornerAnchorCount):
            self.mRotateHandles[i].setVisible(showHandles
                                              and self.mMode == Mode.Rotate)
        for i in range(AnchorPosition.AnchorCount):
            self.mResizeHandles[i].setVisible(showHandles
                                              and self.mMode == Mode.Resize)
        self.mOriginIndicator.setVisible(showOrigin)

    def objectsRemoved(self, objects):
        if (self.mAction != Action.Moving and self.mAction != Action.Rotating
                and self.mAction != Action.Resizing):
            return
        # Abort move/rotate/resize to avoid crashing...
        # TODO: This should really not be allowed to happen in the first place.
        # since it breaks the undo history, for example.
        for i in range(self.mMovingObjects.size() - 1, -1, -1):
            object = self.mMovingObjects[i]
            mapObject = object.item.mapObject()
            if objects.contains(mapObject):
                # Avoid referencing the removed object
                self.mMovingObjects.remove(i)
            else:
                mapObject.setPosition(object.oldPosition)
                mapObject.setSize(object.oldSize)
                mapObject.setPolygon(object.oldPolygon)
                mapObject.setRotation(object.oldRotation)

        self.mapDocument().mapObjectModel().emitObjectsChanged(
            self.changingObjects)
        self.mMovingObjects.clear()

    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 startSelecting(self):
        self.mAction = Action.Selecting
        self.mapScene().addItem(self.mSelectionRectangle)

    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()

    def updateMovingItems(self, pos, modifiers):
        renderer = self.mapDocument().renderer()

        diff = self.snapToGrid(pos - self.mStart, modifiers)
        for object in self.mMovingObjects:
            newPixelPos = object.oldItemPosition + diff
            newPos = renderer.screenToPixelCoords_(newPixelPos)

            mapObject = object.item.mapObject()
            mapObject.setPosition(newPos)
        self.mapDocument().mapObjectModel().emitObjectsChanged(
            self.changingObjects())

    def finishMoving(self, pos):
        self.mAction = Action.NoAction
        self.updateHandles()
        if (self.mStart == pos):  # Move is a no-op
            return
        undoStack = self.mapDocument().undoStack()
        undoStack.beginMacro(
            self.tr("Move %n Object(s)", "", self.mMovingObjects.size()))
        for object in self.mMovingObjects:
            undoStack.push(
                MoveMapObject(self.mapDocument(), object.item.mapObject(),
                              object.oldPosition))

        undoStack.endMacro()
        self.mMovingObjects.clear()

    def startRotating(self):
        self.mAction = Action.Rotating
        self.mOrigin = self.mOriginIndicator.pos()
        self.saveSelectionState()
        self.updateHandleVisibility()

    def updateRotatingItems(self, pos, modifiers):
        renderer = self.mapDocument().renderer()
        startDiff = self.mOrigin - self.mStart
        currentDiff = self.mOrigin - pos
        startAngle = math.atan2(startDiff.y(), startDiff.x())
        currentAngle = math.atan2(currentDiff.y(), currentDiff.x())
        angleDiff = currentAngle - startAngle
        snap = 15 * M_PI / 180  # 15 degrees in radians
        if (modifiers & Qt.ControlModifier):
            angleDiff = math.floor((angleDiff + snap / 2) / snap) * snap
        for object in self.mMovingObjects:
            mapObject = object.item.mapObject()
            offset = mapObject.objectGroup().offset()

            oldRelPos = object.oldItemPosition + offset - self.mOrigin
            sn = math.sin(angleDiff)
            cs = math.cos(angleDiff)
            newRelPos = QPointF(oldRelPos.x() * cs - oldRelPos.y() * sn,
                                oldRelPos.x() * sn + oldRelPos.y() * cs)
            newPixelPos = self.mOrigin + newRelPos - offset
            newPos = renderer.screenToPixelCoords_(newPixelPos)
            newRotation = object.oldRotation + angleDiff * 180 / M_PI
            mapObject.setPosition(newPos)
            mapObject.setRotation(newRotation)

        self.mapDocument().mapObjectModel().emitObjectsChanged(
            self.changingObjects())

    def finishRotating(self, pos):
        self.mAction = Action.NoAction
        self.updateHandles()
        if (self.mStart == pos):  # No rotation at all
            return
        undoStack = self.mapDocument().undoStack()
        undoStack.beginMacro(
            self.tr("Rotate %n Object(s)", "", self.mMovingObjects.size()))
        for object in self.mMovingObjects:
            mapObject = object.item.mapObject()
            undoStack.push(
                MoveMapObject(self.mapDocument(), mapObject,
                              object.oldPosition))
            undoStack.push(
                RotateMapObject(self.mapDocument(), mapObject,
                                object.oldRotation))

        undoStack.endMacro()
        self.mMovingObjects.clear()

    def startResizing(self):
        self.mAction = Action.Resizing
        self.mOrigin = self.mOriginIndicator.pos()
        self.mResizingLimitHorizontal = self.mClickedResizeHandle.resizingLimitHorizontal(
        )
        self.mResizingLimitVertical = self.mClickedResizeHandle.resizingLimitVertical(
        )
        self.mStart = self.mClickedResizeHandle.pos()
        self.saveSelectionState()
        self.updateHandleVisibility()

    def updateResizingItems(self, pos, modifiers):
        renderer = self.mapDocument().renderer()
        resizingOrigin = self.mClickedResizeHandle.resizingOrigin()
        if (modifiers & Qt.ShiftModifier):
            resizingOrigin = self.mOrigin
        self.mOriginIndicator.setPos(resizingOrigin)
        ## Alternative toggle snap modifier, since Control is taken by the preserve
        # aspect ratio option.
        ##
        snapHelper = SnapHelper(renderer)
        if (modifiers & Qt.AltModifier):
            snapHelper.toggleSnap()
        pixelPos = renderer.screenToPixelCoords_(pos)
        snapHelper.snap(pixelPos)
        snappedScreenPos = renderer.pixelToScreenCoords_(pixelPos)
        diff = snappedScreenPos - resizingOrigin
        startDiff = self.mStart - resizingOrigin
        if (self.mMovingObjects.size() == 1):
            ## For single items the resizing is performed in object space in order
            # to handle different scaling on X and Y axis as well as to improve
            # handling of 0-sized objects.
            ##
            self.updateResizingSingleItem(resizingOrigin, snappedScreenPos,
                                          modifiers)
            return

        ## Calculate the scaling factor. Minimum is 1% to protect against making
        # everything 0-sized and non-recoverable (it's still possibly to run into
        # problems by repeatedly scaling down to 1%, but that's asking for it)
        ##
        scale = 0.0
        if (self.mResizingLimitHorizontal):
            scale = max(0.01, diff.y() / startDiff.y())
        elif (self.mResizingLimitVertical):
            scale = max(0.01, diff.x() / startDiff.x())
        else:
            scale = min(max(0.01,
                            diff.x() / startDiff.x()),
                        max(0.01,
                            diff.y() / startDiff.y()))

        if not math.isfinite(scale):
            scale = 1

        for object in self.mMovingObjects:
            mapObject = object.item.mapObject()
            offset = mapObject.objectGroup().offset()

            oldRelPos = object.oldItemPosition + offset - resizingOrigin
            scaledRelPos = QPointF(oldRelPos.x() * scale,
                                   oldRelPos.y() * scale)
            newScreenPos = resizingOrigin + scaledRelPos - offset
            newPos = renderer.screenToPixelCoords_(newScreenPos)
            origSize = object.oldSize
            newSize = QSizeF(origSize.width() * scale,
                             origSize.height() * scale)
            if (mapObject.polygon().isEmpty() == False):
                # For polygons, we have to scale in object space.
                rotation = object.item.rotation() * M_PI / -180
                sn = math.sin(rotation)
                cs = math.cos(rotation)
                oldPolygon = object.oldPolygon
                newPolygon = QPolygonF(oldPolygon.size())
                for n in range(oldPolygon.size()):
                    oldPoint = QPointF(oldPolygon[n])
                    rotPoint = QPointF(oldPoint.x() * cs + oldPoint.y() * sn,
                                       oldPoint.y() * cs - oldPoint.x() * sn)
                    scaledPoint = QPointF(rotPoint.x() * scale,
                                          rotPoint.y() * scale)
                    newPoint = QPointF(
                        scaledPoint.x() * cs - scaledPoint.y() * sn,
                        scaledPoint.y() * cs + scaledPoint.x() * sn)
                    newPolygon[n] = newPoint

                mapObject.setPolygon(newPolygon)

            mapObject.setSize(newSize)
            mapObject.setPosition(newPos)

        self.mapDocument().mapObjectModel().emitObjectsChanged(
            self.changingObjects())

    def updateResizingSingleItem(self, resizingOrigin, screenPos, modifiers):
        renderer = self.mapDocument().renderer()
        object = self.mMovingObjects.first()
        mapObject = object.item.mapObject()

        ## The resizingOrigin, screenPos and mStart are affected by the ObjectGroup
        # offset. We will un-apply it to these variables since the resize for
        # single items happens in local coordinate space.
        ##
        offset = mapObject.objectGroup().offset()

        ## These transformations undo and redo the object rotation, which is always
        # applied in screen space.
        ##
        unrotate = rotateAt(object.oldItemPosition, -object.oldRotation)
        rotate = rotateAt(object.oldItemPosition, object.oldRotation)
        origin = (resizingOrigin - offset) * unrotate
        pos = (screenPos - offset) * unrotate
        start = (self.mStart - offset) * unrotate
        oldPos = object.oldItemPosition
        ## In order for the resizing to work somewhat sanely in isometric mode,
        # the resizing is performed in pixel space except for tile objects, which
        # are not affected by isometric projection apart from their position.
        ##
        pixelSpace = resizeInPixelSpace(mapObject)
        preserveAspect = modifiers & Qt.ControlModifier
        if (pixelSpace):
            origin = renderer.screenToPixelCoords_(origin)
            pos = renderer.screenToPixelCoords_(pos)
            start = renderer.screenToPixelCoords_(start)
            oldPos = object.oldPosition

        newPos = oldPos
        newSize = object.oldSize
        ## In case one of the anchors was used as-is, the desired size can be
        # derived directly from the distance from the origin for rectangle
        # and ellipse objects. This allows scaling up a 0-sized object without
        # dealing with infinite scaling factor issues.
        #
        # For obvious reasons this can't work on polygons or polylines, nor when
        # preserving the aspect ratio.
        ##
        if (self.mClickedResizeHandle.resizingOrigin() == resizingOrigin
                and (mapObject.shape() == MapObject.Rectangle
                     or mapObject.shape() == MapObject.Ellipse)
                and not preserveAspect):
            newBounds = QRectF(newPos, newSize)
            newBounds = align(newBounds, mapObject.alignment())
            x = self.mClickedResizeHandle.anchorPosition()
            if x == AnchorPosition.LeftAnchor or x == AnchorPosition.TopLeftAnchor or x == AnchorPosition.BottomLeftAnchor:
                newBounds.setLeft(min(pos.x(), origin.x()))
            elif x == AnchorPosition.RightAnchor or x == AnchorPosition.TopRightAnchor or x == AnchorPosition.BottomRightAnchor:
                newBounds.setRight(max(pos.x(), origin.x()))
            else:
                # nothing to do on this axis
                pass

            x = self.mClickedResizeHandle.anchorPosition()
            if x == AnchorPosition.TopAnchor or x == AnchorPosition.TopLeftAnchor or x == AnchorPosition.TopRightAnchor:
                newBounds.setTop(min(pos.y(), origin.y()))
            elif x == AnchorPosition.BottomAnchor or x == AnchorPosition.BottomLeftAnchor or x == AnchorPosition.BottomRightAnchor:
                newBounds.setBottom(max(pos.y(), origin.y()))
            else:
                # nothing to do on this axis
                pass

            newBounds = unalign(newBounds, mapObject.alignment())
            newSize = newBounds.size()
            newPos = newBounds.topLeft()
        else:
            relPos = pos - origin
            startDiff = start - origin
            try:
                newx = relPos.x() / startDiff.x()
            except:
                newx = 0
            try:
                newy = relPos.y() / startDiff.y()
            except:
                newy = 0
            scalingFactor = QSizeF(max(0.01, newx), max(0.01, newy))
            if not math.isfinite(scalingFactor.width()):
                scalingFactor.setWidth(1)
            if not math.isfinite(scalingFactor.height()):
                scalingFactor.setHeight(1)

            if (self.mResizingLimitHorizontal):
                if preserveAspect:
                    scalingFactor.setWidth(scalingFactor.height())
                else:
                    scalingFactor.setWidth(1)
            elif (self.mResizingLimitVertical):
                if preserveAspect:
                    scalingFactor.setHeight(scalingFactor.width())
                else:
                    scalingFactor.setHeight(1)
            elif (preserveAspect):
                scale = min(scalingFactor.width(), scalingFactor.height())
                scalingFactor.setWidth(scale)
                scalingFactor.setHeight(scale)

            oldRelPos = oldPos - origin
            newPos = origin + QPointF(oldRelPos.x() * scalingFactor.width(),
                                      oldRelPos.y() * scalingFactor.height())
            newSize.setWidth(newSize.width() * scalingFactor.width())
            newSize.setHeight(newSize.height() * scalingFactor.height())
            if (not object.oldPolygon.isEmpty()):
                newPolygon = QPolygonF(object.oldPolygon.size())
                for n in range(object.oldPolygon.size()):
                    point = object.oldPolygon[n]
                    newPolygon[n] = QPointF(point.x() * scalingFactor.width(),
                                            point.y() * scalingFactor.height())

                mapObject.setPolygon(newPolygon)

        if (pixelSpace):
            newPos = renderer.pixelToScreenCoords_(newPos)
        newPos = renderer.screenToPixelCoords_(newPos * rotate)
        mapObject.setSize(newSize)
        mapObject.setPosition(newPos)
        self.mapDocument().mapObjectModel().emitObjectsChanged(
            self.changingObjects())

    def finishResizing(self, pos):
        self.mAction = Action.NoAction
        self.updateHandles()
        if (self.mStart == pos):  # No scaling at all
            return
        undoStack = self.mapDocument().undoStack()
        undoStack.beginMacro(
            self.tr("Resize %n Object(s)", "", self.mMovingObjects.size()))
        for object in self.mMovingObjects:
            mapObject = object.item.mapObject()
            undoStack.push(
                MoveMapObject(self.mapDocument(), mapObject,
                              object.oldPosition))
            undoStack.push(
                ResizeMapObject(self.mapDocument(), mapObject, object.oldSize))
            if (not object.oldPolygon.isEmpty()):
                undoStack.push(
                    ChangePolygon(self.mapDocument(), mapObject,
                                  object.oldPolygon))

        undoStack.endMacro()
        self.mMovingObjects.clear()

    def setMode(self, mode):
        if (self.mMode != mode):
            self.mMode = mode
            self.updateHandles()

    def saveSelectionState(self):
        self.mMovingObjects.clear()
        # Remember the initial state before moving, resizing or rotating
        for item in self.mapScene().selectedObjectItems():
            mapObject = item.mapObject()
            object = MovingObject()
            object.item = item
            object.oldItemPosition = item.pos()
            object.oldPosition = mapObject.position()
            object.oldSize = mapObject.size()
            object.oldPolygon = mapObject.polygon()
            object.oldRotation = mapObject.rotation()

            self.mMovingObjects.append(object)

    def refreshCursor(self):
        cursorShape = Qt.ArrowCursor

        if self.mAction == Action.NoAction:
            hasSelection = not self.mapScene().selectedObjectItems().isEmpty()

            if ((self.mHoveredObjectItem or
                 ((self.mModifiers & Qt.AltModifier) and hasSelection))
                    and not (self.mModifiers & Qt.ShiftModifier)):
                cursorShape = Qt.SizeAllCursor
        elif self.mAction == Action.Moving:
            cursorShape = Qt.SizeAllCursor

        if self.cursor.shape != cursorShape:
            self.setCursor(cursorShape)

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

        return diff

    def changingObjects(self):
        changingObjects = QList()

        for movingObject in self.mMovingObjects:
            changingObjects.append(movingObject.item.mapObject())

        return changingObjects
Esempio n. 48
0
    def drawGrid(self, painter, exposed, gridColor):
        rect = exposed.toAlignedRect()
        if (rect.isNull()):
            return
        p = RenderParams(self.map())
        # Determine the tile and pixel coordinates to start at
        startTile = self.screenToTileCoords_(rect.topLeft()).toPoint()
        startPos = self.tileToScreenCoords_(startTile).toPoint()
        ## Determine in which half of the tile the top-left corner of the area we
        # need to draw is. If we're in the upper half, we need to start one row
        # up due to those tiles being visible as well. How we go up one row
        # depends on whether we're in the left or right half of the tile.
        ##
        inUpperHalf = rect.y() - startPos.y() < p.sideOffsetY
        inLeftHalf = rect.x() - startPos.x() < p.sideOffsetX
        if (inUpperHalf):
            startTile.setY(startTile.y() - 1)
        if (inLeftHalf):
            startTile.setX(startTile.x() - 1)
        startTile.setX(max(0, startTile.x()))
        startTile.setY(max(0, startTile.y()))
        startPos = self.tileToScreenCoords_(startTile).toPoint()
        oct = [
            QPoint(0, p.tileHeight - p.sideOffsetY),
            QPoint(0, p.sideOffsetY),
            QPoint(p.sideOffsetX, 0),
            QPoint(p.tileWidth - p.sideOffsetX, 0),
            QPoint(p.tileWidth, p.sideOffsetY),
            QPoint(p.tileWidth, p.tileHeight - p.sideOffsetY),
            QPoint(p.tileWidth - p.sideOffsetX, p.tileHeight),
            QPoint(p.sideOffsetX, p.tileHeight)]

        lines = QVector()
        #lines.reserve(8)
        gridColor.setAlpha(128)
        gridPen = QPen(gridColor)
        gridPen.setCosmetic(True)
        _x = QVector()
        _x.append(2)
        _x.append(2)
        gridPen.setDashPattern(_x)
        painter.setPen(gridPen)
        if (p.staggerX):
            # Odd row shifting is applied in the rendering loop, so un-apply it here
            if (p.doStaggerX(startTile.x())):
                startPos.setY(startPos.y() - p.rowHeight)
            while(startPos.x() <= rect.right() and startTile.x() < self.map().width()):
                rowTile = QPoint(startTile)
                rowPos = QPoint(startPos)
                if (p.doStaggerX(startTile.x())):
                    rowPos.setY(rowPos.y() + p.rowHeight)
                while(rowPos.y() <= rect.bottom() and rowTile.y() < self.map().height()):
                    lines.append(QLineF(rowPos + oct[1], rowPos + oct[2]))
                    lines.append(QLineF(rowPos + oct[2], rowPos + oct[3]))
                    lines.append(QLineF(rowPos + oct[3], rowPos + oct[4]))
                    isStaggered = p.doStaggerX(startTile.x())
                    lastRow = rowTile.y() == self.map().height() - 1
                    lastColumn = rowTile.x() == self.map().width() - 1
                    bottomLeft = rowTile.x() == 0 or (lastRow and isStaggered)
                    bottomRight = lastColumn or (lastRow and isStaggered)
                    if (bottomRight):
                        lines.append(QLineF(rowPos + oct[5], rowPos + oct[6]))
                    if (lastRow):
                        lines.append(QLineF(rowPos + oct[6], rowPos + oct[7]))
                    if (bottomLeft):
                        lines.append(QLineF(rowPos + oct[7], rowPos + oct[0]))
                    painter.drawLines(lines)
                    lines.resize(0)
                    rowPos.setY(rowPos.y() + p.tileHeight + p.sideLengthY)
                    rowTile.setY(rowTile.y() + 1)

                startPos.setX(startPos.x() + p.columnWidth)
                startTile.setX(startTile.x() + 1)
        else:
            # Odd row shifting is applied in the rendering loop, so un-apply it here
            if (p.doStaggerY(startTile.y())):
                startPos.setX(startPos.x() - p.columnWidth)
            while(startPos.y() <= rect.bottom() and startTile.y() < self.map().height()):
                rowTile = QPoint(startTile)
                rowPos = QPoint(startPos)
                if (p.doStaggerY(startTile.y())):
                    rowPos.setX(rowPos.x() + p.columnWidth)
                while(rowPos.x() <= rect.right() and rowTile.x() < self.map().width()):
                    lines.append(QLineF(rowPos + oct[0], rowPos + oct[1]))
                    lines.append(QLineF(rowPos + oct[1], rowPos + oct[2]))
                    lines.append(QLineF(rowPos + oct[3], rowPos + oct[4]))
                    isStaggered = p.doStaggerY(startTile.y())
                    lastRow = rowTile.y() == self.map().height() - 1
                    lastColumn = rowTile.x() == self.map().width() - 1
                    bottomLeft = lastRow or (rowTile.x() == 0 and not isStaggered)
                    bottomRight = lastRow or (lastColumn and isStaggered)
                    if (lastColumn):
                        lines.append(QLineF(rowPos + oct[4], rowPos + oct[5]))
                    if (bottomRight):
                        lines.append(QLineF(rowPos + oct[5], rowPos + oct[6]))
                    if (bottomLeft):
                        lines.append(QLineF(rowPos + oct[7], rowPos + oct[0]))
                    painter.drawLines(lines)
                    lines.resize(0)
                    rowPos.setX(rowPos.x() + p.tileWidth + p.sideLengthX)
                    rowTile.setX(rowTile.x() + 1)

                startPos.setY(startPos.y() + p.rowHeight)
                startTile.setY(startTile.y() + 1)
Esempio n. 49
0
    def recalculateTerrainDistances(self):
        # some fancy macros which can search for a value in each byte of a word simultaneously
        def hasZeroByte(dword):
            return (dword - 0x01010101) & ~dword & 0x80808080

        def hasByteEqualTo(dword, value):
            return hasZeroByte(dword ^ int(~0/255 * value))

        # Terrain distances are the number of transitions required before one terrain may meet another
        # Terrains that have no transition path have a distance of -1
        for i in range(self.terrainCount()):
            type = self.terrain(i)
            distance = QVector()
            for _x in range(self.terrainCount() + 1):
                distance.append(-1)
            # Check all tiles for transitions to other terrain types
            for j in range(self.tileCount()):
                t = self.tileAt(j)
                if (not hasByteEqualTo(t.terrain(), i)):
                    continue
                # This tile has transitions, add the transitions as neightbours (distance 1)
                tl = t.cornerTerrainId(0)
                tr = t.cornerTerrainId(1)
                bl = t.cornerTerrainId(2)
                br = t.cornerTerrainId(3)
                # Terrain on diagonally opposite corners are not actually a neighbour
                if (tl == i or br == i):
                    distance[tr + 1] = 1
                    distance[bl + 1] = 1

                if (tr == i or bl == i):
                    distance[tl + 1] = 1
                    distance[br + 1] = 1

                # terrain has at least one tile of its own type
                distance[i + 1] = 0

            type.setTransitionDistances(distance)

        # Calculate indirect transition distances
        bNewConnections = False
        # Repeat while we are still making new connections (could take a
        # number of iterations for distant terrain types to connect)
        while bNewConnections:
            bNewConnections = False
            # For each combination of terrain types
            for i in range(self.terrainCount()):
                t0 = self.terrain(i)
                for j in range(self.terrainCount()):
                    if (i == j):
                        continue
                    t1 = self.terrain(j)
                    # Scan through each terrain type, and see if we have any in common
                    for t in range(-1, self.terrainCount()):
                        d0 = t0.transitionDistance(t)
                        d1 = t1.transitionDistance(t)
                        if (d0 == -1 or d1 == -1):
                            continue
                        # We have cound a common connection
                        d = t0.transitionDistance(j)
                        # If the new path is shorter, record the new distance
                        if (d == -1 or d0 + d1 < d):
                            d = d0 + d1
                            t0.setTransitionDistance(j, d)
                            t1.setTransitionDistance(i, d)
                            # We're making progress, flag for another iteration...
                            bNewConnections = True
Esempio n. 50
0
class ObjectSelectionTool(AbstractObjectTool):
    def __init__(self, parent = None):
        super().__init__(self.tr("Select Objects"),
              QIcon(":images/22x22/tool-select-objects.png"),
              QKeySequence(self.tr("S")),
              parent)
        self.mSelectionRectangle = SelectionRectangle()
        self.mOriginIndicator = OriginIndicator()
        self.mMousePressed = False
        self.mHoveredObjectItem = None
        self.mClickedObjectItem = None
        self.mClickedRotateHandle = None
        self.mClickedResizeHandle = None
        self.mResizingLimitHorizontal = False
        self.mResizingLimitVertical = False
        self.mMode = Mode.Resize
        self.mAction = Action.NoAction
        self.mRotateHandles = [0, 0, 0, 0]
        self.mResizeHandles = [0, 0, 0, 0, 0, 0, 0, 0]
        self.mAlignPosition = QPointF()
        self.mMovingObjects = QVector()
        self.mScreenStart = QPoint()
        self.mStart = QPointF()
        self.mModifiers = 0
        self.mOrigin = QPointF()

        for i in range(AnchorPosition.CornerAnchorCount):
            self.mRotateHandles[i] = RotateHandle(i)
        for i in range(AnchorPosition.AnchorCount):
            self.mResizeHandles[i] = ResizeHandle(i)

    def __del__(self):
        if self.mSelectionRectangle.scene():
            self.mSelectionRectangle.scene().removeItem(self.mSelectionRectangle)
        if self.mOriginIndicator.scene():
            self.mOriginIndicator.scene().removeItem(self.mOriginIndicator)
        for i in range(AnchorPosition.CornerAnchorCount):
            handle = self.mRotateHandles[i]
            scene = handle.scene()
            if scene:
                scene.removeItem(handle)
        self.mRotateHandles.clear()
        for i in range(AnchorPosition.AnchorCount):
            handle = self.mResizeHandles[i]
            scene = handle.scene()
            if scene:
                scene.removeItem(handle)
        self.mResizeHandles.clear()

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

    def activate(self, scene):
        super().activate(scene)
        self.updateHandles()
        self.mapDocument().objectsChanged.connect(self.updateHandles)
        self.mapDocument().mapChanged.connect(self.updateHandles)
        scene.selectedObjectItemsChanged.connect(self.updateHandles)
        self.mapDocument().objectsRemoved.connect(self.objectsRemoved)
        if self.mOriginIndicator.scene() != scene:
            scene.addItem(self.mOriginIndicator)
        for i in range(AnchorPosition.CornerAnchorCount):
            handle = self.mRotateHandles[i]
            if handle.scene() != scene:
                scene.addItem(handle)
        for i in range(AnchorPosition.AnchorCount):
            handle = self.mResizeHandles[i]
            if handle.scene() != scene:
                scene.addItem(handle)

    def deactivate(self, scene):
        if self.mOriginIndicator.scene() == scene:
            scene.removeItem(self.mOriginIndicator)
        for i in range(AnchorPosition.CornerAnchorCount):
            handle = self.mRotateHandles[i]
            if handle.scene() == scene:
                scene.removeItem(handle)
        for i in range(AnchorPosition.AnchorCount):
            handle = self.mResizeHandles[i]
            if handle.scene() == scene:
                scene.removeItem(handle)
        self.mapDocument().objectsChanged.disconnect(self.updateHandles)
        self.mapDocument().mapChanged.disconnect(self.updateHandles)
        scene.selectedObjectItemsChanged.disconnect(self.updateHandles)
        super().deactivate(scene)

    def keyPressed(self, event):
        if (self.mAction != Action.NoAction):
            event.ignore()
            return

        moveBy = QPointF()
        x = event.key()
        if x==Qt.Key_Up:
            moveBy = QPointF(0, -1)
        elif x==Qt.Key_Down:
            moveBy = QPointF(0, 1)
        elif x==Qt.Key_Left:
            moveBy = QPointF(-1, 0)
        elif x==Qt.Key_Right:
            moveBy = QPointF(1, 0)
        else:
            super().keyPressed(event)
            return

        items = self.mapScene().selectedObjectItems()
        modifiers = event.modifiers()
        if (moveBy.isNull() or items.isEmpty() or (modifiers & Qt.ControlModifier)):
            event.ignore()
            return

        moveFast = modifiers & Qt.ShiftModifier
        snapToFineGrid = preferences.Preferences.instance().snapToFineGrid()
        if (moveFast):
            # TODO: This only makes sense for orthogonal maps
            moveBy.setX(moveBy.x() * self.mapDocument().map().tileWidth())
            moveBy.setX(moveBy.y() * self.mapDocument().map().tileHeight())
            if (snapToFineGrid):
                moveBy /= preferences.Preferences.instance().gridFine()

        undoStack = self.mapDocument().undoStack()
        undoStack.beginMacro(self.tr("Move %n Object(s)", "", items.size()))
        i = 0
        for objectItem in items:
            object = objectItem.mapObject()
            oldPos = object.position()
            newPos = oldPos + moveBy
            undoStack.push(MoveMapObject(self.mapDocument(), object, newPos, oldPos))
            i += 1

        undoStack.endMacro()

    def mouseEntered(self):
        pass
        
    def mouseMoved(self, pos, modifiers):
        super().mouseMoved(pos, modifiers)
        
        # Update the hovered item (for mouse cursor)
        hoveredRotateHandle = None
        hoveredResizeHandle = None
        hoveredObjectItem = None
        
        view = self.mapScene().views()[0]
        if view:
            hoveredItem = self.mapScene().itemAt(pos,view.transform())
            hoveredRotateHandle = None
            hoveredResizeHandle = None
            tp = type(hoveredItem)
            if tp==RotateHandle:
                hoveredRotateHandle = hoveredItem
            elif tp==ResizeHandle:
                hoveredResizeHandle = hoveredItem

        if (not hoveredRotateHandle and not hoveredResizeHandle):
            hoveredObjectItem = self.topMostObjectItemAt(pos)

        self.mHoveredObjectItem = hoveredObjectItem
        
        if (self.mAction == Action.NoAction and self.mMousePressed):
            screenPos = QCursor.pos()
            dragDistance = (self.mScreenStart - screenPos).manhattanLength()
            if (dragDistance >= QApplication.startDragDistance()):
                hasSelection = not self.mapScene().selectedObjectItems().isEmpty()
                # Holding Alt forces moving current selection
                # Holding Shift forces selection rectangle
                if ((self.mClickedObjectItem or (modifiers & Qt.AltModifier) and hasSelection) and not (modifiers & Qt.ShiftModifier)):
                    self.startMoving(modifiers)
                elif (self.mClickedRotateHandle):
                    self.startRotating()
                elif (self.mClickedResizeHandle):
                    self.startResizing()
                else:
                    self.startSelecting()

        x = self.mAction
        if x==Action.Selecting:
            self.mSelectionRectangle.setRectangle(QRectF(self.mStart, pos).normalized())
        elif x==Action.Moving:
            self.updateMovingItems(pos, modifiers)
        elif x==Action.Rotating:
            self.updateRotatingItems(pos, modifiers)
        elif x==Action.Resizing:
            self.updateResizingItems(pos, modifiers)
        elif x==Action.NoAction:
            pass
        self.refreshCursor()

    def mousePressed(self, event):
        if (self.mAction != Action.NoAction): # 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()
            clickedRotateHandle = 0
            clickedResizeHandle = 0
            view = findView(event)
            if view:
                clickedItem = self.mapScene().itemAt(event.scenePos(), view.transform())
                clickedRotateHandle = None
                clickedResizeHandle = None
                tp = type(clickedItem)
                if tp==RotateHandle:
                    clickedRotateHandle = clickedItem
                elif tp==ResizeHandle:
                    clickedResizeHandle = clickedItem
            self.mClickedRotateHandle = clickedRotateHandle
            self.mClickedResizeHandle = clickedResizeHandle
            if (not clickedRotateHandle and not clickedResizeHandle):
                self.mClickedObjectItem = self.topMostObjectItemAt(self.mStart)
        else:
            super().mousePressed(event)

    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()
        
    def modifiersChanged(self, modifiers):
        self.mModifiers = modifiers
        self.refreshCursor()

    def languageChanged(self):
        self.setName(self.tr("Select Objects"))
        self.setShortcut(QKeySequence(self.tr("S")))

    def updateHandles(self):
        if (self.mAction == Action.Moving or self.mAction == Action.Rotating or self.mAction == Action.Resizing):
            return
        objects = self.mapDocument().selectedObjects()
        showHandles = objects.size() > 0
        if (showHandles):
            renderer = self.mapDocument().renderer()
            boundingRect = objectBounds(objects.first(), renderer, objectTransform(objects.first(), renderer))
            for i in range(1, objects.size()):
                object = objects.at(i)
                boundingRect |= objectBounds(object, renderer, objectTransform(object, renderer))

            topLeft = boundingRect.topLeft()
            topRight = boundingRect.topRight()
            bottomLeft = boundingRect.bottomLeft()
            bottomRight = boundingRect.bottomRight()
            center = boundingRect.center()
            handleRotation = 0
            # If there is only one object selected, align to its orientation.
            if (objects.size() == 1):
                object = objects.first()
                handleRotation = object.rotation()
                if (resizeInPixelSpace(object)):
                    bounds = pixelBounds(object)
                    transform = QTransform(objectTransform(object, renderer))
                    topLeft = transform.map(renderer.pixelToScreenCoords_(bounds.topLeft()))
                    topRight = transform.map(renderer.pixelToScreenCoords_(bounds.topRight()))
                    bottomLeft = transform.map(renderer.pixelToScreenCoords_(bounds.bottomLeft()))
                    bottomRight = transform.map(renderer.pixelToScreenCoords_(bounds.bottomRight()))
                    center = transform.map(renderer.pixelToScreenCoords_(bounds.center()))
                    # Ugly hack to make handles appear nicer in this case
                    if (self.mapDocument().map().orientation() == Map.Orientation.Isometric):
                        handleRotation += 45
                else:
                    bounds = objectBounds(object, renderer, QTransform())
                    transform = QTransform(objectTransform(object, renderer))
                    topLeft = transform.map(bounds.topLeft())
                    topRight = transform.map(bounds.topRight())
                    bottomLeft = transform.map(bounds.bottomLeft())
                    bottomRight = transform.map(bounds.bottomRight())
                    center = transform.map(bounds.center())

            self.mOriginIndicator.setPos(center)
            self.mRotateHandles[AnchorPosition.TopLeftAnchor].setPos(topLeft)
            self.mRotateHandles[AnchorPosition.TopRightAnchor].setPos(topRight)
            self.mRotateHandles[AnchorPosition.BottomLeftAnchor].setPos(bottomLeft)
            self.mRotateHandles[AnchorPosition.BottomRightAnchor].setPos(bottomRight)
            top = (topLeft + topRight) / 2
            left = (topLeft + bottomLeft) / 2
            right = (topRight + bottomRight) / 2
            bottom = (bottomLeft + bottomRight) / 2
            self.mResizeHandles[AnchorPosition.TopAnchor].setPos(top)
            self.mResizeHandles[AnchorPosition.TopAnchor].setResizingOrigin(bottom)
            self.mResizeHandles[AnchorPosition.LeftAnchor].setPos(left)
            self.mResizeHandles[AnchorPosition.LeftAnchor].setResizingOrigin(right)
            self.mResizeHandles[AnchorPosition.RightAnchor].setPos(right)
            self.mResizeHandles[AnchorPosition.RightAnchor].setResizingOrigin(left)
            self.mResizeHandles[AnchorPosition.BottomAnchor].setPos(bottom)
            self.mResizeHandles[AnchorPosition.BottomAnchor].setResizingOrigin(top)
            self.mResizeHandles[AnchorPosition.TopLeftAnchor].setPos(topLeft)
            self.mResizeHandles[AnchorPosition.TopLeftAnchor].setResizingOrigin(bottomRight)
            self.mResizeHandles[AnchorPosition.TopRightAnchor].setPos(topRight)
            self.mResizeHandles[AnchorPosition.TopRightAnchor].setResizingOrigin(bottomLeft)
            self.mResizeHandles[AnchorPosition.BottomLeftAnchor].setPos(bottomLeft)
            self.mResizeHandles[AnchorPosition.BottomLeftAnchor].setResizingOrigin(topRight)
            self.mResizeHandles[AnchorPosition.BottomRightAnchor].setPos(bottomRight)
            self.mResizeHandles[AnchorPosition.BottomRightAnchor].setResizingOrigin(topLeft)
            for i in range(AnchorPosition.CornerAnchorCount):
                self.mRotateHandles[i].setRotation(handleRotation)
            for i in range(AnchorPosition.AnchorCount):
                self.mResizeHandles[i].setRotation(handleRotation)

        self.updateHandleVisibility()

    def updateHandleVisibility(self):
        hasSelection = not self.mapDocument().selectedObjects().isEmpty()
        showHandles = hasSelection and (self.mAction == Action.NoAction or self.mAction == Action.Selecting)
        showOrigin = hasSelection and self.mAction != Action.Moving and (self.mMode == Mode.Rotate or self.mAction == Action.Resizing)
        for i in range(AnchorPosition.CornerAnchorCount):
            self.mRotateHandles[i].setVisible(showHandles and self.mMode == Mode.Rotate)
        for i in range(AnchorPosition.AnchorCount):
            self.mResizeHandles[i].setVisible(showHandles and self.mMode == Mode.Resize)
        self.mOriginIndicator.setVisible(showOrigin)

    def objectsRemoved(self, objects):
        if (self.mAction != Action.Moving and self.mAction != Action.Rotating and self.mAction != Action.Resizing):
            return
        # Abort move/rotate/resize to avoid crashing...
        # TODO: This should really not be allowed to happen in the first place.
        # since it breaks the undo history, for example.
        for i in range(self.mMovingObjects.size() - 1, -1, -1):
            object = self.mMovingObjects[i]
            mapObject = object.item.mapObject()
            if objects.contains(mapObject):
                # Avoid referencing the removed object
                self.mMovingObjects.remove(i)
            else:
                mapObject.setPosition(object.oldPosition)
                mapObject.setSize(object.oldSize)
                mapObject.setPolygon(object.oldPolygon)
                mapObject.setRotation(object.oldRotation)
        
        self.mapDocument().mapObjectModel().emitObjectsChanged(self.changingObjects)
        self.mMovingObjects.clear()

    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 startSelecting(self):
        self.mAction = Action.Selecting
        self.mapScene().addItem(self.mSelectionRectangle)

    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()

    def updateMovingItems(self, pos, modifiers):
        renderer = self.mapDocument().renderer()

        diff = self.snapToGrid(pos-self.mStart, modifiers)
        for object in self.mMovingObjects:
            newPixelPos = object.oldItemPosition + diff
            newPos = renderer.screenToPixelCoords_(newPixelPos)

            mapObject = object.item.mapObject()
            mapObject.setPosition(newPos)
        self.mapDocument().mapObjectModel().emitObjectsChanged(self.changingObjects())
        
    def finishMoving(self, pos):
        self.mAction = Action.NoAction
        self.updateHandles()
        if (self.mStart == pos): # Move is a no-op
            return
        undoStack = self.mapDocument().undoStack()
        undoStack.beginMacro(self.tr("Move %n Object(s)", "", self.mMovingObjects.size()))
        for object in self.mMovingObjects:
            undoStack.push(MoveMapObject(self.mapDocument(), object.item.mapObject(), object.oldPosition))

        undoStack.endMacro()
        self.mMovingObjects.clear()

    def startRotating(self):
        self.mAction = Action.Rotating
        self.mOrigin = self.mOriginIndicator.pos()
        self.saveSelectionState()
        self.updateHandleVisibility()

    def updateRotatingItems(self, pos, modifiers):
        renderer = self.mapDocument().renderer()
        startDiff = self.mOrigin - self.mStart
        currentDiff = self.mOrigin - pos
        startAngle = math.atan2(startDiff.y(), startDiff.x())
        currentAngle = math.atan2(currentDiff.y(), currentDiff.x())
        angleDiff = currentAngle - startAngle
        snap = 15 * M_PI / 180 # 15 degrees in radians
        if (modifiers & Qt.ControlModifier):
            angleDiff = math.floor((angleDiff + snap / 2) / snap) * snap
        for object in self.mMovingObjects:
            mapObject = object.item.mapObject()
            offset = mapObject.objectGroup().offset()
        
            oldRelPos = object.oldItemPosition + offset - self.mOrigin
            sn = math.sin(angleDiff)
            cs = math.cos(angleDiff)
            newRelPos = QPointF(oldRelPos.x() * cs - oldRelPos.y() * sn, oldRelPos.x() * sn + oldRelPos.y() * cs)
            newPixelPos = self.mOrigin + newRelPos - offset
            newPos = renderer.screenToPixelCoords_(newPixelPos)
            newRotation = object.oldRotation + angleDiff * 180 / M_PI
            mapObject.setPosition(newPos)
            mapObject.setRotation(newRotation)
        
        self.mapDocument().mapObjectModel().emitObjectsChanged(self.changingObjects())
        
    def finishRotating(self, pos):
        self.mAction = Action.NoAction
        self.updateHandles()
        if (self.mStart == pos): # No rotation at all
            return
        undoStack = self.mapDocument().undoStack()
        undoStack.beginMacro(self.tr("Rotate %n Object(s)", "", self.mMovingObjects.size()))
        for object in self.mMovingObjects:
            mapObject = object.item.mapObject()
            undoStack.push(MoveMapObject(self.mapDocument(), mapObject, object.oldPosition))
            undoStack.push(RotateMapObject(self.mapDocument(), mapObject, object.oldRotation))

        undoStack.endMacro()
        self.mMovingObjects.clear()

    def startResizing(self):
        self.mAction = Action.Resizing
        self.mOrigin = self.mOriginIndicator.pos()
        self.mResizingLimitHorizontal = self.mClickedResizeHandle.resizingLimitHorizontal()
        self.mResizingLimitVertical = self.mClickedResizeHandle.resizingLimitVertical()
        self.mStart = self.mClickedResizeHandle.pos()
        self.saveSelectionState()
        self.updateHandleVisibility()

    def updateResizingItems(self, pos, modifiers):
        renderer = self.mapDocument().renderer()
        resizingOrigin = self.mClickedResizeHandle.resizingOrigin()
        if (modifiers & Qt.ShiftModifier):
            resizingOrigin = self.mOrigin
        self.mOriginIndicator.setPos(resizingOrigin)
        ## Alternative toggle snap modifier, since Control is taken by the preserve
        # aspect ratio option.
        ##
        snapHelper = SnapHelper(renderer)
        if (modifiers & Qt.AltModifier):
            snapHelper.toggleSnap()
        pixelPos = renderer.screenToPixelCoords_(pos)
        snapHelper.snap(pixelPos)
        snappedScreenPos = renderer.pixelToScreenCoords_(pixelPos)
        diff = snappedScreenPos - resizingOrigin
        startDiff = self.mStart - resizingOrigin
        if (self.mMovingObjects.size() == 1):
            ## For single items the resizing is performed in object space in order
            # to handle different scaling on X and Y axis as well as to improve
            # handling of 0-sized objects.
            ##
            self.updateResizingSingleItem(resizingOrigin, snappedScreenPos, modifiers)
            return

        ## Calculate the scaling factor. Minimum is 1% to protect against making
        # everything 0-sized and non-recoverable (it's still possibly to run into
        # problems by repeatedly scaling down to 1%, but that's asking for it)
        ##
        scale = 0.0
        if (self.mResizingLimitHorizontal):
            scale = max(0.01, diff.y() / startDiff.y())
        elif (self.mResizingLimitVertical):
            scale = max(0.01, diff.x() / startDiff.x())
        else:
            scale = min(max(0.01, diff.x() / startDiff.x()),
                         max(0.01, diff.y() / startDiff.y()))

        if not math.isfinite(scale):
            scale = 1
        
        for object in self.mMovingObjects:
            mapObject = object.item.mapObject()
            offset = mapObject.objectGroup().offset()
        
            oldRelPos = object.oldItemPosition + offset - resizingOrigin
            scaledRelPos = QPointF(oldRelPos.x() * scale, oldRelPos.y() * scale)
            newScreenPos = resizingOrigin + scaledRelPos - offset
            newPos = renderer.screenToPixelCoords_(newScreenPos)
            origSize = object.oldSize
            newSize = QSizeF(origSize.width() * scale, origSize.height() * scale)
            if (mapObject.polygon().isEmpty() == False):
                # For polygons, we have to scale in object space.
                rotation = object.item.rotation() * M_PI / -180
                sn = math.sin(rotation)
                cs = math.cos(rotation)
                oldPolygon = object.oldPolygon
                newPolygon = QPolygonF(oldPolygon.size())
                for n in range(oldPolygon.size()):
                    oldPoint = QPointF(oldPolygon[n])
                    rotPoint = QPointF(oldPoint.x() * cs + oldPoint.y() * sn, oldPoint.y() * cs - oldPoint.x() * sn)
                    scaledPoint = QPointF(rotPoint.x() * scale, rotPoint.y() * scale)
                    newPoint = QPointF(scaledPoint.x() * cs - scaledPoint.y() * sn, scaledPoint.y() * cs + scaledPoint.x() * sn)
                    newPolygon[n] = newPoint

                mapObject.setPolygon(newPolygon)

            mapObject.setSize(newSize)
            mapObject.setPosition(newPos)
        
        self.mapDocument().mapObjectModel().emitObjectsChanged(self.changingObjects())
        
    def updateResizingSingleItem(self, resizingOrigin, screenPos, modifiers):
        renderer = self.mapDocument().renderer()
        object = self.mMovingObjects.first()
        mapObject = object.item.mapObject()
        
        ## The resizingOrigin, screenPos and mStart are affected by the ObjectGroup
        # offset. We will un-apply it to these variables since the resize for
        # single items happens in local coordinate space.
        ##
        offset = mapObject.objectGroup().offset()
    
        ## These transformations undo and redo the object rotation, which is always
        # applied in screen space.
        ##
        unrotate = rotateAt(object.oldItemPosition, -object.oldRotation)
        rotate = rotateAt(object.oldItemPosition, object.oldRotation)
        origin = (resizingOrigin - offset) * unrotate
        pos = (screenPos - offset) * unrotate
        start = (self.mStart - offset) * unrotate
        oldPos = object.oldItemPosition
        ## In order for the resizing to work somewhat sanely in isometric mode,
        # the resizing is performed in pixel space except for tile objects, which
        # are not affected by isometric projection apart from their position.
        ##
        pixelSpace = resizeInPixelSpace(mapObject)
        preserveAspect = modifiers & Qt.ControlModifier
        if (pixelSpace):
            origin = renderer.screenToPixelCoords_(origin)
            pos = renderer.screenToPixelCoords_(pos)
            start = renderer.screenToPixelCoords_(start)
            oldPos = object.oldPosition

        newPos = oldPos
        newSize = object.oldSize
        ## In case one of the anchors was used as-is, the desired size can be
        # derived directly from the distance from the origin for rectangle
        # and ellipse objects. This allows scaling up a 0-sized object without
        # dealing with infinite scaling factor issues.
        #
        # For obvious reasons this can't work on polygons or polylines, nor when
        # preserving the aspect ratio.
        ##
        if (self.mClickedResizeHandle.resizingOrigin() == resizingOrigin and (mapObject.shape() == MapObject.Rectangle or
                 mapObject.shape() == MapObject.Ellipse) and not preserveAspect):
            newBounds = QRectF(newPos, newSize)
            newBounds = align(newBounds, mapObject.alignment())
            x = self.mClickedResizeHandle.anchorPosition()
            if x==AnchorPosition.LeftAnchor or x==AnchorPosition.TopLeftAnchor or x==AnchorPosition.BottomLeftAnchor:
                newBounds.setLeft(min(pos.x(), origin.x()))
            elif x==AnchorPosition.RightAnchor or x==AnchorPosition.TopRightAnchor or x==AnchorPosition.BottomRightAnchor:
                newBounds.setRight(max(pos.x(), origin.x()))
            else:
                # nothing to do on this axis
                pass

            x = self.mClickedResizeHandle.anchorPosition()
            if x==AnchorPosition.TopAnchor or x==AnchorPosition.TopLeftAnchor or x==AnchorPosition.TopRightAnchor:
                newBounds.setTop(min(pos.y(), origin.y()))
            elif x==AnchorPosition.BottomAnchor or x==AnchorPosition.BottomLeftAnchor or x==AnchorPosition.BottomRightAnchor:
                newBounds.setBottom(max(pos.y(), origin.y()))
            else:
                # nothing to do on this axis
                pass

            newBounds = unalign(newBounds, mapObject.alignment())
            newSize = newBounds.size()
            newPos = newBounds.topLeft()
        else:
            relPos = pos - origin
            startDiff = start - origin
            try:
                newx = relPos.x() / startDiff.x()
            except:
                newx = 0
            try:
                newy = relPos.y() / startDiff.y()
            except:
                newy = 0
            scalingFactor = QSizeF(max(0.01, newx), max(0.01, newy))
            if not math.isfinite(scalingFactor.width()):
                scalingFactor.setWidth(1)
            if not math.isfinite(scalingFactor.height()):
                scalingFactor.setHeight(1)
            
            if (self.mResizingLimitHorizontal):
                if preserveAspect:
                    scalingFactor.setWidth(scalingFactor.height())
                else:
                    scalingFactor.setWidth(1)
            elif (self.mResizingLimitVertical):
                if preserveAspect:
                    scalingFactor.setHeight(scalingFactor.width())
                else:
                    scalingFactor.setHeight(1)
            elif (preserveAspect):
                scale = min(scalingFactor.width(), scalingFactor.height())
                scalingFactor.setWidth(scale)
                scalingFactor.setHeight(scale)

            oldRelPos = oldPos - origin
            newPos = origin + QPointF(oldRelPos.x() * scalingFactor.width(), oldRelPos.y() * scalingFactor.height())
            newSize.setWidth(newSize.width() * scalingFactor.width())
            newSize.setHeight(newSize.height() * scalingFactor.height())
            if (not object.oldPolygon.isEmpty()):
                newPolygon = QPolygonF(object.oldPolygon.size())
                for n in range(object.oldPolygon.size()):
                    point = object.oldPolygon[n]
                    newPolygon[n] = QPointF(point.x() * scalingFactor.width(), point.y() * scalingFactor.height())

                mapObject.setPolygon(newPolygon)

        if (pixelSpace):
            newPos = renderer.pixelToScreenCoords_(newPos)
        newPos = renderer.screenToPixelCoords_(newPos * rotate)
        mapObject.setSize(newSize)
        mapObject.setPosition(newPos)
        self.mapDocument().mapObjectModel().emitObjectsChanged(self.changingObjects())
        
    def finishResizing(self, pos):
        self.mAction = Action.NoAction
        self.updateHandles()
        if (self.mStart == pos): # No scaling at all
            return
        undoStack = self.mapDocument().undoStack()
        undoStack.beginMacro(self.tr("Resize %n Object(s)", "", self.mMovingObjects.size()))
        for object in self.mMovingObjects:
            mapObject = object.item.mapObject()
            undoStack.push(MoveMapObject(self.mapDocument(), mapObject, object.oldPosition))
            undoStack.push(ResizeMapObject(self.mapDocument(), mapObject, object.oldSize))
            if (not object.oldPolygon.isEmpty()):
                undoStack.push(ChangePolygon(self.mapDocument(), mapObject, object.oldPolygon))

        undoStack.endMacro()
        self.mMovingObjects.clear()

    def setMode(self, mode):
        if (self.mMode != mode):
            self.mMode = mode
            self.updateHandles()

    def saveSelectionState(self):
        self.mMovingObjects.clear()
        # Remember the initial state before moving, resizing or rotating
        for item in self.mapScene().selectedObjectItems():
            mapObject = item.mapObject()
            object = MovingObject()
            object.item = item
            object.oldItemPosition = item.pos()
            object.oldPosition = mapObject.position()
            object.oldSize = mapObject.size()
            object.oldPolygon = mapObject.polygon()
            object.oldRotation = mapObject.rotation()

            self.mMovingObjects.append(object)

    def refreshCursor(self):
        cursorShape = Qt.ArrowCursor

        if self.mAction == Action.NoAction:
            hasSelection = not self.mapScene().selectedObjectItems().isEmpty()

            if ((self.mHoveredObjectItem or ((self.mModifiers & Qt.AltModifier) and hasSelection)) and not (self.mModifiers & Qt.ShiftModifier)):
                cursorShape = Qt.SizeAllCursor
        elif self.mAction == Action.Moving:
            cursorShape = Qt.SizeAllCursor

        if self.cursor.shape != cursorShape:
            self.setCursor(cursorShape)

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

        return diff

    def changingObjects(self):
        changingObjects = QList()
        
        for movingObject in self.mMovingObjects:
            changingObjects.append(movingObject.item.mapObject())

        return changingObjects
Esempio n. 51
0
class TileLayer(Layer):
    ##
    # Constructor.
    ##
    def __init__(self, name, x, y, width, height):
        super().__init__(Layer.TileLayerType, name, x, y, width, height)
        self.mMaxTileSize = QSize(0, 0)
        self.mGrid = QVector()
        for i in range(width * height):
            self.mGrid.append(Cell())
        self.mOffsetMargins = QMargins()

    def __iter__(self):
        return self.mGrid.__iter__()
        
    ##
    # Returns the maximum tile size of this layer.
    ##
    def maxTileSize(self):
        return self.mMaxTileSize

    ##
    # Returns the margins that have to be taken into account while drawing
    # this tile layer. The margins depend on the maximum tile size and the
    # offset applied to the tiles.
    ##
    def drawMargins(self):
        return QMargins(self.mOffsetMargins.left(),
                        self.mOffsetMargins.top() + self.mMaxTileSize.height(),
                        self.mOffsetMargins.right() + self.mMaxTileSize.width(),
                        self.mOffsetMargins.bottom())

    ##
    # Recomputes the draw margins. Needed after the tile offset of a tileset
    # has changed for example.
    #
    # Generally you want to call Map.recomputeDrawMargins instead.
    ##
    def recomputeDrawMargins(self):
        maxTileSize = QSize(0, 0)
        offsetMargins = QMargins()
        i = 0
        while(i<self.mGrid.size()):
            cell = self.mGrid.at(i)
            tile = cell.tile
            if tile:
                size = tile.size()
                if (cell.flippedAntiDiagonally):
                    size.transpose()
                offset = tile.offset()
                maxTileSize = maxSize(size, maxTileSize)
                offsetMargins = maxMargins(QMargins(-offset.x(),
                                                     -offset.y(),
                                                     offset.x(),
                                                     offset.y()),
                                            offsetMargins)
            i += 1

        self.mMaxTileSize = maxTileSize
        self.mOffsetMargins = offsetMargins
        if (self.mMap):
            self.mMap.adjustDrawMargins(self.drawMargins())

    ##
    # Returns whether (x, y) is inside this map layer.
    ##
    def contains(self, *args):
        l = len(args)
        if l==2:
            x, y = args
            return x >= 0 and y >= 0 and x < self.mWidth and y < self.mHeight
        elif l==1:
            point = args[0]
            return self.contains(point.x(), point.y())

    ##
    # Calculates the region of cells in this tile layer for which the given
    # \a condition returns True.
    ##
    def region(self, *args):
        l = len(args)
        if l==1:
            condition = args[0]
            region = QRegion()
            for y in range(self.mHeight):
                for x in range(self.mWidth):
                    if (condition(self.cellAt(x, y))):
                        rangeStart = x
                        x += 1
                        while(x<=self.mWidth):
                            if (x == self.mWidth or not condition(self.cellAt(x, y))):
                                rangeEnd = x
                                region += QRect(rangeStart + self.mX, y + self.mY,
                                                rangeEnd - rangeStart, 1)
                                break
                            x += 1

            return region
        elif l==0:
            ##
            # Calculates the region occupied by the tiles of this layer. Similar to
            # Layer.bounds(), but leaves out the regions without tiles.
            ##
            return self.region(lambda cell:not cell.isEmpty())

    ##
    # Returns a read-only reference to the cell at the given coordinates. The
    # coordinates have to be within this layer.
    ##
    def cellAt(self, *args):
        l = len(args)
        if l==2:
            x, y = args
            return self.mGrid.at(x + y * self.mWidth)
        elif l==1:
            point = args[0]
            return self.cellAt(point.x(), point.y())

    ##
    # Sets the cell at the given coordinates.
    ##
    def setCell(self, x, y, cell):
        if (cell.tile):
            size = cell.tile.size()
            if (cell.flippedAntiDiagonally):
                size.transpose()
            offset = cell.tile.offset()
            self.mMaxTileSize = maxSize(size, self.mMaxTileSize)
            self.mOffsetMargins = maxMargins(QMargins(-offset.x(),
                                                 -offset.y(),
                                                 offset.x(),
                                                 offset.y()),
                                        self.mOffsetMargins)
            if (self.mMap):
                self.mMap.adjustDrawMargins(self.drawMargins())

        self.mGrid[x + y * self.mWidth] = cell

    ##
    # Returns a copy of the area specified by the given \a region. The
    # caller is responsible for the returned tile layer.
    ##
    def copy(self, *args):
        l = len(args)
        if l==1:
            region = args[0]
            if type(region) != QRegion:
                region = QRegion(region)
            area = region.intersected(QRect(0, 0, self.width(), self.height()))
            bounds = region.boundingRect()
            areaBounds = area.boundingRect()
            offsetX = max(0, areaBounds.x() - bounds.x())
            offsetY = max(0, areaBounds.y() - bounds.y())
            copied = TileLayer(QString(), 0, 0, bounds.width(), bounds.height())
            for rect in area.rects():
                for x in range(rect.left(), rect.right()+1):
                    for y in range(rect.top(), rect.bottom()+1):
                        copied.setCell(x - areaBounds.x() + offsetX,
                                        y - areaBounds.y() + offsetY,
                                        self.cellAt(x, y))
            return copied
        elif l==4:
            x, y, width, height = args

            return self.copy(QRegion(x, y, width, height))

    ##
    # Merges the given \a layer onto this layer at position \a pos. Parts that
    # fall outside of this layer will be lost and empty tiles in the given
    # layer will have no effect.
    ##
    def merge(self, pos, layer):
        # Determine the overlapping area
        area = QRect(pos, QSize(layer.width(), layer.height()))
        area &= QRect(0, 0, self.width(), self.height())
        for y in range(area.top(), area.bottom()+1):
            for x in range(area.left(), area.right()+1):
                cell = layer.cellAt(x - pos.x(), y - pos.y())
                if (not cell.isEmpty()):
                    self.setCell(x, y, cell)

    ##
    # Removes all cells in the specified region.
    ##
    def erase(self, area):
        emptyCell = Cell()
        for rect in area.rects():
            for x in range(rect.left(), rect.right()+1):
                for y in range(rect.top(), rect.bottom()+1):
                    self.setCell(x, y, emptyCell)

    ##
    # Sets the cells starting at the given position to the cells in the given
    # \a tileLayer. Parts that fall outside of this layer will be ignored.
    #
    # When a \a mask is given, only cells that fall within this mask are set.
    # The mask is applied in local coordinates.
    ##
    def setCells(self, x, y, layer, mask = QRegion()):
        # Determine the overlapping area
        area = QRegion(QRect(x, y, layer.width(), layer.height()))
        area &= QRect(0, 0, self.width(), self.height())
        if (not mask.isEmpty()):
            area &= mask
        for rect in area.rects():
            for _x in range(rect.left(), rect.right()+1):
                for _y in range(rect.top(), rect.bottom()+1):
                    self.setCell(_x, _y, layer.cellAt(_x - x, _y - y))

    ##
    # Flip this tile layer in the given \a direction. Direction must be
    # horizontal or vertical. This doesn't change the dimensions of the
    # tile layer.
    ##
    def flip(self, direction):
        newGrid = QVector()
        for i in range(self.mWidth * self.mHeight):
            newGrid.append(Cell())
        for y in range(self.mHeight):
            for x in range(self.mWidth):
                dest = newGrid[x + y * self.mWidth]
                if (direction == FlipDirection.FlipHorizontally):
                    source = self.cellAt(self.mWidth - x - 1, y)
                    dest = source
                    dest.flippedHorizontally = not source.flippedHorizontally
                elif (direction == FlipDirection.FlipVertically):
                    source = self.cellAt(x, self.mHeight - y - 1)
                    dest = source
                    dest.flippedVertically = not source.flippedVertically

        self.mGrid = newGrid

    ##
    # Rotate this tile layer by 90 degrees left or right. The tile positions
    # are rotated within the layer, and the tiles themselves are rotated. The
    # dimensions of the tile layer are swapped.
    ##
    def rotate(self, direction):
        rotateRightMask = [5, 4, 1, 0, 7, 6, 3, 2]
        rotateLeftMask  = [3, 2, 7, 6, 1, 0, 5, 4]
        if direction == RotateDirection.RotateRight:
            rotateMask = rotateRightMask
        else:
            rotateMask = rotateLeftMask
        newWidth = self.mHeight
        newHeight = self.mWidth
        newGrid = QVector(newWidth * newHeight)
        for y in range(self.mHeight):
            for x in range(self.mWidth):
                source = self.cellAt(x, y)
                dest = source
                mask = (dest.flippedHorizontally << 2) | (dest.flippedVertically << 1) | (dest.flippedAntiDiagonally << 0)
                mask = rotateMask[mask]
                dest.flippedHorizontally = (mask & 4) != 0
                dest.flippedVertically = (mask & 2) != 0
                dest.flippedAntiDiagonally = (mask & 1) != 0
                if (direction == RotateDirection.RotateRight):
                    newGrid[x * newWidth + (self.mHeight - y - 1)] = dest
                else:
                    newGrid[(self.mWidth - x - 1) * newWidth + y] = dest

        t = self.mMaxTileSize.width()
        self.mMaxTileSize.setWidth(self.mMaxTileSize.height())
        self.mMaxTileSize.setHeight(t)
        self.mWidth = newWidth
        self.mHeight = newHeight
        self.mGrid = newGrid

    ##
    # Computes and returns the set of tilesets used by this tile layer.
    ##
    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

    ##
    # Returns whether this tile layer has any cell for which the given
    # \a condition returns True.
    ##
    def hasCell(self, condition):
        i = 0
        for cell in self.mGrid:
            if (condition(cell)):
                return True
            i += 1
        return False

    ##
    # Returns whether this tile layer is referencing the given tileset.
    ##
    def referencesTileset(self, tileset):
        i = 0
        while(i<self.mGrid.size()):
            tile = self.mGrid.at(i).tile
            if (tile and tile.tileset() == tileset):
                return True
            i += 1
        return False

    ##
    # Removes all references to the given tileset. This sets all tiles on this
    # layer that are from the given tileset to null.
    ##
    def removeReferencesToTileset(self, tileset):
        i = 0
        while(i<self.mGrid.size()):
            tile = self.mGrid.at(i).tile
            if (tile and tile.tileset() == tileset):
                self.mGrid.replace(i, Cell())
            i += 1

    ##
    # Replaces all tiles from \a oldTileset with tiles from \a newTileset.
    ##
    def replaceReferencesToTileset(self, oldTileset, newTileset):
        i = 0
        while(i<self.mGrid.size()):
            tile = self.mGrid.at(i).tile
            if (tile and tile.tileset() == oldTileset):
                self.mGrid[i].tile = newTileset.tileAt(tile.id())
            i += 1

    ##
    # Resizes this tile layer to \a size, while shifting all tiles by
    # \a offset.
    ##
    def resize(self, size, offset):
        if (self.size() == size and offset.isNull()):
            return
        newGrid = QVector()
        for i in range(size.width() * size.height()):
            newGrid.append(Cell())
        # Copy over the preserved part
        startX = max(0, -offset.x())
        startY = max(0, -offset.y())
        endX = min(self.mWidth, size.width() - offset.x())
        endY = min(self.mHeight, size.height() - offset.y())
        for y in range(startY, endY):
            for x in range(startX, endX):
                index = x + offset.x() + (y + offset.y()) * size.width()
                newGrid[index] = self.cellAt(x, y)

        self.mGrid = newGrid
        self.setSize(size)

    ##
    # Offsets the tiles in this layer within \a bounds by \a offset,
    # and optionally wraps them.
    #
    # \sa ObjectGroup.offset()
    ##
    def offsetTiles(self, offset, bounds, wrapX, wrapY):
        newGrid = QVector()
        for i in range(self.mWidth * self.mHeight):
            newGrid.append(Cell())
        for y in range(self.mHeight):
            for x in range(self.mWidth):
                # Skip out of bounds tiles
                if (not bounds.contains(x, y)):
                    newGrid[x + y * self.mWidth] = self.cellAt(x, y)
                    continue

                # Get position to pull tile value from
                oldX = x - offset.x()
                oldY = y - offset.y()
                # Wrap x value that will be pulled from
                if (wrapX and bounds.width() > 0):
                    while oldX < bounds.left():
                        oldX += bounds.width()
                    while oldX > bounds.right():
                        oldX -= bounds.width()

                # Wrap y value that will be pulled from
                if (wrapY and bounds.height() > 0):
                    while oldY < bounds.top():
                        oldY += bounds.height()
                    while oldY > bounds.bottom():
                        oldY -= bounds.height()

                # Set the new tile
                if (self.contains(oldX, oldY) and bounds.contains(oldX, oldY)):
                    newGrid[x + y * self.mWidth] = self.cellAt(oldX, oldY)
                else:
                    newGrid[x + y * self.mWidth] = Cell()

        self.mGrid = newGrid

    def canMergeWith(self, other):
        return other.isTileLayer()

    def mergedWith(self, other):
        o = other
        unitedBounds = self.bounds().united(o.bounds())
        offset = self.position() - unitedBounds.topLeft()
        merged = self.clone()
        merged.resize(unitedBounds.size(), offset)
        merged.merge(o.position() - unitedBounds.topLeft(), o)
        return merged

    ##
    # Returns the region where this tile layer and the given tile layer
    # are different. The relative positions of the layers are taken into
    # account. The returned region is relative to this tile layer.
    ##
    def computeDiffRegion(self, other):
        ret = QRegion()
        dx = other.x() - self.mX
        dy = other.y() - self.mY
        r = QRect(0, 0, self.width(), self.height())
        r &= QRect(dx, dy, other.width(), other.height())
        for y in range(r.top(), r.bottom()+1):
            for x in range(r.left(), r.right()+1):
                if (self.cellAt(x, y) != other.cellAt(x - dx, y - dy)):
                    rangeStart = x
                    while (x <= r.right() and self.cellAt(x, y) != other.cellAt(x - dx, y - dy)):
                        x += 1

                    rangeEnd = x
                    ret += QRect(rangeStart, y, rangeEnd - rangeStart, 1)

        return ret

    ##
    # Returns True if all tiles in the layer are empty.
    ##
    def isEmpty(self):
        i = 0
        while(i<self.mGrid.size()):
            if (not self.mGrid.at(i).isEmpty()):
                return False
            i += 1
        return True

    ##
    # Returns a duplicate of this TileLayer.
    #
    # \sa Layer.clone()
    ##
    def clone(self):
        return self.initializeClone(TileLayer(self.mName, self.mX, self.mY, self.mWidth, self.mHeight))

    def begin(self):
        return self.mGrid.begin()
    
    def end(self):
        return self.mGrid.end()
        
    def initializeClone(self, clone):
        super().initializeClone(clone)
        clone.mGrid = self.mGrid
        clone.mMaxTileSize = self.mMaxTileSize
        clone.mOffsetMargins = self.mOffsetMargins
        return clone
Esempio n. 52
0
class Zoomable(QObject):
    scaleChanged = pyqtSignal(float)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.mScale = 1
        self.mZoomFactors = QVector()
        self.mGestureStartScale = 0
        self.mComboBox = None
        self.mComboRegExp = QRegExp("^\\s*(\\d+)\\s*%?\\s*$")
        self.mComboValidator = None

        for i in range(zoomFactorCount):
            self.mZoomFactors.append(zoomFactors[i])

    def setScale(self, scale):
        if (scale == self.mScale):
            return
        self.mScale = scale
        self.syncComboBox()
        self.scaleChanged.emit(self.mScale)

    def scale(self):
        return self.mScale

    def canZoomIn(self):
        return self.mScale < self.mZoomFactors.last()

    def canZoomOut(self):
        return self.mScale > self.mZoomFactors.first()

    ##
    # Changes the current scale based on the given mouse wheel \a delta.
    #
    # For convenience, the delta is assumed to be in the same units as
    # QWheelEvent.delta, which is the distance that the wheel is rotated,
    # in eighths of a degree.
    ##
    def handleWheelDelta(self, delta):
        if (delta <= -120):
            self.zoomOut()
        elif (delta >= 120):
            self.zoomIn()
        else:
            # We're dealing with a finer-resolution mouse. Allow it to have finer
            # control over the zoom level.
            factor = 1 + 0.3 * qAbs(delta / 8 / 15)
            if (delta < 0):
                factor = 1 / factor
            scale = qBound(self.mZoomFactors.first(), self.mScale * factor,
                           self.mZoomFactors.back())
            # Round to at most four digits after the decimal point
            self.setScale(math.floor(scale * 10000 + 0.5) / 10000)

    ##
    # Changes the current scale based on the given pinch gesture.
    ##
    def handlePinchGesture(self, pinch):
        if (not (pinch.changeFlags() & QPinchGesture.ScaleFactorChanged)):
            return
        x = pinch.state()
        if x == Qt.NoGesture:
            pass
        elif x == Qt.GestureStarted:
            self.mGestureStartScale = self.mScale
            # fall through
        elif x == Qt.GestureUpdated:
            factor = pinch.scaleFactor()
            scale = qBound(self.mZoomFactors.first(),
                           self.mGestureStartScale * factor,
                           self.mZoomFactors.back())
            self.setScale(math.floor(scale * 10000 + 0.5) / 10000)
        elif x == Qt.GestureFinished:
            pass
        elif x == Qt.GestureCanceled:
            pass

    ##
    # Returns whether images should be smoothly transformed when drawn at the
    # current scale. This is the case when the scale is not 1 and smaller than
    # 2.
    ##
    def smoothTransform(self):
        return self.mScale != 1.0 and self.mScale < 2.0

    def setZoomFactors(self, factors):
        self.mZoomFactors = factors

    def connectToComboBox(self, comboBox):
        if (self.mComboBox):
            self.mComboBox.disconnect()
            if (self.mComboBox.lineEdit()):
                self.mComboBox.lineEdit().disconnect()
            self.mComboBox.setValidator(None)

        self.mComboBox = comboBox
        if type(comboBox) is QComboBox:
            self.mComboBox.clear()
            for scale in self.mZoomFactors:
                self.mComboBox.addItem(scaleToString(scale), scale)
            self.syncComboBox()
            self.mComboBox.activated.connect(self.comboActivated)
            self.mComboBox.setEditable(True)
            self.mComboBox.setInsertPolicy(QComboBox.NoInsert)
            self.mComboBox.lineEdit().editingFinished.connect(self.comboEdited)
            if (not self.mComboValidator):
                self.mComboValidator = QRegExpValidator(
                    self.mComboRegExp, self)
            self.mComboBox.setValidator(self.mComboValidator)

    def zoomIn(self):
        for scale in self.mZoomFactors:
            if (scale > self.mScale):
                self.setScale(scale)
                break

    def zoomOut(self):
        for i in range(self.mZoomFactors.count() - 1, -1, -1):
            if (self.mZoomFactors[i] < self.mScale):
                self.setScale(self.mZoomFactors[i])
                break

    def resetZoom(self):
        self.setScale(1)

    def comboActivated(self, index):
        self.setScale(self.mComboBox.itemData(index))

    def comboEdited(self):
        pos = self.mComboRegExp.indexIn(self.mComboBox.currentText())
        pos != -1
        scale = qBound(self.mZoomFactors.first(),
                       Float(self.mComboRegExp.cap(1)) / 100.0,
                       self.mZoomFactors.last())
        self.setScale(scale)

    def syncComboBox(self):
        if (not self.mComboBox):
            return
        index = self.mComboBox.findData(self.mScale)
        # For a custom scale, the current index must be set to -1
        self.mComboBox.setCurrentIndex(index)
        self.mComboBox.setEditText(scaleToString(self.mScale))
Esempio n. 53
0
class AutomappingManager(QObject):
    ##
    # This signal is emited after automapping was done and an error occurred.
    ##
    errorsOccurred = pyqtSignal(bool)
    ##
    # This signal is emited after automapping was done and a warning occurred.
    ##
    warningsOccurred = pyqtSignal(bool)

    ##
    # Constructor.
    ##
    def __init__(self, parent = None):
        super().__init__(parent)

        ##
        # The current map document.
        ##
        self.mMapDocument = None
        ##
        # For each new file of rules a new AutoMapper is setup. In this vector we
        # can store all of the AutoMappers in order.
        ##
        self.mAutoMappers = QVector()
        ##
        # This tells you if the rules for the current map document were already
        # loaded.
        ##
        self.mLoaded = False
        ##
        # Contains all errors which occurred until canceling.
        # If mError is not empty, no serious result can be expected.
        ##
        self.mError = ''
        ##
        # Contains all strings, which try to explain unusual and unexpected
        # behavior.
        ##
        self.mWarning = QString()

    def __del__(self):
        self.cleanUp()

    def setMapDocument(self, mapDocument):
        self.cleanUp()
        if (self.mMapDocument):
            self.mMapDocument.disconnect()
        self.mMapDocument = mapDocument
        if (self.mMapDocument):
            self.mMapDocument.regionEdited.connect(self.autoMap)

        self.mLoaded = False

    def errorString(self):
        return self.mError

    def warningString(self):
        return self.mWarning
    
    ##
    # This triggers an automapping on the whole current map document.
    ##
    def autoMap(self, *args):
        l = len(args)
        if l == 0:
            if (not self.mMapDocument):
                return
            map = self.mMapDocument.Map()
            w = map.width()
            h = map.height()
            self.autoMapInternal(QRect(0, 0, w, h), None)
        elif l==2:
            where, touchedLayer = args
            if (preferences.Preferences.instance().automappingDrawing()):
                self.autoMapInternal(where, touchedLayer)

    ##
    # This function parses a rules file.
    # For each path which is a rule, (fileextension is tmx) an AutoMapper
    # object is setup.
    #
    # If a fileextension is txt, this file will be opened and searched for
    # rules again.
    #
    # @return if the loading was successful: return True if it suceeded.
    ##
    def loadFile(self, filePath):
        ret = True
        absPath = QFileInfo(filePath).path()
        rulesFile = QFile(filePath)
        if (not rulesFile.exists()):
            self.mError += self.tr("No rules file found at:\n%s\n"%filePath)
            return False

        if (not rulesFile.open(QIODevice.ReadOnly | QIODevice.Text)):
            self.mError += self.tr("Error opening rules file:\n%s\n"%filePath)
            return False

        i = QTextStream(rulesFile)
        line = ' '
        while line != '':
            line = i.readLine()
            rulePath = line.strip()
            if (rulePath=='' or rulePath.startswith('#') or rulePath.startswith("//")):
                continue
            if (QFileInfo(rulePath).isRelative()):
                rulePath = absPath + '/' + rulePath
            if (not QFileInfo(rulePath).exists()):
                self.mError += self.tr("File not found:\n%s"%rulePath) + '\n'
                ret = False
                continue

            if (rulePath.lower().endswith(".tmx")):
                tmxFormat = TmxMapFormat()
                rules = tmxFormat.read(rulePath)
                if (not rules):
                    self.mError += self.tr("Opening rules map failed:\n%s"%tmxFormat.errorString()) + '\n'
                    ret = False
                    continue

                tilesetManager = TilesetManager.instance()
                tilesetManager.addReferences(rules.tilesets())
                autoMapper = None
                autoMapper = AutoMapper(self.mMapDocument, rules, rulePath)
                self.mWarning += autoMapper.warningString()
                error = autoMapper.errorString()
                if error != '':
                    self.mAutoMappers.append(autoMapper)
                else:
                    self.mError += error
                    del autoMapper

            if (rulePath.lower().endswith(".txt")):
                if (not self.loadFile(rulePath)):
                    ret = False
        return ret

    ##
    # Applies automapping to the Region \a where, considering only layer
    # \a touchedLayer has changed.
    # There will only those Automappers be used which have a rule layer
    # touching the \a touchedLayer
    # If layer is 0, all Automappers are used.
    ##
    def autoMapInternal(self, where, touchedLayer):
        self.mError = ''
        self.mWarning = ''
        if (not self.mMapDocument):
            return
        automatic = touchedLayer != None
        if (not self.mLoaded):
            mapPath = QFileInfo(self.mMapDocument.fileName()).path()
            rulesFileName = mapPath + "/rules.txt"
            if (self.loadFile(rulesFileName)):
                self.mLoaded = True
            else:
                self.errorsOccurred.emit(automatic)
                return
                
        passedAutoMappers = QVector()
        if (touchedLayer):
            for a in self.mAutoMappers:
                if (a.ruleLayerNameUsed(touchedLayer.name())):
                    passedAutoMappers.append(a)
        else:
            passedAutoMappers = self.mAutoMappers

        if (not passedAutoMappers.isEmpty()):
            # use a pointer to the region, so each automapper can manipulate it and the
            # following automappers do see the impact
            region = QRegion(where)
        
            undoStack = self.mMapDocument.undoStack()
            undoStack.beginMacro(self.tr("Apply AutoMap rules"))
            aw = AutoMapperWrapper(self.mMapDocument, passedAutoMappers, region)
            undoStack.push(aw)
            undoStack.endMacro()

        for automapper in self.mAutoMappers:
            self.mWarning += automapper.warningString()
            self.mError += automapper.errorString()

        if self.mWarning != '':
            self.warningsOccurred.emit(automatic)
        if self.mError != '':
            self.errorsOccurred.emit(automatic)

    ##
    # deletes all its data structures
    ##
    def cleanUp(self):
        self.mAutoMappers.clear()
Esempio n. 54
0
class Zoomable(QObject):
    scaleChanged = pyqtSignal(float)

    def __init__(self, parent = None):
        super().__init__(parent)
        self.mScale = 1
        self.mZoomFactors = QVector()
        self.mGestureStartScale = 0
        self.mComboBox = None
        self.mComboRegExp = QRegExp("^\\s*(\\d+)\\s*%?\\s*$")
        self.mComboValidator = None

        for i in range(zoomFactorCount):
            self.mZoomFactors.append(zoomFactors[i])

    def setScale(self, scale):
        if (scale == self.mScale):
            return
        self.mScale = scale
        self.syncComboBox()
        self.scaleChanged.emit(self.mScale)

    def scale(self):
        return self.mScale

    def canZoomIn(self):
        return self.mScale < self.mZoomFactors.last()

    def canZoomOut(self):
        return self.mScale > self.mZoomFactors.first()

    ##
    # Changes the current scale based on the given mouse wheel \a delta.
    #
    # For convenience, the delta is assumed to be in the same units as
    # QWheelEvent.delta, which is the distance that the wheel is rotated,
    # in eighths of a degree.
    ##
    def handleWheelDelta(self, delta):
        if (delta <= -120):
            self.zoomOut()
        elif (delta >= 120):
            self.zoomIn()
        else:
            # We're dealing with a finer-resolution mouse. Allow it to have finer
            # control over the zoom level.
            factor = 1 + 0.3 * qAbs(delta / 8 / 15)
            if (delta < 0):
                factor = 1 / factor
            scale = qBound(self.mZoomFactors.first(),
                                 self.mScale * factor,
                                 self.mZoomFactors.back())
            # Round to at most four digits after the decimal point
            self.setScale(math.floor(scale * 10000 + 0.5) / 10000)

    ##
    # Changes the current scale based on the given pinch gesture.
    ##
    def handlePinchGesture(self, pinch):
        if (not (pinch.changeFlags() & QPinchGesture.ScaleFactorChanged)):
            return
        x = pinch.state()
        if x==Qt.NoGesture:
            pass
        elif x==Qt.GestureStarted:
            self.mGestureStartScale = self.mScale
            # fall through
        elif x==Qt.GestureUpdated:
            factor = pinch.scaleFactor()
            scale = qBound(self.mZoomFactors.first(),
                                 self.mGestureStartScale * factor,
                                 self.mZoomFactors.back())
            self.setScale(math.floor(scale * 10000 + 0.5) / 10000)
        elif x==Qt.GestureFinished:
            pass
        elif x==Qt.GestureCanceled:
            pass

    ##
    # Returns whether images should be smoothly transformed when drawn at the
    # current scale. This is the case when the scale is not 1 and smaller than
    # 2.
    ##
    def smoothTransform(self):
        return self.mScale != 1.0 and self.mScale < 2.0

    def setZoomFactors(self, factors):
        self.mZoomFactors = factors

    def connectToComboBox(self, comboBox):
        if (self.mComboBox):
            self.mComboBox.disconnect()
            if (self.mComboBox.lineEdit()):
                self.mComboBox.lineEdit().disconnect()
            self.mComboBox.setValidator(None)

        self.mComboBox = comboBox
        if type(comboBox) is QComboBox:
            self.mComboBox.clear()
            for scale in self.mZoomFactors:
                self.mComboBox.addItem(scaleToString(scale), scale)
            self.syncComboBox()
            self.mComboBox.activated.connect(self.comboActivated)
            self.mComboBox.setEditable(True)
            self.mComboBox.setInsertPolicy(QComboBox.NoInsert)
            self.mComboBox.lineEdit().editingFinished.connect(self.comboEdited)
            if (not self.mComboValidator):
                self.mComboValidator = QRegExpValidator(self.mComboRegExp, self)
            self.mComboBox.setValidator(self.mComboValidator)

    def zoomIn(self):
        for scale in self.mZoomFactors:
            if (scale > self.mScale):
                self.setScale(scale)
                break

    def zoomOut(self):
        for i in range(self.mZoomFactors.count() - 1, -1, -1):
            if (self.mZoomFactors[i] < self.mScale):
                self.setScale(self.mZoomFactors[i])
                break

    def resetZoom(self):
        self.setScale(1)

    def comboActivated(self, index):
        self.setScale(self.mComboBox.itemData(index))

    def comboEdited(self):
        pos = self.mComboRegExp.indexIn(self.mComboBox.currentText())
        pos != -1
        scale = qBound(self.mZoomFactors.first(), Float(self.mComboRegExp.cap(1)) / 100.0, self.mZoomFactors.last())
        self.setScale(scale)

    def syncComboBox(self):
        if (not self.mComboBox):
            return
        index = self.mComboBox.findData(self.mScale)
        # For a custom scale, the current index must be set to -1
        self.mComboBox.setCurrentIndex(index)
        self.mComboBox.setEditText(scaleToString(self.mScale))
Esempio n. 55
0
class TileStampManager(QObject):
    setStamp = pyqtSignal(TileStamp)

    def __init__(self, toolManager, parent = None):
        super().__init__(parent)
        
        self.mStampsByName = QMap()
        self.mQuickStamps = QVector()
        for i in range(TileStampManager.quickStampKeys().__len__()):
            self.mQuickStamps.append(0)
        
        self.mTileStampModel = TileStampModel(self)
        self.mToolManager = toolManager

        prefs = preferences.Preferences.instance()
        prefs.stampsDirectoryChanged.connect(self.stampsDirectoryChanged)
        self.mTileStampModel.stampAdded.connect(self.stampAdded)
        self.mTileStampModel.stampRenamed.connect(self.stampRenamed)
        self.mTileStampModel.stampChanged.connect(self.saveStamp)
        self.mTileStampModel.stampRemoved.connect(self.deleteStamp)
        self.loadStamps()

    def __del__(self):
        # needs to be over here where the TileStamp type is complete
        pass
        
    ##
    # Returns the keys used for quickly accessible tile stamps.
    # Note: To store a tile layer <Ctrl> is added. The given keys will work
    # for recalling the stored values.
    ##
    def quickStampKeys():
        keys=[Qt.Key_1, Qt.Key_2, Qt.Key_3, Qt.Key_4, Qt.Key_5, Qt.Key_6, Qt.Key_7, Qt.Key_8, Qt.Key_9]
        return keys
        
    def tileStampModel(self):
        return self.mTileStampModel
        
    def createStamp(self):
        stamp = self.tampFromContext(self.mToolManager.selectedTool())
        if (not stamp.isEmpty()):
            self.mTileStampModel.addStamp(stamp)
        return stamp
    
    def addVariation(self, targetStamp):
        stamp = stampFromContext(self.mToolManager.selectedTool())
        if (stamp.isEmpty()):
            return
        if (stamp == targetStamp): # avoid easy mistake of adding duplicates
            return
        for variation in stamp.variations():
            self.mTileStampModel.addVariation(targetStamp, variation)

    def selectQuickStamp(self, index):
        stamp = self.mQuickStamps.at(index)
        if (not stamp.isEmpty()):
            self.setStamp.emit(stamp)
    
    def createQuickStamp(self, index):
        stamp = stampFromContext(self.mToolManager.selectedTool())
        if (stamp.isEmpty()):
            return
        self.setQuickStamp(index, stamp)
    
    def extendQuickStamp(self, index):
        quickStamp = self.mQuickStamps[index]
        if (quickStamp.isEmpty()):
            self.createQuickStamp(index)
        else:
            self.addVariation(quickStamp)
    
    def stampsDirectoryChanged(self):
        # erase current stamps
        self.mQuickStamps.fill(TileStamp())
        self.mStampsByName.clear()
        self.mTileStampModel.clear()
        self.loadStamps()

    def eraseQuickStamp(self, index):
        stamp = self.mQuickStamps.at(index)
        if (not stamp.isEmpty()):
            self.mQuickStamps[index] = TileStamp()
            if (not self.mQuickStamps.contains(stamp)):
                self.mTileStampModel.removeStamp(stamp)

    def setQuickStamp(self, index, stamp):
        stamp.setQuickStampIndex(index)
        # make sure existing quickstamp is removed from stamp model
        self.eraseQuickStamp(index)
        self.mTileStampModel.addStamp(stamp)
        self.mQuickStamps[index] = stamp
    
    def loadStamps(self):
        prefs = preferences.Preferences.instance()
        stampsDirectory = prefs.stampsDirectory()
        stampsDir = QDir(stampsDirectory)
        iterator = QDirIterator(stampsDirectory,
                              ["*.stamp"],
                              QDir.Files | QDir.Readable)
        while (iterator.hasNext()):
            stampFileName = iterator.next()
            stampFile = QFile(stampFileName)
            if (not stampFile.open(QIODevice.ReadOnly)):
                continue
            data = stampFile.readAll()
            document = QJsonDocument.fromBinaryData(data)
            if (document.isNull()):
                # document not valid binary data, maybe it's an JSON text file
                error = QJsonParseError()
                document = QJsonDocument.fromJson(data, error)
                if (error.error != QJsonParseError.NoError):
                    qDebug("Failed to parse stamp file:" + error.errorString())
                    continue

            stamp = TileStamp.fromJson(document.object(), stampsDir)
            if (stamp.isEmpty()):
                continue
            stamp.setFileName(iterator.fileInfo().fileName())
            self.mTileStampModel.addStamp(stamp)
            index = stamp.quickStampIndex()
            if (index >= 0 and index < self.mQuickStamps.size()):
                self.mQuickStamps[index] = stamp


    def stampAdded(self, stamp):
        if (stamp.name().isEmpty() or self.mStampsByName.contains(stamp.name())):
            # pick the first available stamp name
            name = QString()
            index = self.mTileStampModel.stamps().size()
            while(self.mStampsByName.contains(name)):
                name = str(index)
                index += 1
            
            stamp.setName(name)
        
        self.mStampsByName.insert(stamp.name(), stamp)
        if (stamp.fileName().isEmpty()):
            stamp.setFileName(findStampFileName(stamp.name()))
            self.saveStamp(stamp)

    def stampRenamed(self, stamp):
        existingName = self.mStampsByName.key(stamp)
        self.mStampsByName.remove(existingName)
        self.mStampsByName.insert(stamp.name(), stamp)
        existingFileName = stamp.fileName()
        newFileName = findStampFileName(stamp.name(), existingFileName)
        if (existingFileName != newFileName):
            if (QFile.rename(stampFilePath(existingFileName),
                              stampFilePath(newFileName))):
                stamp.setFileName(newFileName)

    
    def saveStamp(self, stamp):
        # make sure we have a stamps directory
        prefs = preferences.Preferences.instance()
        stampsDirectory = prefs.stampsDirectory()
        stampsDir = QDir(stampsDirectory)
        if (not stampsDir.exists() and not stampsDir.mkpath(".")):
            qDebug("Failed to create stamps directory" + stampsDirectory)
            return
        
        filePath = stampsDir.filePath(stamp.fileName())
        file = QSaveFile(filePath)
        if (not file.open(QIODevice.WriteOnly)):
            qDebug("Failed to open stamp file for writing" + filePath)
            return
        
        stampJson = stamp.toJson(QFileInfo(filePath).dir())
        file.write(QJsonDocument(stampJson).toJson(QJsonDocument.Compact))
        if (not file.commit()):
            qDebug() << "Failed to write stamp" << filePath
    
    def deleteStamp(self, stamp):
        self.mStampsByName.remove(stamp.name())
        QFile.remove(stampFilePath(stamp.fileName()))
Esempio n. 56
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()