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)
    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)
Exemple #3
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
Exemple #4
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
 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()
Exemple #6
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
Exemple #7
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
Exemple #8
0
 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 __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)
Exemple #10
0
 def __init__(self, parent = None):
     super().__init__(self.tr("Bucket Fill Tool"),
                        QIcon(":images/22x22/stock-tool-bucket-fill.png"),
                        QKeySequence(self.tr("F")),
                        parent)
     self.mStamp = TileStamp()
     self.mFillOverlay = None
     self.mFillRegion = QRegion()
     self.mMissingTilesets = QVector()
     self.mIsActive = False
     self.mLastShiftStatus = False
     ##
     # Indicates if the tool is using the random mode.
     ##
     self.mIsRandom = False
     ##
     # Contains the value of mIsRandom at that time, when the latest call of
     # tilePositionChanged() took place.
     # This variable is needed to detect if the random mode was changed during
     # mFillOverlay being brushed at an area.
     ##
     self.mLastRandomStatus = False
     ##
     # Contains all used random cells to use in random mode.
     # The same cell can be in the list multiple times to make different
     # random weights possible.
     ##
     self.mRandomCellPicker = RandomPicker()
    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 __init__(self, mapDocument, autoMapper, where):
        super().__init__()
        
        self.mLayersAfter = QVector()
        self.mLayersBefore = QVector()
        self.mMapDocument = mapDocument
        map = self.mMapDocument.Map()
        touchedLayers = QSet()
        index = 0
        while (index < autoMapper.size()):
            a = autoMapper.at(index)
            if (a.prepareAutoMap()):
                touchedLayers|= a.getTouchedTileLayers()
                index += 1
            else:
                autoMapper.remove(index)

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

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

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

        for a in autoMapper:
            a.cleanAll()
Exemple #13
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
Exemple #14
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)
    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)
Exemple #16
0
    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])
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)
    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 __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"))
Exemple #20
0
    def __init__(self, parent):
        super().__init__(parent)
        self.mMapDocument = None
        self.mSelectedTool = None
        self.mActiveTool = None
        self.mObjectSelectionItem = None
        self.mUnderMouse = False
        self.mCurrentModifiers = Qt.NoModifier,
        self.mDarkRectangle = QGraphicsRectItem()
        self.mDefaultBackgroundColor = Qt.darkGray

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

        self.setBackgroundBrush(self.mDefaultBackgroundColor)
        tilesetManager = TilesetManager.instance()
        tilesetManager.tilesetChanged.connect(self.tilesetChanged)
        tilesetManager.repaintTileset.connect(self.tilesetChanged)
        prefs = preferences.Preferences.instance()
        prefs.showGridChanged.connect(self.setGridVisible)
        prefs.showTileObjectOutlinesChanged.connect(self.setShowTileObjectOutlines)
        prefs.objectTypesChanged.connect(self.syncAllObjectItems)
        prefs.highlightCurrentLayerChanged.connect(self.setHighlightCurrentLayer)
        prefs.gridColorChanged.connect(self.update)
        prefs.objectLineWidthChanged.connect(self.setObjectLineWidth)
        self.mDarkRectangle.setPen(QPen(Qt.NoPen))
        self.mDarkRectangle.setBrush(Qt.black)
        self.mDarkRectangle.setOpacity(darkeningFactor)
        self.addItem(self.mDarkRectangle)
        self.mGridVisible = prefs.showGrid()
        self.mObjectLineWidth = prefs.objectLineWidth()
        self.mShowTileObjectOutlines = prefs.showTileObjectOutlines()
        self.mHighlightCurrentLayer = prefs.highlightCurrentLayer()
        # Install an event filter so that we can get key events on behalf of the
        # active tool without having to have the current focus.
        QCoreApplication.instance().installEventFilter(self)
Exemple #21
0
    def __init__(self, *args):
        super().__init__(Object.TileType)
        
        l = len(args)
        if l==3:
            image, id, tileset = args
            self.mImageSource = QString()
        elif l==4:
            image, imageSource, id, tileset = args
            self.mImageSource = imageSource

        self.mId = id
        self.mTileset = tileset
        self.mImage = image
        self.mTerrain = 0xffffffff
        self.mProbability = 1.0
        self.mObjectGroup = None
        self.mFrames = QVector()
        self.mCurrentFrameIndex = 0
        self.mUnusedTime = 0
    def __init__(self, parent = None):
        super().__init__(self.tr("Edit Polygons"),
              QIcon(":images/24x24/tool-edit-polygons.png"),
              QKeySequence(self.tr("E")),
              parent)

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

        self.mSelectionRectangle = SelectionRectangle()
        self.mMousePressed = False
        self.mClickedHandle = None
        self.mClickedObjectItem = None
        self.mMode = EditPolygonTool.NoMode
Exemple #23
0
    def __init__(self, parent = None):
        super().__init__(self.tr("Stamp Brush"),
                           QIcon(":images/22x22/stock-tool-clone.png"),
                           QKeySequence(self.tr("B")),
                           parent)
        ##
        # This stores the current behavior.
        ##
        self.mBrushBehavior = BrushBehavior.Free

        self.mIsRandom = False
        self.mCaptureStart = QPoint()
        self.mRandomCellPicker = RandomPicker()
        ##
        # mStamp is a tile layer in which is the selection the user made
        # either by rightclicking (Capture) or at the tilesetdock
        ##
        self.mStamp = TileStamp()
        self.mPreviewLayer = None
        self.mMissingTilesets = QVector()
        self.mPrevTilePosition = QPoint()
        self.mStampReference = QPoint()
Exemple #24
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
Exemple #25
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)
Exemple #26
0
    def __init__(self, parent=None):
        super().__init__(parent)

        # Shared tileset references because the dock wants to add new tiles
        self.mTilesets = QVector()
        self.mCurrentTilesets = QMap()
        self.mMapDocument = None
        self.mTabBar = QTabBar()
        self.mViewStack = QStackedWidget()
        self.mToolBar = QToolBar()
        self.mCurrentTile = None
        self.mCurrentTiles = None
        self.mNewTileset = QAction(self)
        self.mImportTileset = QAction(self)
        self.mExportTileset = QAction(self)
        self.mPropertiesTileset = QAction(self)
        self.mDeleteTileset = QAction(self)
        self.mEditTerrain = QAction(self)
        self.mAddTiles = QAction(self)
        self.mRemoveTiles = QAction(self)
        self.mTilesetMenuButton = TilesetMenuButton(self)
        self.mTilesetMenu = QMenu(self)  # opens on click of mTilesetMenu
        self.mTilesetActionGroup = QActionGroup(self)
        self.mTilesetMenuMapper = None  # needed due to dynamic content
        self.mEmittingStampCaptured = False
        self.mSynchronizingSelection = False

        self.setObjectName("TilesetDock")
        self.mTabBar.setMovable(True)
        self.mTabBar.setUsesScrollButtons(True)
        self.mTabBar.currentChanged.connect(self.updateActions)
        self.mTabBar.tabMoved.connect(self.moveTileset)
        w = QWidget(self)
        horizontal = QHBoxLayout()
        horizontal.setSpacing(0)
        horizontal.addWidget(self.mTabBar)
        horizontal.addWidget(self.mTilesetMenuButton)
        vertical = QVBoxLayout(w)
        vertical.setSpacing(0)
        vertical.setContentsMargins(5, 5, 5, 5)
        vertical.addLayout(horizontal)
        vertical.addWidget(self.mViewStack)
        horizontal = QHBoxLayout()
        horizontal.setSpacing(0)
        horizontal.addWidget(self.mToolBar, 1)
        vertical.addLayout(horizontal)
        self.mNewTileset.setIcon(QIcon(":images/16x16/document-new.png"))
        self.mImportTileset.setIcon(QIcon(":images/16x16/document-import.png"))
        self.mExportTileset.setIcon(QIcon(":images/16x16/document-export.png"))
        self.mPropertiesTileset.setIcon(
            QIcon(":images/16x16/document-properties.png"))
        self.mDeleteTileset.setIcon(QIcon(":images/16x16/edit-delete.png"))
        self.mEditTerrain.setIcon(QIcon(":images/16x16/terrain.png"))
        self.mAddTiles.setIcon(QIcon(":images/16x16/add.png"))
        self.mRemoveTiles.setIcon(QIcon(":images/16x16/remove.png"))
        Utils.setThemeIcon(self.mNewTileset, "document-new")
        Utils.setThemeIcon(self.mImportTileset, "document-import")
        Utils.setThemeIcon(self.mExportTileset, "document-export")
        Utils.setThemeIcon(self.mPropertiesTileset, "document-properties")
        Utils.setThemeIcon(self.mDeleteTileset, "edit-delete")
        Utils.setThemeIcon(self.mAddTiles, "add")
        Utils.setThemeIcon(self.mRemoveTiles, "remove")
        self.mNewTileset.triggered.connect(self.newTileset)
        self.mImportTileset.triggered.connect(self.importTileset)
        self.mExportTileset.triggered.connect(self.exportTileset)
        self.mPropertiesTileset.triggered.connect(self.editTilesetProperties)
        self.mDeleteTileset.triggered.connect(self.removeTileset)
        self.mEditTerrain.triggered.connect(self.editTerrain)
        self.mAddTiles.triggered.connect(self.addTiles)
        self.mRemoveTiles.triggered.connect(self.removeTiles)
        self.mToolBar.addAction(self.mNewTileset)
        self.mToolBar.setIconSize(QSize(16, 16))
        self.mToolBar.addAction(self.mImportTileset)
        self.mToolBar.addAction(self.mExportTileset)
        self.mToolBar.addAction(self.mPropertiesTileset)
        self.mToolBar.addAction(self.mDeleteTileset)
        self.mToolBar.addAction(self.mEditTerrain)
        self.mToolBar.addAction(self.mAddTiles)
        self.mToolBar.addAction(self.mRemoveTiles)
        self.mZoomable = Zoomable(self)
        self.mZoomComboBox = QComboBox()
        self.mZoomable.connectToComboBox(self.mZoomComboBox)
        horizontal.addWidget(self.mZoomComboBox)
        self.mViewStack.currentChanged.connect(self.updateCurrentTiles)
        TilesetManager.instance().tilesetChanged.connect(self.tilesetChanged)
        DocumentManager.instance().documentAboutToClose.connect(
            self.documentAboutToClose)
        self.mTilesetMenuButton.setMenu(self.mTilesetMenu)
        self.mTilesetMenu.aboutToShow.connect(self.refreshTilesetMenu)
        self.setWidget(w)
        self.retranslateUi()
        self.setAcceptDrops(True)
        self.updateActions()
Exemple #27
0
def pointsOnEllipse(*args):
    l = len(args)
    if l==2:
        a, b = args
        return pointsOnLine(a.x(), a.y(), b.x(), b.y())
    elif l==4:
        x0, y0, x1, y1 = args
        ret = QVector()
        ellipseError = 0
        if x0 > x1:
            radiusX = x0 - x1
        else:
            radiusX = x1 - x0
        if y0 > y1:
            radiusY = y0 - y1
        else:
            radiusY = y1 - y0
        if (radiusX == 0 and radiusY == 0):
            return ret
        twoXSquare = 2 * radiusX * radiusX
        twoYSquare = 2 * radiusY * radiusY
        x = radiusX
        y = 0
        xChange = radiusY * radiusY * (1 - 2 * radiusX)
        yChange = radiusX * radiusX
        ellipseError = 0
        stoppingX = twoYSquare*radiusX
        stoppingY = 0
        while (stoppingX >= stoppingY):
            ret += QPoint(x0 + x, y0 + y)
            ret += QPoint(x0 - x, y0 + y)
            ret += QPoint(x0 + x, y0 - y)
            ret += QPoint(x0 - x, y0 - y)
            y += 1
            stoppingY += twoXSquare
            ellipseError += yChange
            yChange += twoXSquare
            if ((2 * ellipseError + xChange) > 0):
                x -= 1
                stoppingX -= twoYSquare
                ellipseError += xChange
                xChange += twoYSquare

        x = 0
        y = radiusY
        xChange = radiusY * radiusY
        yChange = radiusX * radiusX * (1 - 2 * radiusY)
        ellipseError = 0
        stoppingX = 0
        stoppingY = twoXSquare * radiusY
        while (stoppingX <= stoppingY):
            ret += QPoint(x0 + x, y0 + y)
            ret += QPoint(x0 - x, y0 + y)
            ret += QPoint(x0 + x, y0 - y)
            ret += QPoint(x0 - x, y0 - y)
            x += 1
            stoppingX += twoYSquare
            ellipseError += xChange
            xChange += twoYSquare
            if ((2 * ellipseError + yChange) > 0):
                y -= 1
                stoppingY -= twoXSquare
                ellipseError += yChange
                yChange += twoXSquare

        return ret
Exemple #28
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
    def __init__(self, parent):
        super().__init__(parent)

        self.mObjectTypes = QVector()
 def __init__(self, parent):
     super().__init__(parent)
     
     self.mObjectTypes = QVector()
Exemple #31
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
    def read(self, fileName):
        # Read data.
        file = QFile(fileName)
        if (not file.open(QIODevice.ReadOnly)):
            self.mError = self.tr("Cannot open Replica Island map file!")
            return 0

        _in = QDataStream(file)
        _in.setByteOrder(QDataStream.LittleEndian)
        _in.setFloatingPointPrecision(QDataStream.SinglePrecision)
        # Parse file header.
        mapSignature = _in.readUInt8()
        layerCount = _in.readUInt8()
        backgroundIndex = _in.readUInt8()
        if (_in.status() == QDataStream.ReadPastEnd or mapSignature != 96):
            self.mError = self.tr("Can't parse file header!")
            return 0

        # Create our map, setting width and height to 0 until we load a layer.
        map = Map(Map.Orientation.Orthogonal, 0, 0, 32, 32)
        map.setProperty("background_index", QString.number(backgroundIndex))
        # Load our Tilesets.
        typeTilesets = QVector()
        tileIndexTilesets = QVector()

        self.loadTilesetsFromResources(map, typeTilesets, tileIndexTilesets)
        # Load each of our layers.
        for i in range(layerCount):
            # Parse layer header.
            _type = _in.readUInt8()
            tileIndex = _in.readUInt8()
            scrollSpeed = _in.readFloat()
            levelSignature = _in.readUInt8()
            width = _in.readUInt32()
            height = _in.readUInt32()
            if (_in.status() == QDataStream.ReadPastEnd
                    or levelSignature != 42):
                self.mError = self.tr("Can't parse layer header!")
                return 0

            # Make sure our width and height are consistent.
            if (map.width() == 0):
                map.setWidth(width)
            if (map.height() == 0):
                map.setHeight(height)
            if (map.width() != width or map.height() != height):
                self.mError = self.tr("Inconsistent layer sizes!")
                return 0

            # Create a layer object.
            layer = TileLayer(self.layerTypeToName(_type), 0, 0, width, height)
            layer.setProperty("type", QString.number(_type))
            layer.setProperty("tile_index", QString.number(tileIndex))
            layer.setProperty("scroll_speed", QString.number(scrollSpeed, 'f'))
            map.addLayer(layer)
            # Look up the tileset for this layer.
            tileset = tilesetForLayer(_type, tileIndex, typeTilesets,
                                      tileIndexTilesets)
            # Read our tile data all at once.
            #tileData = QByteArray(width*height, b'\x00')
            bytesNeeded = width * height
            tileData = _in.readRawData(bytesNeeded)
            bytesRead = len(tileData)
            if (bytesRead != bytesNeeded):
                self.mError = self.tr("File ended in middle of layer!")
                return 0

            i = 0
            # Add the tiles to our layer.
            for y in range(0, height):
                for x in range(0, width):
                    tile_id = tileData[i] & 0xff
                    i += 1
                    if (tile_id != 255):
                        tile = tileset.tileAt(tile_id)
                        layer.setCell(x, y, Cell(tile))

        # Make sure we read the entire *.bin file.
        if (_in.status() != QDataStream.Ok or not _in.atEnd()):
            self.mError = self.tr("Unexpected data at end of file!")
            return 0

        return map
Exemple #33
0
class Tile(Object):
    def __init__(self, *args):
        super().__init__(Object.TileType)
        
        l = len(args)
        if l==3:
            image, id, tileset = args
            self.mImageSource = QString()
        elif l==4:
            image, imageSource, id, tileset = args
            self.mImageSource = imageSource

        self.mId = id
        self.mTileset = tileset
        self.mImage = image
        self.mTerrain = 0xffffffff
        self.mProbability = 1.0
        self.mObjectGroup = None
        self.mFrames = QVector()
        self.mCurrentFrameIndex = 0
        self.mUnusedTime = 0

    def __del__(self):
        del self.mObjectGroup

    ##
    # Returns the tileset that this tile is part of as a shared pointer.
    ##
    def sharedTileset(self):
        return self.mTileset.sharedPointer()

    ##
    # Returns ID of this tile within its tileset.
    ##
    def id(self):
        return self.mId

    ##
    # Returns the tileset that this tile is part of.
    ##
    def tileset(self):
        return self.mTileset

    ##
    # Returns the image of this tile.
    ##
    def image(self):
        return QPixmap(self.mImage)

    ##
    # Returns the image for rendering this tile, taking into account tile
    # animations.
    ##
    def currentFrameImage(self):
        if (self.isAnimated()):
            frame = self.mFrames.at(self.mCurrentFrameIndex)
            return self.mTileset.tileAt(frame.tileId).image()
        else:
            return QPixmap(self.mImage)

    ##
    # Returns the drawing offset of the tile (in pixels).
    ##
    def offset(self):
        return self.mTileset.tileOffset()

    ##
    # Sets the image of this tile.
    ##
    def setImage(self, image):
        self.mImage = image

    ##
    # Returns the file name of the external image that represents this tile.
    # When this tile doesn't refer to an external image, an empty string is
    # returned.
    ##
    def imageSource(self):
        return self.mImageSource

    ##
    # Returns the file name of the external image that represents this tile.
    # When this tile doesn't refer to an external image, an empty string is
    # returned.
    ##
    def setImageSource(self, imageSource):
        self.mImageSource = imageSource

    ##
    # Returns the width of this tile.
    ##
    def width(self):
        return self.mImage.width()

    ##
    # Returns the height of this tile.
    ##
    def height(self):
        return self.mImage.height()

    ##
    # Returns the size of this tile.
    ##
    def size(self):
        return self.mImage.size()

    ##
    # Returns the Terrain of a given corner.
    ##
    def terrainAtCorner(self, corner):
        return self.mTileset.terrain(self.cornerTerrainId(corner))

    ##
    # Returns the terrain id at a given corner.
    ##
    def cornerTerrainId(self, corner):
        t = (self.terrain() >> (3 - corner)*8) & 0xFF
        if t == 0xFF:
            return -1
        return t

    ##
    # Set the terrain type of a given corner.
    ##
    def setCornerTerrainId(self, corner, terrainId):
        self.setTerrain(setTerrainCorner(self.mTerrain, corner, terrainId))

    ##
    # Returns the terrain for each corner of this tile.
    ##
    def terrain(self):
        return self.mTerrain

    ##
    # Set the terrain for each corner of the tile.
    ##
    def setTerrain(self, terrain):
        if (self.mTerrain == terrain):
            return
        self.mTerrain = terrain
        self.mTileset.markTerrainDistancesDirty()

    ##
    # Returns the probability of this terrain type appearing while painting (0-100%).
    ##
    def probability(self):
        return self.mProbability

    ##
    # Set the relative probability of this tile appearing while painting.
    ##
    def setProbability(self, probability):
        self.mProbability = probability

    ##
    # @return The group of objects associated with this tile. This is generally
    #         expected to be used for editing collision shapes.
    ##
    def objectGroup(self):
        return self.mObjectGroup

    ##
    # Sets \a objectGroup to be the group of objects associated with this tile.
    # The Tile takes ownership over the ObjectGroup and it can't also be part of
    # a map.
    ##
    def setObjectGroup(self, objectGroup):
        if (self.mObjectGroup == objectGroup):
            return
        del self.mObjectGroup
        self.mObjectGroup = objectGroup

    ##
    # Swaps the object group of this tile with \a objectGroup. The tile releases
    # ownership over its existing object group and takes ownership over the new
    # one.
    #
    # @return The previous object group referenced by this tile.
    ##
    def swapObjectGroup(self, objectGroup):
        previousObjectGroup = self.mObjectGroup
        self.mObjectGroup = objectGroup
        return previousObjectGroup

    def frames(self):
        return self.mFrames

    ##
    # Sets the animation frames to be used by this tile. Resets any currently
    # running animation.
    ##
    def setFrames(self, frames):
        self.mFrames = frames
        self.mCurrentFrameIndex = 0
        self.mUnusedTime = 0

    def isAnimated(self):
        return not self.mFrames.isEmpty()

    def currentFrameIndex(self):
        return self.mCurrentFrameIndex

    ##
    # Advances this tile animation by the given amount of milliseconds. Returns
    # whether this caused the current tileId to change.
    ##
    def advanceAnimation(self, ms):
        if (not self.isAnimated()):
            return False
        self.mUnusedTime += ms
        frame = self.mFrames.at(self.mCurrentFrameIndex)
        previousTileId = frame.tileId
        while (frame.duration > 0 and self.mUnusedTime > frame.duration):
            self.mUnusedTime -= frame.duration
            self.mCurrentFrameIndex = (self.mCurrentFrameIndex + 1) % self.mFrames.size()
            frame = self.mFrames.at(self.mCurrentFrameIndex)

        return previousTileId != frame.tileId
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
Exemple #35
0
 def __init__(self):
     self.mCurrentProgramName = QString()
     self.mOptions = QVector()
     self.mShowHelp = False
     self.mLongestArgument = 0
     self.mFilesToOpen = QStringList()
Exemple #36
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))
Exemple #37
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()
Exemple #38
0
    def __init__(self, painter):
        self.mPainter = painter
        self.mTile = None
        self.mIsOpenGL = hasOpenGLEngine(painter)

        self.mFragments = QVector()
Exemple #39
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
Exemple #40
0
class StampBrush(AbstractTileTool):
    ##
    # Emitted when the currently selected tiles changed. The stamp brush emits
    # this signal instead of setting its stamp directly so that the fill tool
    # also gets the new stamp.
    ##
    currentTilesChanged = pyqtSignal(list)

    ##
    # Emitted when a stamp was captured from the map. The stamp brush emits
    # this signal instead of setting its stamp directly so that the fill tool
    # also gets the new stamp.
    ##
    stampCaptured = pyqtSignal(TileStamp)

    def __init__(self, parent=None):
        super().__init__(self.tr("Stamp Brush"),
                         QIcon(":images/22x22/stock-tool-clone.png"),
                         QKeySequence(self.tr("B")), parent)
        ##
        # This stores the current behavior.
        ##
        self.mBrushBehavior = BrushBehavior.Free

        self.mIsRandom = False
        self.mCaptureStart = QPoint()
        self.mRandomCellPicker = RandomPicker()
        ##
        # mStamp is a tile layer in which is the selection the user made
        # either by rightclicking (Capture) or at the tilesetdock
        ##
        self.mStamp = TileStamp()
        self.mPreviewLayer = None
        self.mMissingTilesets = QVector()
        self.mPrevTilePosition = QPoint()
        self.mStampReference = QPoint()

    def __del__(self):
        pass

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

    def mousePressed(self, event):
        if (not self.brushItem().isVisible()):
            return
        if (event.button() == Qt.LeftButton):
            x = self.mBrushBehavior
            if x == BrushBehavior.Line:
                self.mStampReference = self.tilePosition()
                self.mBrushBehavior = BrushBehavior.LineStartSet
            elif x == BrushBehavior.Circle:
                self.mStampReference = self.tilePosition()
                self.mBrushBehavior = BrushBehavior.CircleMidSet
            elif x == BrushBehavior.LineStartSet:
                self.doPaint()
                self.mStampReference = self.tilePosition()
            elif x == BrushBehavior.CircleMidSet:
                self.doPaint()
            elif x == BrushBehavior.Paint:
                self.beginPaint()
            elif x == BrushBehavior.Free:
                self.beginPaint()
                self.mBrushBehavior = BrushBehavior.Paint
            elif x == BrushBehavior.Capture:
                pass
        else:
            if (event.button() == Qt.RightButton):
                self.beginCapture()

    def mouseReleased(self, event):
        x = self.mBrushBehavior
        if x == BrushBehavior.Capture:
            if (event.button() == Qt.RightButton):
                self.endCapture()
                self.mBrushBehavior = BrushBehavior.Free
        elif x == BrushBehavior.Paint:
            if (event.button() == Qt.LeftButton):
                self.mBrushBehavior = BrushBehavior.Free
                # allow going over different variations by repeatedly clicking
                self.updatePreview()
        else:
            # do nothing?
            pass

    def modifiersChanged(self, modifiers):
        if self.mStamp.isEmpty():
            return
        if (modifiers & Qt.ShiftModifier):
            if (modifiers & Qt.ControlModifier):
                if self.mBrushBehavior == BrushBehavior.LineStartSet:
                    self.mBrushBehavior = BrushBehavior.CircleMidSet
                else:
                    self.mBrushBehavior = BrushBehavior.Circle
            else:
                if self.mBrushBehavior == BrushBehavior.CircleMidSet:
                    self.mBrushBehavior = BrushBehavior.LineStartSet
                else:
                    self.mBrushBehavior = BrushBehavior.Line
        else:
            self.mBrushBehavior = BrushBehavior.Free

        self.updatePreview()

    def languageChanged(self):
        self.setName(self.tr("Stamp Brush"))
        self.setShortcut(QKeySequence(self.tr("B")))

    ##
    # Sets the stamp that is drawn when painting. The stamp brush takes
    # ownership over the stamp layer.
    ##
    def setStamp(self, stamp):
        if (self.mStamp == stamp):
            return

        self.mStamp = stamp
        self.updateRandomList()
        self.updatePreview()

    ##
    # This returns the current tile stamp used for painting.
    ##
    def stamp(self):
        return self.mStamp

    def setRandom(self, value):
        if self.mIsRandom == value:
            return
        self.mIsRandom = value

        self.updateRandomList()
        self.updatePreview()

    def tilePositionChanged(self, pos):
        x = self.mBrushBehavior
        if x == BrushBehavior.Paint:
            # Draw a line from the previous point to avoid gaps, skipping the
            # first point, since it was painted when the mouse was pressed, or the
            # last time the mouse was moved.
            points = pointsOnLine(self.mPrevTilePosition, pos)
            editedRegion = QRegion()

            ptSize = points.size()
            ptLast = ptSize - 1
            for i in range(1, ptSize):
                self.drawPreviewLayer(QVector(points[i]))

                # Only update the brush item for the last drawn piece
                if i == ptLast:
                    self.brushItem().setTileLayer(self.mPreviewLayer)

                editedRegion |= self.doPaint(PaintFlags.Mergeable
                                             | PaintFlags.SuppressRegionEdited)

            self.mapDocument().emitRegionEdited(editedRegion,
                                                self.currentTileLayer())
        else:
            self.updatePreview()

        self.mPrevTilePosition = pos

    def mapDocumentChanged(self, oldDocument, newDocument):
        super().mapDocumentChanged(oldDocument, newDocument)

        if newDocument:
            self.updateRandomList()
            self.updatePreview()

    def beginPaint(self):
        if (self.mBrushBehavior != BrushBehavior.Free):
            return
        self.mBrushBehavior = BrushBehavior.Paint
        self.doPaint()

    ##
    # Merges the tile layer of its brush item into the current map.
    #
    # \a flags can be set to Mergeable, which applies to the undo command.
    #
    # \a offsetX and \a offsetY give an offset where to merge the brush items tile
    # layer into the current map.
    #
    # Returns the edited region.
    ##
    def doPaint(self, flags=0):
        preview = self.mPreviewLayer
        if not preview:
            return QRegion()

        # This method shouldn't be called when current layer is not a tile layer
        tileLayer = self.currentTileLayer()
        if (not tileLayer.bounds().intersects(
                QRect(preview.x(), preview.y(), preview.width(),
                      preview.height()))):
            return QRegion()

        paint = PaintTileLayer(self.mapDocument(), tileLayer, preview.x(),
                               preview.y(), preview)

        if not self.mMissingTilesets.isEmpty():
            for tileset in self.mMissingTilesets:
                AddTileset(self.mapDocument(), tileset, paint)

            self.mMissingTilesets.clear()

        paint.setMergeable(flags & PaintFlags.Mergeable)
        self.mapDocument().undoStack().push(paint)

        editedRegion = preview.region()
        if (not (flags & PaintFlags.SuppressRegionEdited)):
            self.mapDocument().emitRegionEdited(editedRegion, tileLayer)
        return editedRegion

    def beginCapture(self):
        if (self.mBrushBehavior != BrushBehavior.Free):
            return
        self.mBrushBehavior = BrushBehavior.Capture
        self.mCaptureStart = self.tilePosition()
        self.setStamp(TileStamp())

    def endCapture(self):
        if (self.mBrushBehavior != BrushBehavior.Capture):
            return
        self.mBrushBehavior = BrushBehavior.Free
        tileLayer = self.currentTileLayer()
        # Intersect with the layer and translate to layer coordinates
        captured = self.capturedArea()
        captured &= QRect(tileLayer.x(), tileLayer.y(), tileLayer.width(),
                          tileLayer.height())
        if (captured.isValid()):
            captured.translate(-tileLayer.x(), -tileLayer.y())
            map = tileLayer.map()
            capture = tileLayer.copy(captured)

            stamp = Map(map.orientation(), capture.width(), capture.height(),
                        map.tileWidth(), map.tileHeight())
            # Add tileset references to map
            for tileset in capture.usedTilesets():
                stamp.addTileset(tileset)

            stamp.addLayer(capture)

            self.stampCaptured.emit(TileStamp(stamp))
        else:
            self.updatePreview()

    def capturedArea(self):
        captured = QRect(self.mCaptureStart, self.tilePosition()).normalized()
        if (captured.width() == 0):
            captured.adjust(-1, 0, 1, 0)
        if (captured.height() == 0):
            captured.adjust(0, -1, 0, 1)
        return captured

    ##
    # Updates the position of the brush item.
    ##
    def updatePreview(self, *args):
        l = len(args)
        if l == 0:
            ##
            # Updates the position of the brush item based on the mouse position.
            ##
            self.updatePreview(self.tilePosition())
        elif l == 1:
            tilePos = args[0]

            if not self.mapDocument():
                return

            tileRegion = QRegion()

            if self.mBrushBehavior == BrushBehavior.Capture:
                self.mPreviewLayer = None
                tileRegion = self.capturedArea()
            elif self.mStamp.isEmpty():
                self.mPreviewLayer = None
                tileRegion = QRect(tilePos, QSize(1, 1))
            else:
                if self.mBrushBehavior == BrushBehavior.LineStartSet:
                    self.drawPreviewLayer(
                        pointsOnLine(self.mStampReference, tilePos))
                elif self.mBrushBehavior == BrushBehavior.CircleMidSet:
                    self.drawPreviewLayer(
                        pointsOnEllipse(self.mStampReference, tilePos))
                elif self.mBrushBehavior == BrushBehavior.Capture:
                    # already handled above
                    pass
                elif self.mBrushBehavior == BrushBehavior.Circle:
                    # while finding the mid point, there is no need to show
                    # the (maybe bigger than 1x1) stamp
                    self.mPreviewLayer.clear()
                    tileRegion = QRect(tilePos, QSize(1, 1))
                elif self.mBrushBehavior == BrushBehavior.Line or self.mBrushBehavior == BrushBehavior.Free or self.mBrushBehavior == BrushBehavior.Paint:
                    self.drawPreviewLayer(QVector(tilePos))

            self.brushItem().setTileLayer(self.mPreviewLayer)
            if not tileRegion.isEmpty():
                self.brushItem().setTileRegion(tileRegion)

    ##
    # Updates the list used random stamps.
    # This is done by taking all non-null tiles from the original stamp mStamp.
    ##
    def updateRandomList(self):
        self.mRandomCellPicker.clear()

        if not self.mIsRandom:
            return

        self.mMissingTilesets.clear()

        for variation in self.mStamp.variations():
            self.mapDocument().unifyTilesets(variation.map,
                                             self.mMissingTilesets)
            tileLayer = variation.tileLayer()
            for x in range(tileLayer.width()):
                for y in range(tileLayer.height()):
                    cell = tileLayer.cellAt(x, y)
                    if not cell.isEmpty():
                        self.mRandomCellPicker.add(cell,
                                                   cell.tile.probability())

    ##
    # Draws the preview layer.
    # It tries to put at all given points a stamp of the current stamp at the
    # corresponding position.
    # It also takes care, that no overlaps appear.
    # So it will check for every point if it can place a stamp there without
    # overlap.
    ##
    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
Exemple #41
0
    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()
Exemple #42
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
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()
class AutoMapperWrapper(QUndoCommand):
    def __init__(self, mapDocument, autoMapper, where):
        super().__init__()
        
        self.mLayersAfter = QVector()
        self.mLayersBefore = QVector()
        self.mMapDocument = mapDocument
        map = self.mMapDocument.Map()
        touchedLayers = QSet()
        index = 0
        while (index < autoMapper.size()):
            a = autoMapper.at(index)
            if (a.prepareAutoMap()):
                touchedLayers|= a.getTouchedTileLayers()
                index += 1
            else:
                autoMapper.remove(index)

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

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

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

        for a in autoMapper:
            a.cleanAll()

    def __del__(self):
        for i in self.mLayersAfter:
            del i
        for i in self.mLayersBefore:
            del i

    def undo(self):
        map = self.mMapDocument.Map()
        for layer in self.mLayersBefore:
            layerindex = map.indexOfLayer(layer.name())
            if (layerindex != -1):
                self.patchLayer(layerindex, layer)

    def redo(self):
        map = self.mMapDocument.Map()
        for layer in self.mLayersAfter:
            layerindex = (map.indexOfLayer(layer.name()))
            if (layerindex != -1):
                self.patchLayer(layerindex, layer)

    def patchLayer(self, layerIndex, layer):
        map = self.mMapDocument.Map()
        b = layer.bounds()
        t = map.layerAt(layerIndex)
        t.setCells(b.left() - t.x(), b.top() - t.y(), layer,
                    b.translated(-t.position()))
        self.mMapDocument.emitRegionChanged(b, t)
Exemple #45
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)
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()
Exemple #47
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)
class AutoMapperWrapper(QUndoCommand):
    def __init__(self, mapDocument, autoMapper, where):
        super().__init__()

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

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

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

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

        for a in autoMapper:
            a.cleanAll()

    def __del__(self):
        for i in self.mLayersAfter:
            del i
        for i in self.mLayersBefore:
            del i

    def undo(self):
        map = self.mMapDocument.Map()
        for layer in self.mLayersBefore:
            layerindex = map.indexOfLayer(layer.name())
            if (layerindex != -1):
                self.patchLayer(layerindex, layer)

    def redo(self):
        map = self.mMapDocument.Map()
        for layer in self.mLayersAfter:
            layerindex = (map.indexOfLayer(layer.name()))
            if (layerindex != -1):
                self.patchLayer(layerindex, layer)

    def patchLayer(self, layerIndex, layer):
        map = self.mMapDocument.Map()
        b = layer.bounds()
        t = map.layerAt(layerIndex)
        t.setCells(b.left() - t.x(),
                   b.top() - t.y(), layer, b.translated(-t.position()))
        self.mMapDocument.emitRegionChanged(b, t)
Exemple #49
0
class MapScene(QGraphicsScene):
    selectedObjectItemsChanged = pyqtSignal()

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

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

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

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

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

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

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

        self.refreshScene()

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

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

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

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

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

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

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

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

        offset = QPointF()

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

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

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

        return super().event(event)

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

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

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

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

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

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

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

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

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

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

        self.updateSceneRect()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            return

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

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

        return False
Exemple #50
0
class BucketFillTool(AbstractTileTool):
    def tr(self, sourceText, disambiguation = '', n = -1):
        return QCoreApplication.translate('BucketFillTool', sourceText, disambiguation, n)

    def __init__(self, parent = None):
        super().__init__(self.tr("Bucket Fill Tool"),
                           QIcon(":images/22x22/stock-tool-bucket-fill.png"),
                           QKeySequence(self.tr("F")),
                           parent)
        self.mStamp = TileStamp()
        self.mFillOverlay = None
        self.mFillRegion = QRegion()
        self.mMissingTilesets = QVector()
        self.mIsActive = False
        self.mLastShiftStatus = False
        ##
        # Indicates if the tool is using the random mode.
        ##
        self.mIsRandom = False
        ##
        # Contains the value of mIsRandom at that time, when the latest call of
        # tilePositionChanged() took place.
        # This variable is needed to detect if the random mode was changed during
        # mFillOverlay being brushed at an area.
        ##
        self.mLastRandomStatus = False
        ##
        # Contains all used random cells to use in random mode.
        # The same cell can be in the list multiple times to make different
        # random weights possible.
        ##
        self.mRandomCellPicker = RandomPicker()

    def __del__(self):
        pass

    def activate(self, scene):
        super().activate(scene)
        self.mIsActive = True
        self.tilePositionChanged(self.tilePosition())

    def deactivate(self, scene):
        super().deactivate(scene)
        self.mFillRegion = QRegion()
        self.mIsActive = False

    def mousePressed(self, event):
        if (event.button() != Qt.LeftButton or self.mFillRegion.isEmpty()):
            return
        if (not self.brushItem().isVisible()):
            return
        
        preview = self.mFillOverlay
        if not preview:
            return

        paint = PaintTileLayer(self.mapDocument(),
                                       self.currentTileLayer(),
                                       preview.x(),
                                       preview.y(),
                                       preview)

        paint.setText(QCoreApplication.translate("Undo Commands", "Fill Area"))

        if not self.mMissingTilesets.isEmpty():
            for tileset in self.mMissingTilesets:
                AddTileset(self.mapDocument(), tileset, paint)

            self.mMissingTilesets.clear()
            
        fillRegion = QRegion(self.mFillRegion)
        self.mapDocument().undoStack().push(paint)
        self.mapDocument().emitRegionEdited(fillRegion, self.currentTileLayer())

    def mouseReleased(self, event):
        pass

    def modifiersChanged(self, modifiers):
        # Don't need to recalculate fill region if there was no fill region
        if (not self.mFillOverlay):
            return
        self.tilePositionChanged(self.tilePosition())

    def languageChanged(self):
        self.setName(self.tr("Bucket Fill Tool"))
        self.setShortcut(QKeySequence(self.tr("F")))

    ##
    # Sets the stamp that is drawn when filling. The BucketFillTool takes
    # ownership over the stamp layer.
    ##
    def setStamp(self, stamp):
        # Clear any overlay that we presently have with an old stamp
        self.clearOverlay()
        self.mStamp = stamp
        self.updateRandomListAndMissingTilesets()
        if (self.mIsActive and self.brushItem().isVisible()):
            self.tilePositionChanged(self.tilePosition())

    ##
    # This returns the actual tile layer which is used to define the current
    # state.
    ##
    def stamp(self):
        return TileStamp(self.mStamp)

    def setRandom(self, value):
        if (self.mIsRandom == value):
            return
        self.mIsRandom = value
        self.updateRandomListAndMissingTilesets()
        
        # Don't need to recalculate fill region if there was no fill region
        if (not self.mFillOverlay):
            return
        self.tilePositionChanged(self.tilePosition())

    def tilePositionChanged(self, tilePos):
        # Skip filling if the stamp is empty
        if  self.mStamp.isEmpty():
            return
            
        # Make sure that a tile layer is selected
        tileLayer = self.currentTileLayer()
        if (not tileLayer):
            return
        
        shiftPressed = QApplication.keyboardModifiers() & Qt.ShiftModifier
        fillRegionChanged = False
        
        regionComputer = TilePainter(self.mapDocument(), tileLayer)
        # If the stamp is a single tile, ignore it when making the region
        if (not shiftPressed and self.mStamp.variations().size() == 1):
            variation = self.mStamp.variations().first()
            stampLayer = variation.tileLayer()
            if (stampLayer.size() == QSize(1, 1) and stampLayer.cellAt(0, 0) == regionComputer.cellAt(tilePos)):
                return
            
        # This clears the connections so we don't get callbacks
        self.clearConnections(self.mapDocument())
        # Optimization: we don't need to recalculate the fill area
        # if the new mouse position is still over the filled region
        # and the shift modifier hasn't changed.
        if (not self.mFillRegion.contains(tilePos) or shiftPressed != self.mLastShiftStatus):
            # Clear overlay to make way for a new one
            self.clearOverlay()
            # Cache information about how the fill region was created
            self.mLastShiftStatus = shiftPressed
            # Get the new fill region
            if (not shiftPressed):
                # If not holding shift, a region is generated from the current pos
                self.mFillRegion = regionComputer.computePaintableFillRegion(tilePos)
            else:
                # If holding shift, the region is the selection bounds
                self.mFillRegion = self.mapDocument().selectedArea()
                # Fill region is the whole map if there is no selection
                if (self.mFillRegion.isEmpty()):
                    self.mFillRegion = tileLayer.bounds()
                # The mouse needs to be in the region
                if (not self.mFillRegion.contains(tilePos)):
                    self.mFillRegion = QRegion()

            fillRegionChanged = True

        # Ensure that a fill region was created before making an overlay layer
        if (self.mFillRegion.isEmpty()):
            return
        if (self.mLastRandomStatus != self.mIsRandom):
            self.mLastRandomStatus = self.mIsRandom
            fillRegionChanged = True

        if (not self.mFillOverlay):
            # Create a new overlay region
            fillBounds = self.mFillRegion.boundingRect()
            self.mFillOverlay = TileLayer(QString(),
                                         fillBounds.x(),
                                         fillBounds.y(),
                                         fillBounds.width(),
                                         fillBounds.height())

        # Paint the new overlay
        if (not self.mIsRandom):
            if (fillRegionChanged or self.mStamp.variations().size() > 1):
                fillWithStamp(self.mFillOverlay, self.mStamp, self.mFillRegion.translated(-self.mFillOverlay.position()))
                fillRegionChanged = True
        else:
            self.randomFill(self.mFillOverlay, self.mFillRegion)
            fillRegionChanged = True

        if (fillRegionChanged):
            # Update the brush item to draw the overlay
            self.brushItem().setTileLayer(self.mFillOverlay)

        # Create connections to know when the overlay should be cleared
        self.makeConnections()

    def mapDocumentChanged(self, oldDocument, newDocument):
        super().mapDocumentChanged(oldDocument, newDocument)
        self.clearConnections(oldDocument)
        # Reset things that are probably invalid now
        if newDocument:
            self.updateRandomListAndMissingTilesets()

        self.clearOverlay()

    def clearOverlay(self):
        # Clear connections before clearing overlay so there is no
        # risk of getting a callback and causing an infinite loop
        self.clearConnections(self.mapDocument())
        self.brushItem().clear()
        self.mFillOverlay = None

        self.mFillRegion = QRegion()

    def makeConnections(self):
        if (not self.mapDocument()):
            return
        # Overlay may need to be cleared if a region changed
        self.mapDocument().regionChanged.connect(self.clearOverlay)
        # Overlay needs to be cleared if we switch to another layer
        self.mapDocument().currentLayerIndexChanged.connect(self.clearOverlay)
        # Overlay needs be cleared if the selection changes, since
        # the overlay may be bound or may need to be bound to the selection
        self.mapDocument().selectedAreaChanged.connect(self.clearOverlay)

    def clearConnections(self, mapDocument):
        if (not mapDocument):
            return
        try:
            mapDocument.regionChanged.disconnect(self.clearOverlay)
            mapDocument.currentLayerIndexChanged.disconnect(self.clearOverlay)
            mapDocument.selectedAreaChanged.disconnect(self.clearOverlay)
        except:
            pass

    ##
    # Updates the list of random cells.
    # This is done by taking all non-null tiles from the original stamp mStamp.
    ##
    def updateRandomListAndMissingTilesets(self):
        self.mRandomCellPicker.clear()
        self.mMissingTilesets.clear()
        
        for variation in self.mStamp.variations():
            self.mapDocument().unifyTilesets(variation.map, self.mMissingTilesets)

            if self.mIsRandom:
                for cell in variation.tileLayer():
                    if not cell.isEmpty():
                        self.mRandomCellPicker.add(cell, cell.tile.probability())

    ##
    # Fills the given \a region in the given \a tileLayer with random tiles.
    ##
    def randomFill(self, tileLayer, region):
        if (region.isEmpty() or self.mRandomList.empty()):
            return
        for rect in region.translated(-tileLayer.position()).rects():
            for _x in range(rect.left(), rect.right()+1):
                for _y in range(rect.top(), rect.bottom()+1):
                    tileLayer.setCell(_x, _y, self.mRandomCellPicker.pick())
Exemple #51
0
    def __init__(self, *args):
        self.mOrientation = 0
        self.mRenderOrder = 0
        self.mWidth = 0
        self.mHeight = 0
        self.mTileWidth = 0
        self.mTileHeight = 0
        self.mHexSideLength = 0
        self.mStaggerAxis = 0
        self.mStaggerIndex = 0
        self.mBackgroundColor = QColor()
        self.mDrawMargins = QMargins()
        self.mLayers = QList()
        self.mTilesets = QVector()
        self.mLayerDataFormat = None
        self.mNextObjectId = 0

        l = len(args)
        if l == 1:
            ##
            # Copy constructor. Makes sure that a deep-copy of the layers is created.
            ##
            map = args[0]
            super().__init__(map)

            self.mLayers = QList()
            self.mOrientation = map.mOrientation
            self.mRenderOrder = map.mRenderOrder
            self.mWidth = map.mWidth
            self.mHeight = map.mHeight
            self.mTileWidth = map.mTileWidth
            self.mTileHeight = map.mTileHeight
            self.mHexSideLength = map.mHexSideLength
            self.mStaggerAxis = map.mStaggerAxis
            self.mStaggerIndex = map.mStaggerIndex
            self.mBackgroundColor = map.mBackgroundColor
            self.mDrawMargins = map.mDrawMargins
            self.mTilesets = map.mTilesets
            self.mLayerDataFormat = map.mLayerDataFormat
            self.mNextObjectId = 1
            for layer in map.mLayers:
                clone = layer.clone()
                clone.setMap(self)
                self.mLayers.append(clone)
        elif l == 5:
            ##
            # Constructor, taking map orientation, size and tile size as parameters.
            ##
            orientation, width, height, tileWidth, tileHeight = args
            super().__init__(Object.MapType)

            self.mLayers = QList()
            self.mTilesets = QList()
            self.mOrientation = orientation
            self.mRenderOrder = Map.RenderOrder.RightDown
            self.mWidth = width
            self.mHeight = height
            self.mTileWidth = tileWidth
            self.mTileHeight = tileHeight
            self.mHexSideLength = 0
            self.mStaggerAxis = Map.StaggerAxis.StaggerY
            self.mStaggerIndex = Map.StaggerIndex.StaggerOdd
            self.mLayerDataFormat = Map.LayerDataFormat.Base64Zlib
            self.mNextObjectId = 1
    def __init__(self, parent):
        super().__init__(parent)

        self.mFrames = QVector()
        self.mTileset = None
Exemple #53
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)
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()
Exemple #55
0
class TilesetDock(QDockWidget):
    ##
    # Emitted when the current tile changed.
    ##
    currentTileChanged = pyqtSignal(list)
    ##
    # Emitted when the currently selected tiles changed.
    ##
    stampCaptured = pyqtSignal(TileStamp)
    ##
    # Emitted when files are dropped at the tileset dock.
    ##
    tilesetsDropped = pyqtSignal(QStringList)
    newTileset = pyqtSignal()

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

        # Shared tileset references because the dock wants to add new tiles
        self.mTilesets = QVector()
        self.mCurrentTilesets = QMap()
        self.mMapDocument = None
        self.mTabBar = QTabBar()
        self.mViewStack = QStackedWidget()
        self.mToolBar = QToolBar()
        self.mCurrentTile = None
        self.mCurrentTiles = None
        self.mNewTileset = QAction(self)
        self.mImportTileset = QAction(self)
        self.mExportTileset = QAction(self)
        self.mPropertiesTileset = QAction(self)
        self.mDeleteTileset = QAction(self)
        self.mEditTerrain = QAction(self)
        self.mAddTiles = QAction(self)
        self.mRemoveTiles = QAction(self)
        self.mTilesetMenuButton = TilesetMenuButton(self)
        self.mTilesetMenu = QMenu(self)  # opens on click of mTilesetMenu
        self.mTilesetActionGroup = QActionGroup(self)
        self.mTilesetMenuMapper = None  # needed due to dynamic content
        self.mEmittingStampCaptured = False
        self.mSynchronizingSelection = False

        self.setObjectName("TilesetDock")
        self.mTabBar.setMovable(True)
        self.mTabBar.setUsesScrollButtons(True)
        self.mTabBar.currentChanged.connect(self.updateActions)
        self.mTabBar.tabMoved.connect(self.moveTileset)
        w = QWidget(self)
        horizontal = QHBoxLayout()
        horizontal.setSpacing(0)
        horizontal.addWidget(self.mTabBar)
        horizontal.addWidget(self.mTilesetMenuButton)
        vertical = QVBoxLayout(w)
        vertical.setSpacing(0)
        vertical.setContentsMargins(5, 5, 5, 5)
        vertical.addLayout(horizontal)
        vertical.addWidget(self.mViewStack)
        horizontal = QHBoxLayout()
        horizontal.setSpacing(0)
        horizontal.addWidget(self.mToolBar, 1)
        vertical.addLayout(horizontal)
        self.mNewTileset.setIcon(QIcon(":images/16x16/document-new.png"))
        self.mImportTileset.setIcon(QIcon(":images/16x16/document-import.png"))
        self.mExportTileset.setIcon(QIcon(":images/16x16/document-export.png"))
        self.mPropertiesTileset.setIcon(
            QIcon(":images/16x16/document-properties.png"))
        self.mDeleteTileset.setIcon(QIcon(":images/16x16/edit-delete.png"))
        self.mEditTerrain.setIcon(QIcon(":images/16x16/terrain.png"))
        self.mAddTiles.setIcon(QIcon(":images/16x16/add.png"))
        self.mRemoveTiles.setIcon(QIcon(":images/16x16/remove.png"))
        Utils.setThemeIcon(self.mNewTileset, "document-new")
        Utils.setThemeIcon(self.mImportTileset, "document-import")
        Utils.setThemeIcon(self.mExportTileset, "document-export")
        Utils.setThemeIcon(self.mPropertiesTileset, "document-properties")
        Utils.setThemeIcon(self.mDeleteTileset, "edit-delete")
        Utils.setThemeIcon(self.mAddTiles, "add")
        Utils.setThemeIcon(self.mRemoveTiles, "remove")
        self.mNewTileset.triggered.connect(self.newTileset)
        self.mImportTileset.triggered.connect(self.importTileset)
        self.mExportTileset.triggered.connect(self.exportTileset)
        self.mPropertiesTileset.triggered.connect(self.editTilesetProperties)
        self.mDeleteTileset.triggered.connect(self.removeTileset)
        self.mEditTerrain.triggered.connect(self.editTerrain)
        self.mAddTiles.triggered.connect(self.addTiles)
        self.mRemoveTiles.triggered.connect(self.removeTiles)
        self.mToolBar.addAction(self.mNewTileset)
        self.mToolBar.setIconSize(QSize(16, 16))
        self.mToolBar.addAction(self.mImportTileset)
        self.mToolBar.addAction(self.mExportTileset)
        self.mToolBar.addAction(self.mPropertiesTileset)
        self.mToolBar.addAction(self.mDeleteTileset)
        self.mToolBar.addAction(self.mEditTerrain)
        self.mToolBar.addAction(self.mAddTiles)
        self.mToolBar.addAction(self.mRemoveTiles)
        self.mZoomable = Zoomable(self)
        self.mZoomComboBox = QComboBox()
        self.mZoomable.connectToComboBox(self.mZoomComboBox)
        horizontal.addWidget(self.mZoomComboBox)
        self.mViewStack.currentChanged.connect(self.updateCurrentTiles)
        TilesetManager.instance().tilesetChanged.connect(self.tilesetChanged)
        DocumentManager.instance().documentAboutToClose.connect(
            self.documentAboutToClose)
        self.mTilesetMenuButton.setMenu(self.mTilesetMenu)
        self.mTilesetMenu.aboutToShow.connect(self.refreshTilesetMenu)
        self.setWidget(w)
        self.retranslateUi()
        self.setAcceptDrops(True)
        self.updateActions()

    def __del__(self):
        del self.mCurrentTiles

    ##
    # Sets the map for which the tilesets should be displayed.
    ##
    def setMapDocument(self, mapDocument):
        if (self.mMapDocument == mapDocument):
            return
        # Hide while we update the tab bar, to avoid repeated layouting
        if sys.platform != 'darwin':
            self.widget().hide()

        self.setCurrentTiles(None)
        self.setCurrentTile(None)

        if (self.mMapDocument):
            # Remember the last visible tileset for this map
            tilesetName = self.mTabBar.tabText(self.mTabBar.currentIndex())
            self.mCurrentTilesets.insert(self.mMapDocument, tilesetName)

        # Clear previous content
        while (self.mTabBar.count()):
            self.mTabBar.removeTab(0)
        while (self.mViewStack.count()):
            self.mViewStack.removeWidget(self.mViewStack.widget(0))
        #self.mTilesets.clear()
        # Clear all connections to the previous document
        if (self.mMapDocument):
            self.mMapDocument.disconnect()
        self.mMapDocument = mapDocument
        if (self.mMapDocument):
            self.mTilesets = self.mMapDocument.map().tilesets()
            for tileset in self.mTilesets:
                view = TilesetView()
                view.setMapDocument(self.mMapDocument)
                view.setZoomable(self.mZoomable)
                self.mTabBar.addTab(tileset.name())
                self.mViewStack.addWidget(view)

            self.mMapDocument.tilesetAdded.connect(self.tilesetAdded)
            self.mMapDocument.tilesetRemoved.connect(self.tilesetRemoved)
            self.mMapDocument.tilesetMoved.connect(self.tilesetMoved)
            self.mMapDocument.tilesetNameChanged.connect(
                self.tilesetNameChanged)
            self.mMapDocument.tilesetFileNameChanged.connect(
                self.updateActions)
            self.mMapDocument.tilesetChanged.connect(self.tilesetChanged)
            self.mMapDocument.tileAnimationChanged.connect(
                self.tileAnimationChanged)
            cacheName = self.mCurrentTilesets.take(self.mMapDocument)
            for i in range(self.mTabBar.count()):
                if (self.mTabBar.tabText(i) == cacheName):
                    self.mTabBar.setCurrentIndex(i)
                    break

            object = self.mMapDocument.currentObject()
            if object:
                if object.typeId() == Object.TileType:
                    self.setCurrentTile(object)

        self.updateActions()
        if sys.platform != 'darwin':
            self.widget().show()

    ##
    # Synchronizes the selection with the given stamp. Ignored when the stamp is
    # changing because of a selection change in the TilesetDock.
    ##
    def selectTilesInStamp(self, stamp):
        if self.mEmittingStampCaptured:
            return
        processed = QSet()
        selections = QMap()
        for variation in stamp.variations():
            tileLayer = variation.tileLayer()
            for cell in tileLayer:
                tile = cell.tile
                if tile:
                    if (processed.contains(tile)):
                        continue
                    processed.insert(tile)  # avoid spending time on duplicates
                    tileset = tile.tileset()
                    tilesetIndex = self.mTilesets.indexOf(
                        tileset.sharedPointer())
                    if (tilesetIndex != -1):
                        view = self.tilesetViewAt(tilesetIndex)
                        if (not view.model()):  # Lazily set up the model
                            self.setupTilesetModel(view, tileset)
                        model = view.tilesetModel()
                        modelIndex = model.tileIndex(tile)
                        selectionModel = view.selectionModel()

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

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

            # Show/edit properties of all captured tiles
            self.mMapDocument.setSelectedTiles(processed.toList())
            # Update the current tile (useful for animation and collision editors)
            first = selections.first()
            selectionModel = first[0]
            selection = first[1]
            currentIndex = QModelIndex(selection.first().topLeft())
            if (selectionModel.currentIndex() != currentIndex):
                selectionModel.setCurrentIndex(currentIndex,
                                               QItemSelectionModel.NoUpdate)
            else:
                self.currentChanged(currentIndex)
            self.mSynchronizingSelection = False

    def currentTilesetChanged(self):
        view = self.currentTilesetView()
        if view:
            s = view.selectionModel()
            if s:
                self.setCurrentTile(view.tilesetModel().tileAt(
                    s.currentIndex()))

    ##
    # Returns the currently selected tile.
    ##
    def currentTile(self):
        return self.mCurrentTile

    def changeEvent(self, e):
        super().changeEvent(e)
        x = e.type()
        if x == QEvent.LanguageChange:
            self.retranslateUi()
        else:
            pass

    def dragEnterEvent(self, e):
        urls = e.mimeData().urls()
        if (not urls.isEmpty() and not urls.at(0).toLocalFile().isEmpty()):
            e.accept()

    def dropEvent(self, e):
        paths = QStringList()
        for url in e.mimeData().urls():
            localFile = url.toLocalFile()
            if (not localFile.isEmpty()):
                paths.append(localFile)

        if (not paths.isEmpty()):
            self.tilesetsDropped.emit(paths)
            e.accept()

    def selectionChanged(self):
        self.updateActions()
        if not self.mSynchronizingSelection:
            self.updateCurrentTiles()

    def currentChanged(self, index):
        if (not index.isValid()):
            return
        model = index.model()
        self.setCurrentTile(model.tileAt(index))

    def updateActions(self):
        external = False
        hasImageSource = False
        hasSelection = False
        view = None
        index = self.mTabBar.currentIndex()
        if (index > -1):
            view = self.tilesetViewAt(index)
            if (view):
                tileset = self.mTilesets.at(index)
                if (not view.model()):  # Lazily set up the model
                    self.setupTilesetModel(view, tileset)

                self.mViewStack.setCurrentIndex(index)
                external = tileset.isExternal()
                hasImageSource = tileset.imageSource() != ''
                hasSelection = view.selectionModel().hasSelection()

        tilesetIsDisplayed = view != None
        mapIsDisplayed = self.mMapDocument != None
        self.mNewTileset.setEnabled(mapIsDisplayed)
        self.mImportTileset.setEnabled(tilesetIsDisplayed and external)
        self.mExportTileset.setEnabled(tilesetIsDisplayed and not external)
        self.mPropertiesTileset.setEnabled(tilesetIsDisplayed and not external)
        self.mDeleteTileset.setEnabled(tilesetIsDisplayed)
        self.mEditTerrain.setEnabled(tilesetIsDisplayed and not external)
        self.mAddTiles.setEnabled(tilesetIsDisplayed and not hasImageSource
                                  and not external)
        self.mRemoveTiles.setEnabled(tilesetIsDisplayed and not hasImageSource
                                     and hasSelection and not external)

    def updateCurrentTiles(self):
        view = self.currentTilesetView()
        if (not view):
            return
        s = view.selectionModel()
        if (not s):
            return
        indexes = s.selection().indexes()
        if len(indexes) == 0:
            return
        first = indexes[0]
        minX = first.column()
        maxX = first.column()
        minY = first.row()
        maxY = first.row()
        for index in indexes:
            if minX > index.column():
                minX = index.column()
            if maxX < index.column():
                maxX = index.column()
            if minY > index.row():
                minY = index.row()
            if maxY < index.row():
                maxY = index.row()

        # Create a tile layer from the current selection
        tileLayer = TileLayer(QString(), 0, 0, maxX - minX + 1,
                              maxY - minY + 1)
        model = view.tilesetModel()
        for index in indexes:
            tileLayer.setCell(index.column() - minX,
                              index.row() - minY, Cell(model.tileAt(index)))

        self.setCurrentTiles(tileLayer)

    def indexPressed(self, index):
        view = self.currentTilesetView()
        tile = view.tilesetModel().tileAt(index)
        if tile:
            self.mMapDocument.setCurrentObject(tile)

    def tilesetAdded(self, index, tileset):
        view = TilesetView()
        view.setMapDocument(self.mMapDocument)
        view.setZoomable(self.mZoomable)
        self.mTilesets.insert(index, tileset.sharedPointer())
        self.mTabBar.insertTab(index, tileset.name())
        self.mViewStack.insertWidget(index, view)
        self.updateActions()

    def tilesetChanged(self, tileset):
        # Update the affected tileset model, if it exists
        index = indexOf(self.mTilesets, tileset)
        if (index < 0):
            return
        model = self.tilesetViewAt(index).tilesetModel()
        if model:
            model.tilesetChanged()

    def tilesetRemoved(self, tileset):
        # Delete the related tileset view
        index = indexOf(self.mTilesets, tileset)
        self.mTilesets.removeAt(index)
        self.mTabBar.removeTab(index)
        self.tilesetViewAt(index).close()

        # Make sure we don't reference this tileset anymore
        if (self.mCurrentTiles):
            # TODO: Don't clean unnecessarily (but first the concept of
            #       "current brush" would need to be introduced)
            cleaned = self.mCurrentTiles.clone()
            cleaned.removeReferencesToTileset(tileset)
            self.setCurrentTiles(cleaned)

        if (self.mCurrentTile and self.mCurrentTile.tileset() == tileset):
            self.setCurrentTile(None)
        self.updateActions()

    def tilesetMoved(self, _from, to):
        self.mTilesets.insert(to, self.mTilesets.takeAt(_from))
        # Move the related tileset views
        widget = self.mViewStack.widget(_from)
        self.mViewStack.removeWidget(widget)
        self.mViewStack.insertWidget(to, widget)
        self.mViewStack.setCurrentIndex(self.mTabBar.currentIndex())
        # Update the titles of the affected tabs
        start = min(_from, to)
        end = max(_from, to)
        for i in range(start, end + 1):
            tileset = self.mTilesets.at(i)
            if (self.mTabBar.tabText(i) != tileset.name()):
                self.mTabBar.setTabText(i, tileset.name())

    def tilesetNameChanged(self, tileset):
        index = indexOf(self.mTilesets, tileset)
        self.mTabBar.setTabText(index, tileset.name())

    def tileAnimationChanged(self, tile):
        view = self.currentTilesetView()
        if view:
            model = view.tilesetModel()
            if model:
                model.tileChanged(tile)

    ##
    # Removes the currently selected tileset.
    ##
    def removeTileset(self, *args):
        l = len(args)
        if l == 0:
            currentIndex = self.mViewStack.currentIndex()
            if (currentIndex != -1):
                self.removeTileset(self.mViewStack.currentIndex())
        elif l == 1:
            ##
            # Removes the tileset at the given index. Prompting the user when the tileset
            # is in use by the map.
            ##
            index = args[0]
            tileset = self.mTilesets.at(index).data()
            inUse = self.mMapDocument.map().isTilesetUsed(tileset)
            # If the tileset is in use, warn the user and confirm removal
            if (inUse):
                warning = QMessageBox(
                    QMessageBox.Warning, self.tr("Remove Tileset"),
                    self.tr("The tileset \"%s\" is still in use by the map!" %
                            tileset.name()), QMessageBox.Yes | QMessageBox.No,
                    self)
                warning.setDefaultButton(QMessageBox.Yes)
                warning.setInformativeText(
                    self.tr("Remove this tileset and all references "
                            "to the tiles in this tileset?"))
                if (warning.exec() != QMessageBox.Yes):
                    return

            remove = RemoveTileset(self.mMapDocument, index, tileset)
            undoStack = self.mMapDocument.undoStack()
            if (inUse):
                # Remove references to tiles in this tileset from the current map
                def referencesTileset(cell):
                    tile = cell.tile
                    if tile:
                        return tile.tileset() == tileset
                    return False

                undoStack.beginMacro(remove.text())
                removeTileReferences(self.mMapDocument, referencesTileset)

            undoStack.push(remove)
            if (inUse):
                undoStack.endMacro()

    def moveTileset(self, _from, to):
        command = MoveTileset(self.mMapDocument, _from, to)
        self.mMapDocument.undoStack().push(command)

    def editTilesetProperties(self):
        tileset = self.currentTileset()
        if (not tileset):
            return
        self.mMapDocument.setCurrentObject(tileset)
        self.mMapDocument.emitEditCurrentObject()

    def importTileset(self):
        tileset = self.currentTileset()
        if (not tileset):
            return
        command = SetTilesetFileName(self.mMapDocument, tileset, QString())
        self.mMapDocument.undoStack().push(command)

    def exportTileset(self):
        tileset = self.currentTileset()
        if (not tileset):
            return

        tsxFilter = self.tr("Tiled tileset files (*.tsx)")
        helper = FormatHelper(FileFormat.ReadWrite, tsxFilter)

        prefs = preferences.Preferences.instance()

        suggestedFileName = prefs.lastPath(
            preferences.Preferences.ExternalTileset)
        suggestedFileName += '/'
        suggestedFileName += tileset.name()

        extension = ".tsx"

        if (not suggestedFileName.endswith(extension)):
            suggestedFileName += extension

        selectedFilter = tsxFilter
        fileName, _ = QFileDialog.getSaveFileName(self,
                                                  self.tr("Export Tileset"),
                                                  suggestedFileName,
                                                  helper.filter(),
                                                  selectedFilter)
        if fileName == '':
            return
        prefs.setLastPath(preferences.Preferences.ExternalTileset,
                          QFileInfo(fileName).path())

        tsxFormat = TsxTilesetFormat()
        format = helper.formatByNameFilter(selectedFilter)
        if not format:
            format = tsxFormat

        if format.write(tileset, fileName):
            command = SetTilesetFileName(self.mMapDocument, tileset, fileName)
            self.mMapDocument.undoStack().push(command)
        else:
            error = format.errorString()
            QMessageBox.critical(self.window(), self.tr("Export Tileset"),
                                 self.tr("Error saving tileset: %s" % error))

    def editTerrain(self):
        tileset = self.currentTileset()
        if (not tileset):
            return
        editTerrainDialog = EditTerrainDialog(self.mMapDocument, tileset, self)
        editTerrainDialog.exec()

    def addTiles(self):
        tileset = self.currentTileset()
        if (not tileset):
            return
        prefs = preferences.Preferences.instance()
        startLocation = QFileInfo(
            prefs.lastPath(preferences.Preferences.ImageFile)).absolutePath()
        filter = Utils.readableImageFormatsFilter()
        files = QFileDialog.getOpenFileNames(self.window(),
                                             self.tr("Add Tiles"),
                                             startLocation, filter)
        tiles = QList()
        id = tileset.tileCount()
        for file in files:
            image = QPixmap(file)
            if (not image.isNull()):
                tiles.append(Tile(image, file, id, tileset))
                id += 1
            else:
                warning = QMessageBox(QMessageBox.Warning,
                                      self.tr("Add Tiles"),
                                      self.tr("Could not load \"%s\"!" % file),
                                      QMessageBox.Ignore | QMessageBox.Cancel,
                                      self.window())
                warning.setDefaultButton(QMessageBox.Ignore)
                if (warning.exec() != QMessageBox.Ignore):
                    tiles.clear()
                    return

        if (tiles.isEmpty()):
            return
        prefs.setLastPath(preferences.Preferences.ImageFile, files.last())
        self.mMapDocument.undoStack().push(
            AddTiles(self.mMapDocument, tileset, tiles))

    def removeTiles(self):
        view = self.currentTilesetView()
        if (not view):
            return
        if (not view.selectionModel().hasSelection()):
            return
        indexes = view.selectionModel().selectedIndexes()
        model = view.tilesetModel()
        tileIds = RangeSet()
        tiles = QList()
        for index in indexes:
            tile = model.tileAt(index)
            if tile:
                tileIds.insert(tile.id())
                tiles.append(tile)

        def matchesAnyTile(cell):
            tile = cell.tile
            if tile:
                return tiles.contains(tile)
            return False

        inUse = self.hasTileReferences(self.mMapDocument, matchesAnyTile)
        # If the tileset is in use, warn the user and confirm removal
        if (inUse):
            warning = QMessageBox(
                QMessageBox.Warning, self.tr("Remove Tiles"),
                self.tr("One or more of the tiles to be removed are "
                        "still in use by the map!"),
                QMessageBox.Yes | QMessageBox.No, self)
            warning.setDefaultButton(QMessageBox.Yes)
            warning.setInformativeText(
                self.tr("Remove all references to these tiles?"))
            if (warning.exec() != QMessageBox.Yes):
                return

        undoStack = self.mMapDocument.undoStack()
        undoStack.beginMacro(self.tr("Remove Tiles"))
        removeTileReferences(self.mMapDocument, matchesAnyTile)
        # Iterate backwards over the ranges in order to keep the indexes valid
        firstRange = tileIds.begin()
        it = tileIds.end()
        if (it == firstRange):  # no range
            return
        tileset = view.tilesetModel().tileset()
        while (it != firstRange):
            it -= 1
            item = tileIds.item(it)
            length = item[1] - item[0] + 1
            undoStack.push(
                RemoveTiles(self.mMapDocument, tileset, item[0], length))

        undoStack.endMacro()
        # Clear the current tiles, will be referencing the removed tiles
        self.setCurrentTiles(None)
        self.setCurrentTile(None)

    def documentAboutToClose(self, mapDocument):
        self.mCurrentTilesets.remove(mapDocument)

    def refreshTilesetMenu(self):
        self.mTilesetMenu.clear()
        if (self.mTilesetMenuMapper):
            self.mTabBar.disconnect(self.mTilesetMenuMapper)
            del self.mTilesetMenuMapper

        self.mTilesetMenuMapper = QSignalMapper(self)
        self.mTilesetMenuMapper.mapped.connect(self.mTabBar.setCurrentIndex)
        currentIndex = self.mTabBar.currentIndex()
        for i in range(self.mTabBar.count()):
            action = QAction(self.mTabBar.tabText(i), self)
            action.setCheckable(True)
            self.mTilesetActionGroup.addAction(action)
            if (i == currentIndex):
                action.setChecked(True)
            self.mTilesetMenu.addAction(action)
            action.triggered.connect(self.mTilesetMenuMapper.map)
            self.mTilesetMenuMapper.setMapping(action, i)

    def setCurrentTile(self, tile):
        if (self.mCurrentTile == tile):
            return
        self.mCurrentTile = tile
        self.currentTileChanged.emit([tile])
        if (tile):
            self.mMapDocument.setCurrentObject(tile)

    def setCurrentTiles(self, tiles):
        if (self.mCurrentTiles == tiles):
            return
        del self.mCurrentTiles
        self.mCurrentTiles = tiles
        # Set the selected tiles on the map document
        if (tiles):
            selectedTiles = QList()
            for y in range(tiles.height()):
                for x in range(tiles.width()):
                    cell = tiles.cellAt(x, y)
                    if (not cell.isEmpty()):
                        selectedTiles.append(cell.tile)

            self.mMapDocument.setSelectedTiles(selectedTiles)

            # Create a tile stamp with these tiles
            map = self.mMapDocument.map()
            stamp = Map(map.orientation(), tiles.width(), tiles.height(),
                        map.tileWidth(), map.tileHeight())
            stamp.addLayer(tiles.clone())
            stamp.addTilesets(tiles.usedTilesets())

            self.mEmittingStampCaptured = True
            self.stampCaptured.emit(TileStamp(stamp))
            self.mEmittingStampCaptured = False

    def retranslateUi(self):
        self.setWindowTitle(self.tr("Tilesets"))
        self.mNewTileset.setText(self.tr("New Tileset"))
        self.mImportTileset.setText(self.tr("Import Tileset"))
        self.mExportTileset.setText(self.tr("Export Tileset As..."))
        self.mPropertiesTileset.setText(self.tr("Tileset Properties"))
        self.mDeleteTileset.setText(self.tr("Remove Tileset"))
        self.mEditTerrain.setText(self.tr("Edit Terrain Information"))
        self.mAddTiles.setText(self.tr("Add Tiles"))
        self.mRemoveTiles.setText(self.tr("Remove Tiles"))

    def currentTileset(self):
        index = self.mTabBar.currentIndex()
        if (index == -1):
            return None
        return self.mTilesets.at(index)

    def currentTilesetView(self):
        return self.mViewStack.currentWidget()

    def tilesetViewAt(self, index):
        return self.mViewStack.widget(index)

    def setupTilesetModel(self, view, tileset):
        view.setModel(TilesetModel(tileset, view))

        s = view.selectionModel()
        s.selectionChanged.connect(self.selectionChanged)
        s.currentChanged.connect(self.currentChanged)
        view.pressed.connect(self.indexPressed)
Exemple #56
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
Exemple #57
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)