class MapDocument(QObject): fileNameChanged = pyqtSignal(str, str) modifiedChanged = pyqtSignal() saved = pyqtSignal() ## # Emitted when the selected tile region changes. Sends the currently # selected region and the previously selected region. ## selectedAreaChanged = pyqtSignal(QRegion, QRegion) ## # Emitted when the list of selected objects changes. ## selectedObjectsChanged = pyqtSignal() ## # Emitted when the list of selected tiles from the dock changes. ## selectedTilesChanged = pyqtSignal() currentObjectChanged = pyqtSignal(list) ## # Emitted when the map size or its tile size changes. ## mapChanged = pyqtSignal() layerAdded = pyqtSignal(int) layerAboutToBeRemoved = pyqtSignal(int) layerRenamed = pyqtSignal(int) layerRemoved = pyqtSignal(int) layerChanged = pyqtSignal(int) ## # Emitted after a new layer was added and the name should be edited. # Applies to the current layer. ## editLayerNameRequested = pyqtSignal() editCurrentObject = pyqtSignal() ## # Emitted when the current layer index changes. ## currentLayerIndexChanged = pyqtSignal(int) ## # Emitted when a certain region of the map changes. The region is given in # tile coordinates. ## regionChanged = pyqtSignal(QRegion, Layer) ## # Emitted when a certain region of the map was edited by user input. # The region is given in tile coordinates. # If multiple layers have been edited, multiple signals will be emitted. ## regionEdited = pyqtSignal(QRegion, Layer) tileLayerDrawMarginsChanged = pyqtSignal(TileLayer) tileTerrainChanged = pyqtSignal(QList) tileProbabilityChanged = pyqtSignal(Tile) tileObjectGroupChanged = pyqtSignal(Tile) tileAnimationChanged = pyqtSignal(Tile) objectGroupChanged = pyqtSignal(ObjectGroup) imageLayerChanged = pyqtSignal(ImageLayer) tilesetAboutToBeAdded = pyqtSignal(int) tilesetAdded = pyqtSignal(int, Tileset) tilesetAboutToBeRemoved = pyqtSignal(int) tilesetRemoved = pyqtSignal(Tileset) tilesetMoved = pyqtSignal(int, int) tilesetFileNameChanged = pyqtSignal(Tileset) tilesetNameChanged = pyqtSignal(Tileset) tilesetTileOffsetChanged = pyqtSignal(Tileset) tilesetChanged = pyqtSignal(Tileset) objectsAdded = pyqtSignal(QList) objectsInserted = pyqtSignal(ObjectGroup, int, int) objectsRemoved = pyqtSignal(QList) objectsChanged = pyqtSignal(QList) objectsIndexChanged = pyqtSignal(ObjectGroup, int, int) propertyAdded = pyqtSignal(Object, str) propertyRemoved = pyqtSignal(Object, str) propertyChanged = pyqtSignal(Object, str) propertiesChanged = pyqtSignal(Object) ## # Constructs a map document around the given map. The map document takes # ownership of the map. ## def __init__(self, map, fileName = QString()): super().__init__() ## # The filename of a plugin is unique. So it can be used to determine # the right plugin to be used for saving or reloading the map. # The nameFilter of a plugin can not be used, since it's translatable. # The filename of a plugin must not change while maps are open using this # plugin. ## self.mReaderFormat = None self.mWriterFormat = None self.mExportFormat = None self.mSelectedArea = QRegion() self.mSelectedObjects = QList() self.mSelectedTiles = QList() self.mCurrentLayerIndex = 0 self.mLastSaved = QDateTime() self.mLastExportFileName = '' self.mFileName = fileName self.mMap = map self.mLayerModel = LayerModel(self) self.mCurrentObject = map ## Current properties object. ## self.mRenderer = None self.mMapObjectModel = MapObjectModel(self) self.mTerrainModel = TerrainModel(self, self) self.mUndoStack = QUndoStack(self) self.createRenderer() if (map.layerCount() == 0): _x = -1 else: _x = 0 self.mCurrentLayerIndex = _x self.mLayerModel.setMapDocument(self) # Forward signals emitted from the layer model self.mLayerModel.layerAdded.connect(self.onLayerAdded) self.mLayerModel.layerAboutToBeRemoved.connect(self.onLayerAboutToBeRemoved) self.mLayerModel.layerRemoved.connect(self.onLayerRemoved) self.mLayerModel.layerChanged.connect(self.layerChanged) # Forward signals emitted from the map object model self.mMapObjectModel.setMapDocument(self) self.mMapObjectModel.objectsAdded.connect(self.objectsAdded) self.mMapObjectModel.objectsChanged.connect(self.objectsChanged) self.mMapObjectModel.objectsRemoved.connect(self.onObjectsRemoved) self.mMapObjectModel.rowsInserted.connect(self.onMapObjectModelRowsInserted) self.mMapObjectModel.rowsRemoved.connect(self.onMapObjectModelRowsInsertedOrRemoved) self.mMapObjectModel.rowsMoved.connect(self.onObjectsMoved) self.mTerrainModel.terrainRemoved.connect(self.onTerrainRemoved) self.mUndoStack.cleanChanged.connect(self.modifiedChanged) # Register tileset references tilesetManager = TilesetManager.instance() tilesetManager.addReferences(self.mMap.tilesets()) ## # Destructor. ## def __del__(self): # Unregister tileset references tilesetManager = TilesetManager.instance() tilesetManager.removeReferences(self.mMap.tilesets()) del self.mRenderer del self.mMap ## # Saves the map to its current file name. Returns whether or not the file # was saved successfully. If not, <i>error</i> will be set to the error # message if it is not 0. ## def save(self, *args): l = len(args) if l==0: args = ('') if l==1: arg = args[0] file = QFileInfo(arg) if not file.isFile(): fileName = self.fileName() error = args[0] else: fileName = arg error = '' return self.save(fileName, error) if l==2: ## # Saves the map to the file at \a fileName. Returns whether or not the # file was saved successfully. If not, <i>error</i> will be set to the # error message if it is not 0. # # If the save was successful, the file name of this document will be set # to \a fileName. # # The map format will be the same as this map was opened with. ## fileName, error = args mapFormat = self.mWriterFormat tmxMapFormat = TmxMapFormat() if (not mapFormat): mapFormat = tmxMapFormat if (not mapFormat.write(self.map(), fileName)): if (error): error = mapFormat.errorString() return False self.undoStack().setClean() self.setFileName(fileName) self.mLastSaved = QFileInfo(fileName).lastModified() self.saved.emit() return True ## # Loads a map and returns a MapDocument instance on success. Returns 0 # on error and sets the \a error message. ## def load(fileName, mapFormat = None): error = '' tmxMapFormat = TmxMapFormat() if (not mapFormat and not tmxMapFormat.supportsFile(fileName)): # Try to find a plugin that implements support for this format formats = PluginManager.objects() for format in formats: if (format.supportsFile(fileName)): mapFormat = format break map = None errorString = '' if mapFormat: map = mapFormat.read(fileName) errorString = mapFormat.errorString() else: map = tmxMapFormat.read(fileName) errorString = tmxMapFormat.errorString() if (not map): error = errorString return None, error mapDocument = MapDocument(map, fileName) if mapFormat: mapDocument.setReaderFormat(mapFormat) if mapFormat.hasCapabilities(MapFormat.Write): mapDocument.setWriterFormat(mapFormat) return mapDocument, error def fileName(self): return self.mFileName def lastExportFileName(self): return self.mLastExportFileName def setLastExportFileName(self, fileName): self.mLastExportFileName = fileName def readerFormat(self): return self.mReaderFormat def setReaderFormat(self, format): self.mReaderFormat = format def writerFormat(self): return self.mWriterFormat def setWriterFormat(self, format): self.mWriterFormat = format def exportFormat(self): return self.mExportFormat def setExportFormat(self, format): self.mExportFormat = format ## # Returns the name with which to display this map. It is the file name without # its path, or 'untitled.tmx' when the map has no file name. ## def displayName(self): displayName = QFileInfo(self.mFileName).fileName() if len(displayName)==0: displayName = self.tr("untitled.tmx") return displayName ## # Returns whether the map has unsaved changes. ## def isModified(self): return not self.mUndoStack.isClean() def lastSaved(self): return self.mLastSaved ## # Returns the map instance. Be aware that directly modifying the map will # not allow the GUI to update itself appropriately. ## def map(self): return self.mMap ## # Sets the current layer to the given index. ## def setCurrentLayerIndex(self, index): changed = self.mCurrentLayerIndex != index self.mCurrentLayerIndex = index ## This function always sends the following signal, even if the index # didn't actually change. This is because the selected index in the layer # table view might be out of date anyway, and would otherwise not be # properly updated. # # This problem happens due to the selection model not sending signals # about changes to its current index when it is due to insertion/removal # of other items. The selected item doesn't change in that case, but our # layer index does. ## self.currentLayerIndexChanged.emit(self.mCurrentLayerIndex) if (changed and self.mCurrentLayerIndex != -1): self.setCurrentObject(self.currentLayer()) ## # Returns the index of the currently selected layer. Returns -1 if no # layer is currently selected. ## def currentLayerIndex(self): return self.mCurrentLayerIndex ## # Returns the currently selected layer, or 0 if no layer is currently # selected. ## def currentLayer(self): if (self.mCurrentLayerIndex == -1): return None return self.mMap.layerAt(self.mCurrentLayerIndex) ## # Resize this map to the given \a size, while at the same time shifting # the contents by \a offset. ## def resizeMap(self, size, offset): movedSelection = self.mSelectedArea.translated(offset) newArea = QRect(-offset, size) visibleArea = self.mRenderer.boundingRect(newArea) origin = self.mRenderer.tileToPixelCoords_(QPointF()) newOrigin = self.mRenderer.tileToPixelCoords_(-offset) pixelOffset = origin - newOrigin # Resize the map and each layer self.mUndoStack.beginMacro(self.tr("Resize Map")) for i in range(self.mMap.layerCount()): layer = self.mMap.layerAt(i) x = layer.layerType() if x==Layer.TileLayerType: tileLayer = layer self.mUndoStack.push(ResizeTileLayer(self, tileLayer, size, offset)) elif x==Layer.ObjectGroupType: objectGroup = layer # Remove objects that will fall outside of the map for o in objectGroup.objects(): if (not visibleIn(visibleArea, o, self.mRenderer)): self.mUndoStack.push(RemoveMapObject(self, o)) else: oldPos = o.position() newPos = oldPos + pixelOffset self.mUndoStack.push(MoveMapObject(self, newPos, oldPos)) elif x==Layer.ImageLayerType: # Currently not adjusted when resizing the map break self.mUndoStack.push(ResizeMap(self, size)) self.mUndoStack.push(ChangeSelectedArea(self, movedSelection)) self.mUndoStack.endMacro() # TODO: Handle layers that don't match the map size correctly ## # Offsets the layers at \a layerIndexes by \a offset, within \a bounds, # and optionally wraps on the X or Y axis. ## def offsetMap(self, layerIndexes, offset, bounds, wrapX, wrapY): if (layerIndexes.empty()): return if (layerIndexes.size() == 1): self.mUndoStack.push(OffsetLayer(self, layerIndexes.first(), offset, bounds, wrapX, wrapY)) else: self.mUndoStack.beginMacro(self.tr("Offset Map")) for layerIndex in layerIndexes: self.mUndoStack.push(OffsetLayer(self, layerIndex, offset, bounds, wrapX, wrapY)) self.mUndoStack.endMacro() ## # Flips the selected objects in the given \a direction. ## def flipSelectedObjects(self, direction): if (self.mSelectedObjects.isEmpty()): return self.mUndoStack.push(FlipMapObjects(self, self.mSelectedObjects, direction)) ## # Rotates the selected objects. ## def rotateSelectedObjects(self, direction): if (self.mSelectedObjects.isEmpty()): return self.mUndoStack.beginMacro(self.tr("Rotate %n Object(s)", "", self.mSelectedObjects.size())) # TODO: Rotate them properly as a group for mapObject in self.mSelectedObjects: oldRotation = mapObject.rotation() newRotation = oldRotation if (direction == RotateDirection.RotateLeft): newRotation -= 90 if (newRotation < -180): newRotation += 360 else: newRotation += 90 if (newRotation > 180): newRotation -= 360 self.mUndoStack.push(RotateMapObject(self, mapObject, newRotation, oldRotation)) self.mUndoStack.endMacro() ## # Adds a layer of the given type to the top of the layer stack. After adding # the new layer, emits editLayerNameRequested(). ## def addLayer(self, layerType): layer = None name = QString() x = layerType if x==Layer.TileLayerType: name = self.tr("Tile Layer %d"%(self.mMap.tileLayerCount() + 1)) layer = TileLayer(name, 0, 0, self.mMap.width(), self.mMap.height()) elif x==Layer.ObjectGroupType: name = self.tr("Object Layer %d"%(self.mMap.objectGroupCount() + 1)) layer = ObjectGroup(name, 0, 0, self.mMap.width(), self.mMap.height()) elif x==Layer.ImageLayerType: name = self.tr("Image Layer %d"%(self.mMap.imageLayerCount() + 1)) layer = ImageLayer(name, 0, 0, self.mMap.width(), self.mMap.height()) index = self.mMap.layerCount() self.mUndoStack.push(AddLayer(self, index, layer)) self.setCurrentLayerIndex(index) self.editLayerNameRequested.emit() ## # Duplicates the currently selected layer. ## def duplicateLayer(self): if (self.mCurrentLayerIndex == -1): return duplicate = self.mMap.layerAt(self.mCurrentLayerIndex).clone() duplicate.setName(self.tr("Copy of %s"%duplicate.name())) index = self.mCurrentLayerIndex + 1 cmd = AddLayer(self, index, duplicate) cmd.setText(self.tr("Duplicate Layer")) self.mUndoStack.push(cmd) self.setCurrentLayerIndex(index) ## # Merges the currently selected layer with the layer below. This only works # when the layers can be merged. # # \see Layer.canMergeWith ## def mergeLayerDown(self): if (self.mCurrentLayerIndex < 1): return upperLayer = self.mMap.layerAt(self.mCurrentLayerIndex) lowerLayer = self.mMap.layerAt(self.mCurrentLayerIndex - 1) if (not lowerLayer.canMergeWith(upperLayer)): return merged = lowerLayer.mergedWith(upperLayer) self.mUndoStack.beginMacro(self.tr("Merge Layer Down")) self.mUndoStack.push(AddLayer(self, self.mCurrentLayerIndex - 1, merged)) self.mUndoStack.push(RemoveLayer(self, self.mCurrentLayerIndex)) self.mUndoStack.push(RemoveLayer(self, self.mCurrentLayerIndex)) self.mUndoStack.endMacro() ## # Moves the given layer up. Does nothing when no valid layer index is # given. ## def moveLayerUp(self, index): if index<0 or index>=self.mMap.layerCount() - 1: return self.mUndoStack.push(MoveLayer(self, index, MoveLayer.Up)) ## # Moves the given layer down. Does nothing when no valid layer index is # given. ## def moveLayerDown(self, index): if index<1 or index>=self.mMap.layerCount(): return self.mUndoStack.push(MoveLayer(self, index, MoveLayer.Down)) ## # Removes the given layer. ## def removeLayer(self, index): if index<0 or index>=self.mMap.layerCount(): return self.mUndoStack.push(RemoveLayer(self, index)) ## # Show or hide all other layers except the layer at the given index. # If any other layer is visible then all layers will be hidden, otherwise # the layers will be shown. ## def toggleOtherLayers(self, index): self.mLayerModel.toggleOtherLayers(index) ## # Adds a tileset to this map at the given \a index. Emits the appropriate # signal. ## def insertTileset(self, index, tileset): self.tilesetAboutToBeAdded.emit(index) self.mMap.insertTileset(index, tileset) tilesetManager = TilesetManager.instance() tilesetManager.addReference(tileset) self.tilesetAdded.emit(index, tileset) ## # Removes the tileset at the given \a index from this map. Emits the # appropriate signal. # # \warning Does not make sure that any references to tiles in the removed # tileset are cleared. ## def removeTilesetAt(self, index): self.tilesetAboutToBeRemoved.emit(index) tileset = self.mMap.tilesets().at(index) if (tileset == self.mCurrentObject or isFromTileset(self.mCurrentObject, tileset)): self.setCurrentObject(None) self.mMap.removeTilesetAt(index) self.tilesetRemoved.emit(tileset) tilesetManager = TilesetManager.instance() tilesetManager.removeReference(tileset) def moveTileset(self, _from, to): if (_from == to): return tileset = self.mMap.tilesets().at(_from) self.mMap.removeTilesetAt(_from) self.mMap.insertTileset(to, tileset) self.tilesetMoved.emit(_from, to) def setTilesetFileName(self, tileset, fileName): tileset.setFileName(fileName) self.tilesetFileNameChanged.emit(tileset) def setTilesetName(self, tileset, name): tileset.setName(name) self.tilesetNameChanged.emit(tileset) def setTilesetTileOffset(self, tileset, tileOffset): tileset.setTileOffset(tileOffset) self.mMap.recomputeDrawMargins() self.tilesetTileOffsetChanged.emit(tileset) def duplicateObjects(self, objects): if (objects.isEmpty()): return self.mUndoStack.beginMacro(self.tr("Duplicate %n Object(s)", "", objects.size())) clones = QList() for mapObject in objects: clone = mapObject.clone() clones.append(clone) self.mUndoStack.push(AddMapObject(self, mapObject.objectGroup(), clone)) self.mUndoStack.endMacro() self.setSelectedObjects(clones) def removeObjects(self, objects): if (objects.isEmpty()): return self.mUndoStack.beginMacro(self.tr("Remove %n Object(s)", "", objects.size())) for mapObject in objects: self.mUndoStack.push(RemoveMapObject(self, mapObject)) self.mUndoStack.endMacro() def moveObjectsToGroup(self, objects, objectGroup): if (objects.isEmpty()): return self.mUndoStack.beginMacro(self.tr("Move %n Object(s) to Layer", "", objects.size())) for mapObject in objects: if (mapObject.objectGroup() == objectGroup): continue self.mUndoStack.push(MoveMapObjectToGroup(self, mapObject, objectGroup)) self.mUndoStack.endMacro() def setProperty(self, object, name, value): hadProperty = object.hasProperty(name) object.setProperty(name, value) if (hadProperty): self.propertyChanged.emit(object, name) else: self.propertyAdded.emit(object, name) def setProperties(self, object, properties): object.setProperties(properties) self.propertiesChanged.emit(object) def removeProperty(self, object, name): object.removeProperty(name) self.propertyRemoved.emit(object, name) ## # Returns the layer model. Can be used to modify the layer stack of the # map, and to display the layer stack in a view. ## def layerModel(self): return self.mLayerModel def mapObjectModel(self): return self.mMapObjectModel def terrainModel(self): return self.mTerrainModel ## # Returns the map renderer. ## def renderer(self): return self.mRenderer ## # Creates the map renderer. Should be called after changing the map # orientation. ## def createRenderer(self): if (self.mRenderer): del self.mRenderer x = self.mMap.orientation() if x==Map.Orientation.Isometric: self.mRenderer = IsometricRenderer(self.mMap) elif x==Map.Orientation.Staggered: self.mRenderer = StaggeredRenderer(self.mMap) elif x==Map.Orientation.Hexagonal: self.mRenderer = HexagonalRenderer(self.mMap) else: self.mRenderer = OrthogonalRenderer(self.mMap) ## # Returns the undo stack of this map document. Should be used to push any # commands on that modify the map. ## def undoStack(self): return self.mUndoStack ## # Returns the selected area of tiles. ## def selectedArea(self): return QRegion(self.mSelectedArea) ## # Sets the selected area of tiles. ## def setSelectedArea(self, selection): if (self.mSelectedArea != selection): oldSelectedArea = self.mSelectedArea self.mSelectedArea = selection self.selectedAreaChanged.emit(self.mSelectedArea, oldSelectedArea) ## # Returns the list of selected objects. ## def selectedObjects(self): return self.mSelectedObjects ## # Sets the list of selected objects, emitting the selectedObjectsChanged # signal. ## def setSelectedObjects(self, selectedObjects): if selectedObjects.nequal(self.mSelectedObjects): self.mSelectedObjects = selectedObjects self.selectedObjectsChanged.emit() if (selectedObjects.size() == 1): self.setCurrentObject(selectedObjects.first()) ## # Returns the list of selected tiles. ## def selectedTiles(self): return self.mSelectedTiles def setSelectedTiles(self, selectedTiles): self.mSelectedTiles = selectedTiles self.selectedTilesChanged.emit() def currentObject(self): return self.mCurrentObject def setCurrentObject(self, object): if (object == self.mCurrentObject): return self.mCurrentObject = object self.currentObjectChanged.emit([object]) def currentObjects(self): objects = QList() if (self.mCurrentObject): if (self.mCurrentObject.typeId() == Object.MapObjectType and not self.mSelectedObjects.isEmpty()): for mapObj in self.mSelectedObjects: objects.append(mapObj) elif (self.mCurrentObject.typeId() == Object.TileType and not self.mSelectedTiles.isEmpty()): for tile in self.mSelectedTiles: objects.append(tile) else: objects.append(self.mCurrentObject) return objects def unifyTilesets(self, *args): l = len(args) if l==1: ## # Makes sure the all tilesets which are used at the given \a map will be # present in the map document. # # To reach the aim, all similar tilesets will be replaced by the version # in the current map document and all missing tilesets will be added to # the current map document. # # \warning This method assumes that the tilesets in \a map are managed by # the TilesetManager! ## map = args[0] undoCommands = QList() existingTilesets = self.mMap.tilesets() tilesetManager = TilesetManager.instance() # Add tilesets that are not yet part of this map for tileset in map.tilesets(): if (existingTilesets.contains(tileset)): continue replacement = tileset.findSimilarTileset(existingTilesets) if (not replacement): undoCommands.append(AddTileset(self, tileset)) continue # Merge the tile properties sharedTileCount = min(tileset.tileCount(), replacement.tileCount()) for i in range(sharedTileCount): replacementTile = replacement.tileAt(i) properties = replacementTile.properties() properties.merge(tileset.tileAt(i).properties()) undoCommands.append(ChangeProperties(self, self.tr("Tile"), replacementTile, properties)) map.replaceTileset(tileset, replacement) tilesetManager.addReference(replacement) tilesetManager.removeReference(tileset) if (not undoCommands.isEmpty()): self.mUndoStack.beginMacro(self.tr("Tileset Changes")) for command in undoCommands: self.mUndoStack.push(command) self.mUndoStack.endMacro() elif l==2: map, missingTilesets = args existingTilesets = self.mMap.tilesets() tilesetManager = TilesetManager.instance() for tileset in map.tilesets(): # tileset already added if existingTilesets.contains(tileset): continue replacement = tileset.findSimilarTileset(existingTilesets) # tileset not present and no replacement tileset found if not replacement: if not missingTilesets.contains(tileset): missingTilesets.append(tileset) continue # replacement tileset found, change given map map.replaceTileset(tileset, replacement) tilesetManager.addReference(replacement) tilesetManager.removeReference(tileset) ## # Emits the map changed signal. This signal should be emitted after changing # the map size or its tile size. ## def emitMapChanged(self): self.mapChanged.emit() ## # Emits the region changed signal for the specified region. The region # should be in tile coordinates. This method is used by the TilePainter. ## def emitRegionChanged(self, region, layer): self.regionChanged.emit(region, layer) ## # Emits the region edited signal for the specified region and tile layer. # The region should be in tile coordinates. This should be called from # all map document changing classes which are triggered by user input. ## def emitRegionEdited(self, region, layer): self.regionEdited.emit(region, layer) def emitTileLayerDrawMarginsChanged(self, layer): self.tileLayerDrawMarginsChanged.emit(layer) ## # Emits the tileset changed signal. This signal is currently used when adding # or removing tiles from a tileset. # # @todo Emit more specific signals. ## def emitTilesetChanged(self, tileset): self.tilesetChanged.emit(tileset) ## # Emits the signal notifying about the terrain probability of a tile changing. ## def emitTileProbabilityChanged(self, tile): self.tileProbabilityChanged.emit(tile) ## # Emits the signal notifying tileset models about changes to tile terrain # information. All the \a tiles need to be from the same tileset. ## def emitTileTerrainChanged(self, tiles): if (not tiles.isEmpty()): self.tileTerrainChanged.emit(tiles) ## # Emits the signal notifying the TileCollisionEditor about the object group # of a tile changing. ## def emitTileObjectGroupChanged(self, tile): self.tileObjectGroupChanged.emit(tile) ## # Emits the signal notifying about the animation of a tile changing. ## def emitTileAnimationChanged(self, tile): self.tileAnimationChanged.emit(tile) ## # Emits the objectGroupChanged signal, should be called when changing the # color or drawing order of an object group. ## def emitObjectGroupChanged(self, objectGroup): self.objectGroupChanged.emit(objectGroup) ## # Emits the imageLayerChanged signal, should be called when changing the # image or the transparent color of an image layer. ## def emitImageLayerChanged(self, imageLayer): self.imageLayerChanged.emit(imageLayer) ## # Emits the editLayerNameRequested signal, to get renamed. ## def emitEditLayerNameRequested(self): self.editLayerNameRequested.emit() ## # Emits the editCurrentObject signal, which makes the Properties window become # visible and take focus. ## def emitEditCurrentObject(self): self.editCurrentObject.emit() ## # Before forwarding the signal, the objects are removed from the list of # selected objects, triggering a selectedObjectsChanged signal when # appropriate. ## def onObjectsRemoved(self, objects): self.deselectObjects(objects) self.objectsRemoved.emit(objects) def onMapObjectModelRowsInserted(self, parent, first, last): objectGroup = self.mMapObjectModel.toObjectGroup(parent) if (not objectGroup): # we're not dealing with insertion of objects return self.objectsInserted.emit(objectGroup, first, last) self.onMapObjectModelRowsInsertedOrRemoved(parent, first, last) def onMapObjectModelRowsInsertedOrRemoved(self, parent, first, last): objectGroup = self.mMapObjectModel.toObjectGroup(parent) if (not objectGroup): return # Inserting or removing objects changes the index of any that come after lastIndex = objectGroup.objectCount() - 1 if (last < lastIndex): self.objectsIndexChanged.emit(objectGroup, last + 1, lastIndex) def onObjectsMoved(self, parent, start, end, destination, row): if (parent != destination): return objectGroup = self.mMapObjectModel.toObjectGroup(parent) # Determine the full range over which object indexes changed first = min(start, row) last = max(end, row - 1) self.objectsIndexChanged.emit(objectGroup, first, last) def onLayerAdded(self, index): self.layerAdded.emit(index) # Select the first layer that gets added to the map if (self.mMap.layerCount() == 1): self.setCurrentLayerIndex(0) def onLayerAboutToBeRemoved(self, index): layer = self.mMap.layerAt(index) if (layer == self.mCurrentObject): self.setCurrentObject(None) # Deselect any objects on this layer when necessary og = layer if type(og) == ObjectGroup: self.deselectObjects(og.objects()) self.layerAboutToBeRemoved.emit(index) def onLayerRemoved(self, index): # Bring the current layer index to safety currentLayerRemoved = self.mCurrentLayerIndex == self.mMap.layerCount() if (currentLayerRemoved): self.mCurrentLayerIndex = self.mCurrentLayerIndex - 1 self.layerRemoved.emit(index) # Emitted after the layerRemoved signal so that the MapScene has a chance # of synchronizing before adapting to the newly selected index if (currentLayerRemoved): self.currentLayerIndexChanged.emit(self.mCurrentLayerIndex) def onTerrainRemoved(self, terrain): if (terrain == self.mCurrentObject): self.setCurrentObject(None) def setFileName(self, fileName): if (self.mFileName == fileName): return oldFileName = self.mFileName self.mFileName = fileName self.fileNameChanged.emit(fileName, oldFileName) def deselectObjects(self, objects): # Unset the current object when it was part of this list of objects if (self.mCurrentObject and self.mCurrentObject.typeId() == Object.MapObjectType): if (objects.contains(self.mCurrentObject)): self.setCurrentObject(None) removedCount = 0 for object in objects: removedCount += self.mSelectedObjects.removeAll(object) if (removedCount > 0): self.selectedObjectsChanged.emit() def disconnect(self): try: super().disconnect() except: pass
class MapDocument(QObject): fileNameChanged = pyqtSignal(str, str) modifiedChanged = pyqtSignal() saved = pyqtSignal() ## # Emitted when the selected tile region changes. Sends the currently # selected region and the previously selected region. ## selectedAreaChanged = pyqtSignal(QRegion, QRegion) ## # Emitted when the list of selected objects changes. ## selectedObjectsChanged = pyqtSignal() ## # Emitted when the list of selected tiles from the dock changes. ## selectedTilesChanged = pyqtSignal() currentObjectChanged = pyqtSignal(list) ## # Emitted when the map size or its tile size changes. ## mapChanged = pyqtSignal() layerAdded = pyqtSignal(int) layerAboutToBeRemoved = pyqtSignal(int) layerRenamed = pyqtSignal(int) layerRemoved = pyqtSignal(int) layerChanged = pyqtSignal(int) ## # Emitted after a new layer was added and the name should be edited. # Applies to the current layer. ## editLayerNameRequested = pyqtSignal() editCurrentObject = pyqtSignal() ## # Emitted when the current layer index changes. ## currentLayerIndexChanged = pyqtSignal(int) ## # Emitted when a certain region of the map changes. The region is given in # tile coordinates. ## regionChanged = pyqtSignal(QRegion, Layer) ## # Emitted when a certain region of the map was edited by user input. # The region is given in tile coordinates. # If multiple layers have been edited, multiple signals will be emitted. ## regionEdited = pyqtSignal(QRegion, Layer) tileLayerDrawMarginsChanged = pyqtSignal(TileLayer) tileTerrainChanged = pyqtSignal(QList) tileProbabilityChanged = pyqtSignal(Tile) tileObjectGroupChanged = pyqtSignal(Tile) tileAnimationChanged = pyqtSignal(Tile) objectGroupChanged = pyqtSignal(ObjectGroup) imageLayerChanged = pyqtSignal(ImageLayer) tilesetAboutToBeAdded = pyqtSignal(int) tilesetAdded = pyqtSignal(int, Tileset) tilesetAboutToBeRemoved = pyqtSignal(int) tilesetRemoved = pyqtSignal(Tileset) tilesetMoved = pyqtSignal(int, int) tilesetFileNameChanged = pyqtSignal(Tileset) tilesetNameChanged = pyqtSignal(Tileset) tilesetTileOffsetChanged = pyqtSignal(Tileset) tilesetChanged = pyqtSignal(Tileset) objectsAdded = pyqtSignal(QList) objectsInserted = pyqtSignal(ObjectGroup, int, int) objectsRemoved = pyqtSignal(QList) objectsChanged = pyqtSignal(QList) objectsIndexChanged = pyqtSignal(ObjectGroup, int, int) propertyAdded = pyqtSignal(Object, str) propertyRemoved = pyqtSignal(Object, str) propertyChanged = pyqtSignal(Object, str) propertiesChanged = pyqtSignal(Object) ## # Constructs a map document around the given map. The map document takes # ownership of the map. ## def __init__(self, map, fileName=QString()): super().__init__() ## # The filename of a plugin is unique. So it can be used to determine # the right plugin to be used for saving or reloading the map. # The nameFilter of a plugin can not be used, since it's translatable. # The filename of a plugin must not change while maps are open using this # plugin. ## self.mReaderFormat = None self.mWriterFormat = None self.mExportFormat = None self.mSelectedArea = QRegion() self.mSelectedObjects = QList() self.mSelectedTiles = QList() self.mCurrentLayerIndex = 0 self.mLastSaved = QDateTime() self.mLastExportFileName = '' self.mFileName = fileName self.mMap = map self.mLayerModel = LayerModel(self) self.mCurrentObject = map ## Current properties object. ## self.mRenderer = None self.mMapObjectModel = MapObjectModel(self) self.mTerrainModel = TerrainModel(self, self) self.mUndoStack = QUndoStack(self) self.createRenderer() if (map.layerCount() == 0): _x = -1 else: _x = 0 self.mCurrentLayerIndex = _x self.mLayerModel.setMapDocument(self) # Forward signals emitted from the layer model self.mLayerModel.layerAdded.connect(self.onLayerAdded) self.mLayerModel.layerAboutToBeRemoved.connect( self.onLayerAboutToBeRemoved) self.mLayerModel.layerRemoved.connect(self.onLayerRemoved) self.mLayerModel.layerChanged.connect(self.layerChanged) # Forward signals emitted from the map object model self.mMapObjectModel.setMapDocument(self) self.mMapObjectModel.objectsAdded.connect(self.objectsAdded) self.mMapObjectModel.objectsChanged.connect(self.objectsChanged) self.mMapObjectModel.objectsRemoved.connect(self.onObjectsRemoved) self.mMapObjectModel.rowsInserted.connect( self.onMapObjectModelRowsInserted) self.mMapObjectModel.rowsRemoved.connect( self.onMapObjectModelRowsInsertedOrRemoved) self.mMapObjectModel.rowsMoved.connect(self.onObjectsMoved) self.mTerrainModel.terrainRemoved.connect(self.onTerrainRemoved) self.mUndoStack.cleanChanged.connect(self.modifiedChanged) # Register tileset references tilesetManager = TilesetManager.instance() tilesetManager.addReferences(self.mMap.tilesets()) ## # Destructor. ## def __del__(self): # Unregister tileset references tilesetManager = TilesetManager.instance() tilesetManager.removeReferences(self.mMap.tilesets()) del self.mRenderer del self.mMap ## # Saves the map to its current file name. Returns whether or not the file # was saved successfully. If not, <i>error</i> will be set to the error # message if it is not 0. ## def save(self, *args): l = len(args) if l == 0: args = ('') if l == 1: arg = args[0] file = QFileInfo(arg) if not file.isFile(): fileName = self.fileName() error = args[0] else: fileName = arg error = '' return self.save(fileName, error) if l == 2: ## # Saves the map to the file at \a fileName. Returns whether or not the # file was saved successfully. If not, <i>error</i> will be set to the # error message if it is not 0. # # If the save was successful, the file name of this document will be set # to \a fileName. # # The map format will be the same as this map was opened with. ## fileName, error = args mapFormat = self.mWriterFormat tmxMapFormat = TmxMapFormat() if (not mapFormat): mapFormat = tmxMapFormat if (not mapFormat.write(self.map(), fileName)): if (error): error = mapFormat.errorString() return False self.undoStack().setClean() self.setFileName(fileName) self.mLastSaved = QFileInfo(fileName).lastModified() self.saved.emit() return True ## # Loads a map and returns a MapDocument instance on success. Returns 0 # on error and sets the \a error message. ## def load(fileName, mapFormat=None): error = '' tmxMapFormat = TmxMapFormat() if (not mapFormat and not tmxMapFormat.supportsFile(fileName)): # Try to find a plugin that implements support for this format formats = PluginManager.objects() for format in formats: if (format.supportsFile(fileName)): mapFormat = format break map = None errorString = '' if mapFormat: map = mapFormat.read(fileName) errorString = mapFormat.errorString() else: map = tmxMapFormat.read(fileName) errorString = tmxMapFormat.errorString() if (not map): error = errorString return None, error mapDocument = MapDocument(map, fileName) if mapFormat: mapDocument.setReaderFormat(mapFormat) if mapFormat.hasCapabilities(MapFormat.Write): mapDocument.setWriterFormat(mapFormat) return mapDocument, error def fileName(self): return self.mFileName def lastExportFileName(self): return self.mLastExportFileName def setLastExportFileName(self, fileName): self.mLastExportFileName = fileName def readerFormat(self): return self.mReaderFormat def setReaderFormat(self, format): self.mReaderFormat = format def writerFormat(self): return self.mWriterFormat def setWriterFormat(self, format): self.mWriterFormat = format def exportFormat(self): return self.mExportFormat def setExportFormat(self, format): self.mExportFormat = format ## # Returns the name with which to display this map. It is the file name without # its path, or 'untitled.tmx' when the map has no file name. ## def displayName(self): displayName = QFileInfo(self.mFileName).fileName() if len(displayName) == 0: displayName = self.tr("untitled.tmx") return displayName ## # Returns whether the map has unsaved changes. ## def isModified(self): return not self.mUndoStack.isClean() def lastSaved(self): return self.mLastSaved ## # Returns the map instance. Be aware that directly modifying the map will # not allow the GUI to update itself appropriately. ## def map(self): return self.mMap ## # Sets the current layer to the given index. ## def setCurrentLayerIndex(self, index): changed = self.mCurrentLayerIndex != index self.mCurrentLayerIndex = index ## This function always sends the following signal, even if the index # didn't actually change. This is because the selected index in the layer # table view might be out of date anyway, and would otherwise not be # properly updated. # # This problem happens due to the selection model not sending signals # about changes to its current index when it is due to insertion/removal # of other items. The selected item doesn't change in that case, but our # layer index does. ## self.currentLayerIndexChanged.emit(self.mCurrentLayerIndex) if (changed and self.mCurrentLayerIndex != -1): self.setCurrentObject(self.currentLayer()) ## # Returns the index of the currently selected layer. Returns -1 if no # layer is currently selected. ## def currentLayerIndex(self): return self.mCurrentLayerIndex ## # Returns the currently selected layer, or 0 if no layer is currently # selected. ## def currentLayer(self): if (self.mCurrentLayerIndex == -1): return None return self.mMap.layerAt(self.mCurrentLayerIndex) ## # Resize this map to the given \a size, while at the same time shifting # the contents by \a offset. ## def resizeMap(self, size, offset): movedSelection = self.mSelectedArea.translated(offset) newArea = QRect(-offset, size) visibleArea = self.mRenderer.boundingRect(newArea) origin = self.mRenderer.tileToPixelCoords_(QPointF()) newOrigin = self.mRenderer.tileToPixelCoords_(-offset) pixelOffset = origin - newOrigin # Resize the map and each layer self.mUndoStack.beginMacro(self.tr("Resize Map")) for i in range(self.mMap.layerCount()): layer = self.mMap.layerAt(i) x = layer.layerType() if x == Layer.TileLayerType: tileLayer = layer self.mUndoStack.push( ResizeTileLayer(self, tileLayer, size, offset)) elif x == Layer.ObjectGroupType: objectGroup = layer # Remove objects that will fall outside of the map for o in objectGroup.objects(): if (not visibleIn(visibleArea, o, self.mRenderer)): self.mUndoStack.push(RemoveMapObject(self, o)) else: oldPos = o.position() newPos = oldPos + pixelOffset self.mUndoStack.push( MoveMapObject(self, newPos, oldPos)) elif x == Layer.ImageLayerType: # Currently not adjusted when resizing the map break self.mUndoStack.push(ResizeMap(self, size)) self.mUndoStack.push(ChangeSelectedArea(self, movedSelection)) self.mUndoStack.endMacro() # TODO: Handle layers that don't match the map size correctly ## # Offsets the layers at \a layerIndexes by \a offset, within \a bounds, # and optionally wraps on the X or Y axis. ## def offsetMap(self, layerIndexes, offset, bounds, wrapX, wrapY): if (layerIndexes.empty()): return if (layerIndexes.size() == 1): self.mUndoStack.push( OffsetLayer(self, layerIndexes.first(), offset, bounds, wrapX, wrapY)) else: self.mUndoStack.beginMacro(self.tr("Offset Map")) for layerIndex in layerIndexes: self.mUndoStack.push( OffsetLayer(self, layerIndex, offset, bounds, wrapX, wrapY)) self.mUndoStack.endMacro() ## # Flips the selected objects in the given \a direction. ## def flipSelectedObjects(self, direction): if (self.mSelectedObjects.isEmpty()): return self.mUndoStack.push( FlipMapObjects(self, self.mSelectedObjects, direction)) ## # Rotates the selected objects. ## def rotateSelectedObjects(self, direction): if (self.mSelectedObjects.isEmpty()): return self.mUndoStack.beginMacro( self.tr("Rotate %n Object(s)", "", self.mSelectedObjects.size())) # TODO: Rotate them properly as a group for mapObject in self.mSelectedObjects: oldRotation = mapObject.rotation() newRotation = oldRotation if (direction == RotateDirection.RotateLeft): newRotation -= 90 if (newRotation < -180): newRotation += 360 else: newRotation += 90 if (newRotation > 180): newRotation -= 360 self.mUndoStack.push( RotateMapObject(self, mapObject, newRotation, oldRotation)) self.mUndoStack.endMacro() ## # Adds a layer of the given type to the top of the layer stack. After adding # the new layer, emits editLayerNameRequested(). ## def addLayer(self, layerType): layer = None name = QString() x = layerType if x == Layer.TileLayerType: name = self.tr("Tile Layer %d" % (self.mMap.tileLayerCount() + 1)) layer = TileLayer(name, 0, 0, self.mMap.width(), self.mMap.height()) elif x == Layer.ObjectGroupType: name = self.tr("Object Layer %d" % (self.mMap.objectGroupCount() + 1)) layer = ObjectGroup(name, 0, 0, self.mMap.width(), self.mMap.height()) elif x == Layer.ImageLayerType: name = self.tr("Image Layer %d" % (self.mMap.imageLayerCount() + 1)) layer = ImageLayer(name, 0, 0, self.mMap.width(), self.mMap.height()) index = self.mMap.layerCount() self.mUndoStack.push(AddLayer(self, index, layer)) self.setCurrentLayerIndex(index) self.editLayerNameRequested.emit() ## # Duplicates the currently selected layer. ## def duplicateLayer(self): if (self.mCurrentLayerIndex == -1): return duplicate = self.mMap.layerAt(self.mCurrentLayerIndex).clone() duplicate.setName(self.tr("Copy of %s" % duplicate.name())) index = self.mCurrentLayerIndex + 1 cmd = AddLayer(self, index, duplicate) cmd.setText(self.tr("Duplicate Layer")) self.mUndoStack.push(cmd) self.setCurrentLayerIndex(index) ## # Merges the currently selected layer with the layer below. This only works # when the layers can be merged. # # \see Layer.canMergeWith ## def mergeLayerDown(self): if (self.mCurrentLayerIndex < 1): return upperLayer = self.mMap.layerAt(self.mCurrentLayerIndex) lowerLayer = self.mMap.layerAt(self.mCurrentLayerIndex - 1) if (not lowerLayer.canMergeWith(upperLayer)): return merged = lowerLayer.mergedWith(upperLayer) self.mUndoStack.beginMacro(self.tr("Merge Layer Down")) self.mUndoStack.push( AddLayer(self, self.mCurrentLayerIndex - 1, merged)) self.mUndoStack.push(RemoveLayer(self, self.mCurrentLayerIndex)) self.mUndoStack.push(RemoveLayer(self, self.mCurrentLayerIndex)) self.mUndoStack.endMacro() ## # Moves the given layer up. Does nothing when no valid layer index is # given. ## def moveLayerUp(self, index): if index < 0 or index >= self.mMap.layerCount() - 1: return self.mUndoStack.push(MoveLayer(self, index, MoveLayer.Up)) ## # Moves the given layer down. Does nothing when no valid layer index is # given. ## def moveLayerDown(self, index): if index < 1 or index >= self.mMap.layerCount(): return self.mUndoStack.push(MoveLayer(self, index, MoveLayer.Down)) ## # Removes the given layer. ## def removeLayer(self, index): if index < 0 or index >= self.mMap.layerCount(): return self.mUndoStack.push(RemoveLayer(self, index)) ## # Show or hide all other layers except the layer at the given index. # If any other layer is visible then all layers will be hidden, otherwise # the layers will be shown. ## def toggleOtherLayers(self, index): self.mLayerModel.toggleOtherLayers(index) ## # Adds a tileset to this map at the given \a index. Emits the appropriate # signal. ## def insertTileset(self, index, tileset): self.tilesetAboutToBeAdded.emit(index) self.mMap.insertTileset(index, tileset) tilesetManager = TilesetManager.instance() tilesetManager.addReference(tileset) self.tilesetAdded.emit(index, tileset) ## # Removes the tileset at the given \a index from this map. Emits the # appropriate signal. # # \warning Does not make sure that any references to tiles in the removed # tileset are cleared. ## def removeTilesetAt(self, index): self.tilesetAboutToBeRemoved.emit(index) tileset = self.mMap.tilesets().at(index) if (tileset == self.mCurrentObject or isFromTileset(self.mCurrentObject, tileset)): self.setCurrentObject(None) self.mMap.removeTilesetAt(index) self.tilesetRemoved.emit(tileset) tilesetManager = TilesetManager.instance() tilesetManager.removeReference(tileset) def moveTileset(self, _from, to): if (_from == to): return tileset = self.mMap.tilesets().at(_from) self.mMap.removeTilesetAt(_from) self.mMap.insertTileset(to, tileset) self.tilesetMoved.emit(_from, to) def setTilesetFileName(self, tileset, fileName): tileset.setFileName(fileName) self.tilesetFileNameChanged.emit(tileset) def setTilesetName(self, tileset, name): tileset.setName(name) self.tilesetNameChanged.emit(tileset) def setTilesetTileOffset(self, tileset, tileOffset): tileset.setTileOffset(tileOffset) self.mMap.recomputeDrawMargins() self.tilesetTileOffsetChanged.emit(tileset) def duplicateObjects(self, objects): if (objects.isEmpty()): return self.mUndoStack.beginMacro( self.tr("Duplicate %n Object(s)", "", objects.size())) clones = QList() for mapObject in objects: clone = mapObject.clone() clones.append(clone) self.mUndoStack.push( AddMapObject(self, mapObject.objectGroup(), clone)) self.mUndoStack.endMacro() self.setSelectedObjects(clones) def removeObjects(self, objects): if (objects.isEmpty()): return self.mUndoStack.beginMacro( self.tr("Remove %n Object(s)", "", objects.size())) for mapObject in objects: self.mUndoStack.push(RemoveMapObject(self, mapObject)) self.mUndoStack.endMacro() def moveObjectsToGroup(self, objects, objectGroup): if (objects.isEmpty()): return self.mUndoStack.beginMacro( self.tr("Move %n Object(s) to Layer", "", objects.size())) for mapObject in objects: if (mapObject.objectGroup() == objectGroup): continue self.mUndoStack.push( MoveMapObjectToGroup(self, mapObject, objectGroup)) self.mUndoStack.endMacro() def setProperty(self, object, name, value): hadProperty = object.hasProperty(name) object.setProperty(name, value) if (hadProperty): self.propertyChanged.emit(object, name) else: self.propertyAdded.emit(object, name) def setProperties(self, object, properties): object.setProperties(properties) self.propertiesChanged.emit(object) def removeProperty(self, object, name): object.removeProperty(name) self.propertyRemoved.emit(object, name) ## # Returns the layer model. Can be used to modify the layer stack of the # map, and to display the layer stack in a view. ## def layerModel(self): return self.mLayerModel def mapObjectModel(self): return self.mMapObjectModel def terrainModel(self): return self.mTerrainModel ## # Returns the map renderer. ## def renderer(self): return self.mRenderer ## # Creates the map renderer. Should be called after changing the map # orientation. ## def createRenderer(self): if (self.mRenderer): del self.mRenderer x = self.mMap.orientation() if x == Map.Orientation.Isometric: self.mRenderer = IsometricRenderer(self.mMap) elif x == Map.Orientation.Staggered: self.mRenderer = StaggeredRenderer(self.mMap) elif x == Map.Orientation.Hexagonal: self.mRenderer = HexagonalRenderer(self.mMap) else: self.mRenderer = OrthogonalRenderer(self.mMap) ## # Returns the undo stack of this map document. Should be used to push any # commands on that modify the map. ## def undoStack(self): return self.mUndoStack ## # Returns the selected area of tiles. ## def selectedArea(self): return QRegion(self.mSelectedArea) ## # Sets the selected area of tiles. ## def setSelectedArea(self, selection): if (self.mSelectedArea != selection): oldSelectedArea = self.mSelectedArea self.mSelectedArea = selection self.selectedAreaChanged.emit(self.mSelectedArea, oldSelectedArea) ## # Returns the list of selected objects. ## def selectedObjects(self): return self.mSelectedObjects ## # Sets the list of selected objects, emitting the selectedObjectsChanged # signal. ## def setSelectedObjects(self, selectedObjects): if selectedObjects.nequal(self.mSelectedObjects): self.mSelectedObjects = selectedObjects self.selectedObjectsChanged.emit() if (selectedObjects.size() == 1): self.setCurrentObject(selectedObjects.first()) ## # Returns the list of selected tiles. ## def selectedTiles(self): return self.mSelectedTiles def setSelectedTiles(self, selectedTiles): self.mSelectedTiles = selectedTiles self.selectedTilesChanged.emit() def currentObject(self): return self.mCurrentObject def setCurrentObject(self, object): if (object == self.mCurrentObject): return self.mCurrentObject = object self.currentObjectChanged.emit([object]) def currentObjects(self): objects = QList() if (self.mCurrentObject): if (self.mCurrentObject.typeId() == Object.MapObjectType and not self.mSelectedObjects.isEmpty()): for mapObj in self.mSelectedObjects: objects.append(mapObj) elif (self.mCurrentObject.typeId() == Object.TileType and not self.mSelectedTiles.isEmpty()): for tile in self.mSelectedTiles: objects.append(tile) else: objects.append(self.mCurrentObject) return objects def unifyTilesets(self, *args): l = len(args) if l == 1: ## # Makes sure the all tilesets which are used at the given \a map will be # present in the map document. # # To reach the aim, all similar tilesets will be replaced by the version # in the current map document and all missing tilesets will be added to # the current map document. # # \warning This method assumes that the tilesets in \a map are managed by # the TilesetManager! ## map = args[0] undoCommands = QList() existingTilesets = self.mMap.tilesets() tilesetManager = TilesetManager.instance() # Add tilesets that are not yet part of this map for tileset in map.tilesets(): if (existingTilesets.contains(tileset)): continue replacement = tileset.findSimilarTileset(existingTilesets) if (not replacement): undoCommands.append(AddTileset(self, tileset)) continue # Merge the tile properties sharedTileCount = min(tileset.tileCount(), replacement.tileCount()) for i in range(sharedTileCount): replacementTile = replacement.tileAt(i) properties = replacementTile.properties() properties.merge(tileset.tileAt(i).properties()) undoCommands.append( ChangeProperties(self, self.tr("Tile"), replacementTile, properties)) map.replaceTileset(tileset, replacement) tilesetManager.addReference(replacement) tilesetManager.removeReference(tileset) if (not undoCommands.isEmpty()): self.mUndoStack.beginMacro(self.tr("Tileset Changes")) for command in undoCommands: self.mUndoStack.push(command) self.mUndoStack.endMacro() elif l == 2: map, missingTilesets = args existingTilesets = self.mMap.tilesets() tilesetManager = TilesetManager.instance() for tileset in map.tilesets(): # tileset already added if existingTilesets.contains(tileset): continue replacement = tileset.findSimilarTileset(existingTilesets) # tileset not present and no replacement tileset found if not replacement: if not missingTilesets.contains(tileset): missingTilesets.append(tileset) continue # replacement tileset found, change given map map.replaceTileset(tileset, replacement) tilesetManager.addReference(replacement) tilesetManager.removeReference(tileset) ## # Emits the map changed signal. This signal should be emitted after changing # the map size or its tile size. ## def emitMapChanged(self): self.mapChanged.emit() ## # Emits the region changed signal for the specified region. The region # should be in tile coordinates. This method is used by the TilePainter. ## def emitRegionChanged(self, region, layer): self.regionChanged.emit(region, layer) ## # Emits the region edited signal for the specified region and tile layer. # The region should be in tile coordinates. This should be called from # all map document changing classes which are triggered by user input. ## def emitRegionEdited(self, region, layer): self.regionEdited.emit(region, layer) def emitTileLayerDrawMarginsChanged(self, layer): self.tileLayerDrawMarginsChanged.emit(layer) ## # Emits the tileset changed signal. This signal is currently used when adding # or removing tiles from a tileset. # # @todo Emit more specific signals. ## def emitTilesetChanged(self, tileset): self.tilesetChanged.emit(tileset) ## # Emits the signal notifying about the terrain probability of a tile changing. ## def emitTileProbabilityChanged(self, tile): self.tileProbabilityChanged.emit(tile) ## # Emits the signal notifying tileset models about changes to tile terrain # information. All the \a tiles need to be from the same tileset. ## def emitTileTerrainChanged(self, tiles): if (not tiles.isEmpty()): self.tileTerrainChanged.emit(tiles) ## # Emits the signal notifying the TileCollisionEditor about the object group # of a tile changing. ## def emitTileObjectGroupChanged(self, tile): self.tileObjectGroupChanged.emit(tile) ## # Emits the signal notifying about the animation of a tile changing. ## def emitTileAnimationChanged(self, tile): self.tileAnimationChanged.emit(tile) ## # Emits the objectGroupChanged signal, should be called when changing the # color or drawing order of an object group. ## def emitObjectGroupChanged(self, objectGroup): self.objectGroupChanged.emit(objectGroup) ## # Emits the imageLayerChanged signal, should be called when changing the # image or the transparent color of an image layer. ## def emitImageLayerChanged(self, imageLayer): self.imageLayerChanged.emit(imageLayer) ## # Emits the editLayerNameRequested signal, to get renamed. ## def emitEditLayerNameRequested(self): self.editLayerNameRequested.emit() ## # Emits the editCurrentObject signal, which makes the Properties window become # visible and take focus. ## def emitEditCurrentObject(self): self.editCurrentObject.emit() ## # Before forwarding the signal, the objects are removed from the list of # selected objects, triggering a selectedObjectsChanged signal when # appropriate. ## def onObjectsRemoved(self, objects): self.deselectObjects(objects) self.objectsRemoved.emit(objects) def onMapObjectModelRowsInserted(self, parent, first, last): objectGroup = self.mMapObjectModel.toObjectGroup(parent) if (not objectGroup): # we're not dealing with insertion of objects return self.objectsInserted.emit(objectGroup, first, last) self.onMapObjectModelRowsInsertedOrRemoved(parent, first, last) def onMapObjectModelRowsInsertedOrRemoved(self, parent, first, last): objectGroup = self.mMapObjectModel.toObjectGroup(parent) if (not objectGroup): return # Inserting or removing objects changes the index of any that come after lastIndex = objectGroup.objectCount() - 1 if (last < lastIndex): self.objectsIndexChanged.emit(objectGroup, last + 1, lastIndex) def onObjectsMoved(self, parent, start, end, destination, row): if (parent != destination): return objectGroup = self.mMapObjectModel.toObjectGroup(parent) # Determine the full range over which object indexes changed first = min(start, row) last = max(end, row - 1) self.objectsIndexChanged.emit(objectGroup, first, last) def onLayerAdded(self, index): self.layerAdded.emit(index) # Select the first layer that gets added to the map if (self.mMap.layerCount() == 1): self.setCurrentLayerIndex(0) def onLayerAboutToBeRemoved(self, index): layer = self.mMap.layerAt(index) if (layer == self.mCurrentObject): self.setCurrentObject(None) # Deselect any objects on this layer when necessary og = layer if type(og) == ObjectGroup: self.deselectObjects(og.objects()) self.layerAboutToBeRemoved.emit(index) def onLayerRemoved(self, index): # Bring the current layer index to safety currentLayerRemoved = self.mCurrentLayerIndex == self.mMap.layerCount() if (currentLayerRemoved): self.mCurrentLayerIndex = self.mCurrentLayerIndex - 1 self.layerRemoved.emit(index) # Emitted after the layerRemoved signal so that the MapScene has a chance # of synchronizing before adapting to the newly selected index if (currentLayerRemoved): self.currentLayerIndexChanged.emit(self.mCurrentLayerIndex) def onTerrainRemoved(self, terrain): if (terrain == self.mCurrentObject): self.setCurrentObject(None) def setFileName(self, fileName): if (self.mFileName == fileName): return oldFileName = self.mFileName self.mFileName = fileName self.fileNameChanged.emit(fileName, oldFileName) def deselectObjects(self, objects): # Unset the current object when it was part of this list of objects if (self.mCurrentObject and self.mCurrentObject.typeId() == Object.MapObjectType): if (objects.contains(self.mCurrentObject)): self.setCurrentObject(None) removedCount = 0 for object in objects: removedCount += self.mSelectedObjects.removeAll(object) if (removedCount > 0): self.selectedObjectsChanged.emit() def disconnect(self): try: super().disconnect() except: pass
class PangoGraphicsScene(QGraphicsScene): gfx_changed = pyqtSignal(PangoGraphic, QGraphicsItem.GraphicsItemChange) gfx_removed = pyqtSignal(PangoGraphic) clear_tool = pyqtSignal() def __init__(self, parent=None): super().__init__(parent) self.change_stacks = {} self.stack = QUndoStack() self.fpath = None self.px = QPixmap() self.active_label = PangoGraphic() self.active_com = CreateShape(PangoGraphic, QPointF(), PangoGraphic()) self.tool = None self.tool_size = 10 self.full_clear() def full_clear(self): self.stack.clear() self.clear() self.init_reticle() self.reset_com() self.clear_tool.emit() def init_reticle(self): self.reticle = QGraphicsEllipseItem(-5, -5, 10, 10) self.reticle.setVisible(False) self.reticle.setPen(QPen(Qt.NoPen)) self.addItem(self.reticle) def set_fpath(self, fpath): self.fpath = fpath self.px = QPixmap(self.fpath) self.setSceneRect(QRectF(self.px.rect())) def drawBackground(self, painter, rect): painter.drawPixmap(0, 0, self.px) def reset_com(self): if type(self.active_com.gfx) is PangoPolyGraphic: if not self.active_com.gfx.poly.isClosed(): self.unravel_shapes(self.active_com.gfx) self.active_com = CreateShape(PangoGraphic, QPointF(), PangoGraphic()) # Undo all commands for shapes (including creation) def unravel_shapes(self, *gfxs): for stack in self.change_stacks.values(): for i in range(stack.count() - 1, -1, -1): com = stack.command(i) if type(com) is QUndoCommand: for j in range(0, com.childCount()): sub_com = com.child(j) if sub_com.gfx in gfxs: com.setObsolete(True) else: if com.gfx in gfxs: com.setObsolete(True) if type(com) == CreateShape: break # Reached shape creation stack.setIndex(0) stack.setIndex(stack.count()) self.active_com = CreateShape(PangoGraphic, QPointF(), PangoGraphic()) def event(self, event): super().event(event) if self.tool == "Lasso": self.select_handler(event) elif self.tool == "Path": self.path_handler(event) elif self.tool == "Poly": self.poly_handler(event) elif self.tool == "Bbox": self.bbox_handler(event) return False def select_handler(self, event): if event.type() == QEvent.GraphicsSceneMousePress and event.buttons( ) & Qt.LeftButton: gfx = self.itemAt(event.scenePos(), QTransform()) if type(gfx) is PangoPolyGraphic: min_dx = float("inf") for i in range(0, gfx.poly.count()): dx = QLineF(gfx.poly.value(i), event.scenePos()).length() if dx < min_dx: min_dx = dx idx = i if min_dx < 20: self.active_com = MoveShape(event.scenePos(), gfx, idx=idx) self.stack.push(self.active_com) elif type(gfx) is PangoBboxGraphic: min_dx = float("inf") for corner in [ "topLeft", "topRight", "bottomLeft", "bottomRight" ]: dx = QLineF(getattr(gfx.rect, corner)(), event.scenePos()).length() if dx < min_dx: min_dx = dx min_corner = corner if min_dx < 20: self.active_com = MoveShape(event.scenePos(), gfx, corner=min_corner) self.stack.push(self.active_com) elif event.type( ) == QEvent.GraphicsSceneMouseMove and event.buttons() & Qt.LeftButton: if type(self.active_com) is MoveShape: if type(self.active_com.gfx) is PangoPolyGraphic: self.stack.undo() self.active_com.pos = event.scenePos() self.stack.redo() elif type(self.active_com.gfx) is PangoBboxGraphic: self.stack.undo() old_pos = self.active_com.pos self.active_com.pos = event.scenePos() self.stack.redo() tl = self.active_com.gfx.rect.topLeft() br = self.active_com.gfx.rect.bottomRight() if tl.x() > br.x() or tl.y() > br.y(): self.stack.undo() self.active_com.pos = old_pos self.stack.redo() elif event.type() == QEvent.GraphicsSceneMouseRelease: self.reset_com() def path_handler(self, event): if event.type() == QEvent.GraphicsSceneMousePress: if type(self.active_com.gfx) is not PangoPathGraphic: self.active_com = CreateShape(PangoPathGraphic, event.scenePos(), self.active_label) self.stack.push(self.active_com) self.stack.beginMacro("Extended " + self.active_com.gfx.name) self.active_com = ExtendShape(event.scenePos(), self.active_com.gfx, "moveTo") self.stack.push(self.active_com) elif event.type() == QEvent.GraphicsSceneMouseMove: self.reticle.setPos(event.scenePos()) if event.buttons() & Qt.LeftButton: if type(self.active_com.gfx) is PangoPathGraphic: self.active_com = ExtendShape(event.scenePos(), self.active_com.gfx, "lineTo") self.stack.push(self.active_com) elif event.type() == QEvent.GraphicsSceneMouseRelease: self.stack.endMacro() def poly_handler(self, event): if event.type() == QEvent.GraphicsSceneMousePress: if type(self.active_com.gfx) is not PangoPolyGraphic: self.active_com = CreateShape(PangoPolyGraphic, event.scenePos(), self.active_label) self.stack.push(self.active_com) gfx = self.active_com.gfx pos = event.scenePos() if gfx.poly.count() <= 1 or not gfx.poly.isClosed(): if QLineF(event.scenePos(), gfx.poly.first()).length() < gfx.dw() * 2: pos = QPointF() pos.setX(gfx.poly.first().x()) pos.setY(gfx.poly.first().y()) self.active_com = ExtendShape(pos, self.active_com.gfx) self.stack.push(self.active_com) if gfx.poly.count() > 1 and gfx.poly.isClosed(): self.reset_com() def bbox_handler(self, event): if event.type() == QEvent.GraphicsSceneMousePress: if type(self.active_com.gfx) is not PangoBboxGraphic: self.active_com = CreateShape(PangoBboxGraphic, event.scenePos(), self.active_label) self.stack.beginMacro(self.active_com.text()) self.stack.push(self.active_com) self.active_com = MoveShape(event.scenePos(), self.active_com.gfx, corner="topLeft") self.stack.push(self.active_com) self.active_com = MoveShape(event.scenePos(), self.active_com.gfx, corner="bottomRight") self.stack.push(self.active_com) elif event.type() == QEvent.GraphicsSceneMouseMove: if event.buttons() & Qt.LeftButton: if type(self.active_com.gfx) is PangoBboxGraphic: tl = self.active_com.gfx.rect.topLeft() br = event.scenePos() if tl.x() < br.x() and tl.y() < br.y(): self.active_com = MoveShape(event.scenePos(), self.active_com.gfx, corner="bottomRight") self.stack.push(self.active_com) elif event.type() == QEvent.GraphicsSceneMouseRelease: if type(self.active_com.gfx) is PangoBboxGraphic: self.stack.endMacro() tl = self.active_com.gfx.rect.topLeft() br = self.active_com.gfx.rect.bottomRight() if QLineF(tl, br).length() < self.active_com.gfx.dw() * 2: self.unravel_shapes(self.active_com.gfx) self.reset_com()