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())
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 #3
0
class AutoMapper(QObject):
    ##
    # Constructs an AutoMapper.
    # All data structures, which only rely on the rules map are setup
    # here.
    #
    # @param workingDocument: the map to work on.
    # @param rules: The rule map which should be used for automapping
    # @param rulePath: The filepath to the rule map.
    ##
    def __init__(self, workingDocument, rules, rulePath):
        ##
        # where to work in
        ##
        self.mMapDocument = workingDocument

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    def __del__(self):
        self.cleanUpRulesMap()

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

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

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

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

        #where += region

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

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

        #where = where.united(ret)

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

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

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

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

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

        return True

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

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

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

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

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

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

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

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

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

                continue

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

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

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

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

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

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

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

                continue

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

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

        return True

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

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

        return True

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

        return True

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

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

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

        return True

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

        return result

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

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

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

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

                    if (allLayerNamesMatch):
                        anymatch = True
                        break

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

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

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

        return ret

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

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

        self.mAddedTilesets.clear()

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

        self.mAddedTileLayers.clear()
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())
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 #6
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 #7
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 #8
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 #9
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 #10
0
class AutoMapper(QObject):
    ##
    # Constructs an AutoMapper.
    # All data structures, which only rely on the rules map are setup
    # here.
    #
    # @param workingDocument: the map to work on.
    # @param rules: The rule map which should be used for automapping
    # @param rulePath: The filepath to the rule map.
    ##
    def __init__(self, workingDocument, rules, rulePath):
        ##
        # where to work in
        ##
        self.mMapDocument = workingDocument

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    def __del__(self):
        self.cleanUpRulesMap()

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

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

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

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

           #where += region

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

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

        #where = where.united(ret)

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

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

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

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

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

        return True

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

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

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

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

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

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

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

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

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

                continue

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

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

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

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

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

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

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

                continue

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

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

        return True

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

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

        return True

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

        return True

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

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

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

        return True

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

        return result

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

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

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

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

                    if (allLayerNamesMatch):
                        anymatch = True
                        break

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

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

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

        return ret

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

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

        self.mAddedTilesets.clear()

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

        self.mAddedTileLayers.clear()
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 #12
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
class EditPolygonTool(AbstractObjectTool):
    NoMode, Selecting, Moving = range(3)

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

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

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

    def __del__(self):
        del self.mSelectionRectangle

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

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

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

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

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

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

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

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

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

        self.mMousePressed = False
        self.mClickedHandle = None

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

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

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

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

                del self.mHandles[l]

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

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

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

            self.mHandles.insert(item, pointHandles)

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

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

        undoStack.endMacro()

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

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

        if (macroStarted):
            undoStack.endMacro()

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

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

        if (macroStarted):
            undoStack.endMacro()

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

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

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

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

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

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

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

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

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

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

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

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

            i += 1

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

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

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