def drawGrid(self, painter, rect, gridColor): tileWidth = self.map().tileWidth() tileHeight = self.map().tileHeight() if (tileWidth <= 0 or tileHeight <= 0): return startX = max(0, int(rect.x() / tileWidth) * tileWidth) startY = max(0, int(rect.y() / tileHeight) * tileHeight) endX = min(math.ceil(rect.right()), self.map().width() * tileWidth + 1) endY = min(math.ceil(rect.bottom()), self.map().height() * tileHeight + 1) gridColor.setAlpha(128) gridPen = QPen(gridColor) gridPen.setCosmetic(True) _x = QVector() _x.append(2) _x.append(2) gridPen.setDashPattern(_x) if (startY < endY): gridPen.setDashOffset(startY) painter.setPen(gridPen) for x in range(startX, endX, tileWidth): painter.drawLine(x, startY, x, endY - 1) if (startX < endX): gridPen.setDashOffset(startX) painter.setPen(gridPen) for y in range(startY, endY, tileHeight): painter.drawLine(startX, y, endX - 1, y)
def drawGrid(self, painter, rect, gridColor): tileWidth = self.map().tileWidth() tileHeight = self.map().tileHeight() r = rect.toAlignedRect() r.adjust(-tileWidth / 2, -tileHeight / 2, tileWidth / 2, tileHeight / 2) startX = int(max(0.0, self.screenToTileCoords_(r.topLeft()).x())) startY = int(max(0.0, self.screenToTileCoords_(r.topRight()).y())) endX = int(min(self.map().width(), self.screenToTileCoords_(r.bottomRight()).x())) endY = int(min(self.map().height(), self.screenToTileCoords_(r.bottomLeft()).y())) gridColor.setAlpha(128) gridPen = QPen(gridColor) gridPen.setCosmetic(True) _x = QVector() _x.append(2) _x.append(2) gridPen.setDashPattern(_x) painter.setPen(gridPen) for y in range(startY, endY+1): start = self.tileToScreenCoords(startX, y) end = self.tileToScreenCoords(endX, y) painter.drawLine(start, end) for x in range(startX, endX+1): start = self.tileToScreenCoords(x, startY) end = self.tileToScreenCoords(x, endY) painter.drawLine(start, end)
def offsetTiles(self, offset, bounds, wrapX, wrapY): newGrid = QVector() for i in range(self.mWidth * self.mHeight): newGrid.append(Cell()) for y in range(self.mHeight): for x in range(self.mWidth): # Skip out of bounds tiles if (not bounds.contains(x, y)): newGrid[x + y * self.mWidth] = self.cellAt(x, y) continue # Get position to pull tile value from oldX = x - offset.x() oldY = y - offset.y() # Wrap x value that will be pulled from if (wrapX and bounds.width() > 0): while oldX < bounds.left(): oldX += bounds.width() while oldX > bounds.right(): oldX -= bounds.width() # Wrap y value that will be pulled from if (wrapY and bounds.height() > 0): while oldY < bounds.top(): oldY += bounds.height() while oldY > bounds.bottom(): oldY -= bounds.height() # Set the new tile if (self.contains(oldX, oldY) and bounds.contains(oldX, oldY)): newGrid[x + y * self.mWidth] = self.cellAt(oldX, oldY) else: newGrid[x + y * self.mWidth] = Cell() self.mGrid = newGrid
def readObjectTypes(self, fileName): self.mError = '' objectTypes = QVector() file = QFile(fileName) if (not file.open(QIODevice.ReadOnly | QIODevice.Text)): self.mError = QCoreApplication.translate( "ObjectTypes", "Could not open file.") return objectTypes reader = QXmlStreamReader(file) if (not reader.readNextStartElement() or reader.name() != "objecttypes"): self.mError = QCoreApplication.translate( "ObjectTypes", "File doesn't contain object types.") return objectTypes while (reader.readNextStartElement()): if (reader.name() == "objecttype"): atts = reader.attributes() name = QString(atts.value("name")) color = QColor(atts.value("color")) objectTypes.append(ObjectType(name, color)) reader.skipCurrentElement() if (reader.hasError()): self.mError = QCoreApplication.translate("ObjectTypes", "%s\n\nLine %d, column %d"%(reader.errorString(), reader.lineNumber(), reader.columnNumber())) return objectTypes return objectTypes
def removeObjectTypes(self, indexes): rows = QVector() for index in indexes: rows.append(index.row()) rows = sorted(rows) for i in range(len(rows) - 1, -1, -1): row = rows[i] self.beginRemoveRows(QModelIndex(), row, row) self.mObjectTypes.remove(row) self.endRemoveRows()
def cellsInRegion(list, r): cells = QVector() for tilelayer in list: for rect in r.rects(): for x in range(rect.left(), rect.right()+1): for y in range(rect.top(), rect.bottom()+1): cell = tilelayer.cellAt(x, y) if (not cells.contains(cell)): cells.append(cell) return cells
def __readAnimationFrames(self): frames = QVector() while (self.xml.readNextStartElement()): if (self.xml.name() == "frame"): atts = self.xml.attributes() frame = Frame() frame.tileId = Int(atts.value("tileid")) frame.duration = Int(atts.value("duration")) frames.append(frame) self.xml.skipCurrentElement() else: self.__readUnknownElement() return frames
def __init__(self, name, x, y, width, height): super().__init__(Layer.TileLayerType, name, x, y, width, height) self.mMaxTileSize = QSize(0, 0) self.mGrid = QVector() for i in range(width * height): self.mGrid.append(Cell()) self.mOffsetMargins = QMargins()
def __init__(self, parent = None): super().__init__(self.tr("Select Objects"), QIcon(":images/22x22/tool-select-objects.png"), QKeySequence(self.tr("S")), parent) self.mSelectionRectangle = SelectionRectangle() self.mOriginIndicator = OriginIndicator() self.mMousePressed = False self.mHoveredObjectItem = None self.mClickedObjectItem = None self.mClickedRotateHandle = None self.mClickedResizeHandle = None self.mResizingLimitHorizontal = False self.mResizingLimitVertical = False self.mMode = Mode.Resize self.mAction = Action.NoAction self.mRotateHandles = [0, 0, 0, 0] self.mResizeHandles = [0, 0, 0, 0, 0, 0, 0, 0] self.mAlignPosition = QPointF() self.mMovingObjects = QVector() self.mScreenStart = QPoint() self.mStart = QPointF() self.mModifiers = 0 self.mOrigin = QPointF() for i in range(AnchorPosition.CornerAnchorCount): self.mRotateHandles[i] = RotateHandle(i) for i in range(AnchorPosition.AnchorCount): self.mResizeHandles[i] = ResizeHandle(i)
def __init__(self, parent = None): super().__init__(self.tr("Bucket Fill Tool"), QIcon(":images/22x22/stock-tool-bucket-fill.png"), QKeySequence(self.tr("F")), parent) self.mStamp = TileStamp() self.mFillOverlay = None self.mFillRegion = QRegion() self.mMissingTilesets = QVector() self.mIsActive = False self.mLastShiftStatus = False ## # Indicates if the tool is using the random mode. ## self.mIsRandom = False ## # Contains the value of mIsRandom at that time, when the latest call of # tilePositionChanged() took place. # This variable is needed to detect if the random mode was changed during # mFillOverlay being brushed at an area. ## self.mLastRandomStatus = False ## # Contains all used random cells to use in random mode. # The same cell can be in the list multiple times to make different # random weights possible. ## self.mRandomCellPicker = RandomPicker()
def __init__(self, parent = None): super().__init__(parent) ## # The current map document. ## self.mMapDocument = None ## # For each new file of rules a new AutoMapper is setup. In this vector we # can store all of the AutoMappers in order. ## self.mAutoMappers = QVector() ## # This tells you if the rules for the current map document were already # loaded. ## self.mLoaded = False ## # Contains all errors which occurred until canceling. # If mError is not empty, no serious result can be expected. ## self.mError = '' ## # Contains all strings, which try to explain unusual and unexpected # behavior. ## self.mWarning = QString()
def __init__(self, mapDocument, autoMapper, where): super().__init__() self.mLayersAfter = QVector() self.mLayersBefore = QVector() self.mMapDocument = mapDocument map = self.mMapDocument.Map() touchedLayers = QSet() index = 0 while (index < autoMapper.size()): a = autoMapper.at(index) if (a.prepareAutoMap()): touchedLayers|= a.getTouchedTileLayers() index += 1 else: autoMapper.remove(index) for layerName in touchedLayers: layerindex = map.indexOfLayer(layerName) self.mLayersBefore (map.layerAt(layerindex).clone()) for a in autoMapper: a.autoMap(where) for layerName in touchedLayers: layerindex = map.indexOfLayer(layerName) # layerindex exists, because AutoMapper is still alive, dont check self.mLayersAfter (map.layerAt(layerindex).clone()) # reduce memory usage by saving only diffs for i in range(self.mLayersAfter.size()): before = self.mLayersBefore.at(i) after = self.mLayersAfter.at(i) diffRegion = before.computeDiffRegion(after).boundingRect() before1 = before.copy(diffRegion) after1 = after.copy(diffRegion) before1.setPosition(diffRegion.topLeft()) after1.setPosition(diffRegion.topLeft()) before1.setName(before.name()) after1.setName(after.name()) self.mLayersBefore.replace(i, before1) self.mLayersAfter.replace(i, after1) del before del after for a in autoMapper: a.cleanAll()
def flip(self, direction): newGrid = QVector() for i in range(self.mWidth * self.mHeight): newGrid.append(Cell()) for y in range(self.mHeight): for x in range(self.mWidth): dest = newGrid[x + y * self.mWidth] if (direction == FlipDirection.FlipHorizontally): source = self.cellAt(self.mWidth - x - 1, y) dest = source dest.flippedHorizontally = not source.flippedHorizontally elif (direction == FlipDirection.FlipVertically): source = self.cellAt(x, self.mHeight - y - 1) dest = source dest.flippedVertically = not source.flippedVertically self.mGrid = newGrid
def resize(self, size, offset): if (self.size() == size and offset.isNull()): return newGrid = QVector() for i in range(size.width() * size.height()): newGrid.append(Cell()) # Copy over the preserved part startX = max(0, -offset.x()) startY = max(0, -offset.y()) endX = min(self.mWidth, size.width() - offset.x()) endY = min(self.mHeight, size.height() - offset.y()) for y in range(startY, endY): for x in range(startX, endX): index = x + offset.x() + (y + offset.y()) * size.width() newGrid[index] = self.cellAt(x, y) self.mGrid = newGrid self.setSize(size)
def autoMapInternal(self, where, touchedLayer): self.mError = '' self.mWarning = '' if (not self.mMapDocument): return automatic = touchedLayer != None if (not self.mLoaded): mapPath = QFileInfo(self.mMapDocument.fileName()).path() rulesFileName = mapPath + "/rules.txt" if (self.loadFile(rulesFileName)): self.mLoaded = True else: self.errorsOccurred.emit(automatic) return passedAutoMappers = QVector() if (touchedLayer): for a in self.mAutoMappers: if (a.ruleLayerNameUsed(touchedLayer.name())): passedAutoMappers.append(a) else: passedAutoMappers = self.mAutoMappers if (not passedAutoMappers.isEmpty()): # use a pointer to the region, so each automapper can manipulate it and the # following automappers do see the impact region = QRegion(where) undoStack = self.mMapDocument.undoStack() undoStack.beginMacro(self.tr("Apply AutoMap rules")) aw = AutoMapperWrapper(self.mMapDocument, passedAutoMappers, region) undoStack.push(aw) undoStack.endMacro() for automapper in self.mAutoMappers: self.mWarning += automapper.warningString() self.mError += automapper.errorString() if self.mWarning != '': self.warningsOccurred.emit(automatic) if self.mError != '': self.errorsOccurred.emit(automatic)
def __init__(self, parent = None): super().__init__(parent) self.mScale = 1 self.mZoomFactors = QVector() self.mGestureStartScale = 0 self.mComboBox = None self.mComboRegExp = QRegExp("^\\s*(\\d+)\\s*%?\\s*$") self.mComboValidator = None for i in range(zoomFactorCount): self.mZoomFactors.append(zoomFactors[i])
class SetProperty(QUndoCommand): ## # Constructs a new 'Set Property' command. # # @param mapDocument the map document of the object's map # @param objects the objects of which the property should be changed # @param name the name of the property to be changed # @param value the new value of the property ## def __init__(self, mapDocument, objects, name, value, parent = None): super().__init__(parent) self.mProperties = QVector() self.mMapDocument = mapDocument self.mObjects = objects self.mName = name self.mValue = value for obj in self.mObjects: prop = ObjectProperty() prop.existed = obj.hasProperty(self.mName) prop.previousValue = obj.property(self.mName) self.mProperties.append(prop) if (self.mObjects.size() > 1 or self.mObjects[0].hasProperty(self.mName)): self.setText(QCoreApplication.translate("Undo Commands", "Set Property")) else: self.setText(QCoreApplication.translate("Undo Commands", "Add Property")) def undo(self): for i in range(self.mObjects.size()): if (self.mProperties[i].existed): self.mMapDocument.setProperty(self.mObjects[i], self.mName, self.mProperties[i].previousValue) else: self.mMapDocument.removeProperty(self.mObjects[i], self.mName) def redo(self): for obj in self.mObjects: self.mMapDocument.setProperty(obj, self.mName, self.mValue)
def __init__(self, toolManager, parent = None): super().__init__(parent) self.mStampsByName = QMap() self.mQuickStamps = QVector() for i in range(TileStampManager.quickStampKeys().__len__()): self.mQuickStamps.append(0) self.mTileStampModel = TileStampModel(self) self.mToolManager = toolManager prefs = preferences.Preferences.instance() prefs.stampsDirectoryChanged.connect(self.stampsDirectoryChanged) self.mTileStampModel.stampAdded.connect(self.stampAdded) self.mTileStampModel.stampRenamed.connect(self.stampRenamed) self.mTileStampModel.stampChanged.connect(self.saveStamp) self.mTileStampModel.stampRemoved.connect(self.deleteStamp) self.loadStamps()
def __init__(self, mapDocument, objects, name, value, parent = None): super().__init__(parent) self.mProperties = QVector() self.mMapDocument = mapDocument self.mObjects = objects self.mName = name self.mValue = value for obj in self.mObjects: prop = ObjectProperty() prop.existed = obj.hasProperty(self.mName) prop.previousValue = obj.property(self.mName) self.mProperties.append(prop) if (self.mObjects.size() > 1 or self.mObjects[0].hasProperty(self.mName)): self.setText(QCoreApplication.translate("Undo Commands", "Set Property")) else: self.setText(QCoreApplication.translate("Undo Commands", "Add Property"))
def __init__(self, parent): super().__init__(parent) self.mMapDocument = None self.mSelectedTool = None self.mActiveTool = None self.mObjectSelectionItem = None self.mUnderMouse = False self.mCurrentModifiers = Qt.NoModifier, self.mDarkRectangle = QGraphicsRectItem() self.mDefaultBackgroundColor = Qt.darkGray self.mLayerItems = QVector() self.mObjectItems = QMap() self.mObjectLineWidth = 0.0 self.mSelectedObjectItems = QSet() self.mLastMousePos = QPointF() self.mShowTileObjectOutlines = False self.mHighlightCurrentLayer = False self.mGridVisible = False self.setBackgroundBrush(self.mDefaultBackgroundColor) tilesetManager = TilesetManager.instance() tilesetManager.tilesetChanged.connect(self.tilesetChanged) tilesetManager.repaintTileset.connect(self.tilesetChanged) prefs = preferences.Preferences.instance() prefs.showGridChanged.connect(self.setGridVisible) prefs.showTileObjectOutlinesChanged.connect(self.setShowTileObjectOutlines) prefs.objectTypesChanged.connect(self.syncAllObjectItems) prefs.highlightCurrentLayerChanged.connect(self.setHighlightCurrentLayer) prefs.gridColorChanged.connect(self.update) prefs.objectLineWidthChanged.connect(self.setObjectLineWidth) self.mDarkRectangle.setPen(QPen(Qt.NoPen)) self.mDarkRectangle.setBrush(Qt.black) self.mDarkRectangle.setOpacity(darkeningFactor) self.addItem(self.mDarkRectangle) self.mGridVisible = prefs.showGrid() self.mObjectLineWidth = prefs.objectLineWidth() self.mShowTileObjectOutlines = prefs.showTileObjectOutlines() self.mHighlightCurrentLayer = prefs.highlightCurrentLayer() # Install an event filter so that we can get key events on behalf of the # active tool without having to have the current focus. QCoreApplication.instance().installEventFilter(self)
def __init__(self, *args): super().__init__(Object.TileType) l = len(args) if l==3: image, id, tileset = args self.mImageSource = QString() elif l==4: image, imageSource, id, tileset = args self.mImageSource = imageSource self.mId = id self.mTileset = tileset self.mImage = image self.mTerrain = 0xffffffff self.mProbability = 1.0 self.mObjectGroup = None self.mFrames = QVector() self.mCurrentFrameIndex = 0 self.mUnusedTime = 0
def __init__(self, parent = None): super().__init__(self.tr("Edit Polygons"), QIcon(":images/24x24/tool-edit-polygons.png"), QKeySequence(self.tr("E")), parent) self.mSelectedHandles = QSet() self.mModifiers = Qt.KeyboardModifiers() self.mScreenStart = QPoint() self.mOldHandlePositions = QVector() self.mAlignPosition = QPointF() ## The list of handles associated with each selected map object self.mHandles = QMapList() self.mOldPolygons = QMap() self.mStart = QPointF() self.mSelectionRectangle = SelectionRectangle() self.mMousePressed = False self.mClickedHandle = None self.mClickedObjectItem = None self.mMode = EditPolygonTool.NoMode
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 pointsOnLine(*args): l = len(args) if l==2: a, b = args return pointsOnLine(a.x(), a.y(), b.x(), b.y()) else: x0, y0, x1, y1 = args ret = QVector() steep = abs(y1 - y0) > abs(x1 - x0) if steep: x0, y0 = y0, x0 x1, y1 = y1, x1 reverse = x0 > x1 if reverse: x0, x1 = x1, x0 y0, y1 = y1, y0 deltax = x1 - x0 deltay = abs(y1 - y0) error= int(deltax / 2) ystep = 0 y = y0 if (y0 < y1): ystep = 1 else: ystep = -1 for x in range(x0, x1+1): if (steep): ret.append(QPoint(y, x)) else: ret.append(QPoint(x, y)) error = error - deltay if (error < 0): y = y + ystep error = error + deltax if reverse: ret.reverse() return ret
class CellRenderer(): BottomLeft, BottomCenter, TopLeft = range(3) def __init__(self, painter): self.mPainter = painter self.mTile = None self.mIsOpenGL = hasOpenGLEngine(painter) self.mFragments = QVector() def __del__(self): self.flush() ## # Renders a \a cell with the given \a origin at \a pos, taking into account # the flipping and tile offset. # # For performance reasons, the actual drawing is delayed until a different # kind of tile has to be drawn. For this reason it is necessary to call # flush when finished doing drawCell calls. This function is also called by # the destructor so usually an explicit call is not needed. ## def render(self, cell, pos, cellSize, origin): if (self.mTile != cell.tile): self.flush() image = cell.tile.currentFrameImage() size = image.size() if cellSize == QSizeF(0, 0): objectSize = size else: objectSize = cellSize scale = QSizeF(objectSize.width() / size.width(), objectSize.height() / size.height()) offset = cell.tile.offset() sizeHalf = QPointF(objectSize.width() / 2, objectSize.height() / 2) fragment = QPainter.PixmapFragment() fragment.x = pos.x() + (offset.x() * scale.width()) + sizeHalf.x() fragment.y = pos.y() + ( offset.y() * scale.height()) + sizeHalf.y() - objectSize.height() fragment.sourceLeft = 0 fragment.sourceTop = 0 fragment.width = size.width() fragment.height = size.height() if cell.flippedHorizontally: fragment.scaleX = -1 else: fragment.scaleX = 1 if cell.flippedVertically: fragment.scaleY = -1 else: fragment.scaleY = 1 fragment.rotation = 0 fragment.opacity = 1 flippedHorizontally = cell.flippedHorizontally flippedVertically = cell.flippedVertically if (origin == CellRenderer.BottomCenter): fragment.x -= sizeHalf.x() if (cell.flippedAntiDiagonally): fragment.rotation = 90 flippedHorizontally = cell.flippedVertically flippedVertically = not cell.flippedHorizontally # Compensate for the swap of image dimensions halfDiff = sizeHalf.y() - sizeHalf.x() fragment.y += halfDiff if (origin != CellRenderer.BottomCenter): fragment.x += halfDiff if flippedHorizontally: x = -1 else: x = 1 fragment.scaleX = scale.width() * x if flippedVertically: x = -1 else: x = 1 fragment.scaleY = scale.height() * x if (self.mIsOpenGL or (fragment.scaleX > 0 and fragment.scaleY > 0)): self.mTile = cell.tile self.mFragments.append(fragment) return # The Raster paint engine as of Qt 4.8.4 / 5.0.2 does not support # drawing fragments with a negative scaling factor. self.flush() # make sure we drew all tiles so far oldTransform = self.mPainter.transform() transform = oldTransform transform.translate(fragment.x, fragment.y) transform.rotate(fragment.rotation) transform.scale(fragment.scaleX, fragment.scaleY) target = QRectF(fragment.width * -0.5, fragment.height * -0.5, fragment.width, fragment.height) source = QRectF(0, 0, fragment.width, fragment.height) self.mPainter.setTransform(transform) self.mPainter.drawPixmap(target, image, source) self.mPainter.setTransform(oldTransform) def flush(self): if (not self.mTile): return self.mPainter.drawPixmapFragments(self.mFragments, self.mTile.currentFrameImage()) self.mTile = None self.mFragments.resize(0)
def __init__(self, parent=None): super().__init__(parent) # Shared tileset references because the dock wants to add new tiles self.mTilesets = QVector() self.mCurrentTilesets = QMap() self.mMapDocument = None self.mTabBar = QTabBar() self.mViewStack = QStackedWidget() self.mToolBar = QToolBar() self.mCurrentTile = None self.mCurrentTiles = None self.mNewTileset = QAction(self) self.mImportTileset = QAction(self) self.mExportTileset = QAction(self) self.mPropertiesTileset = QAction(self) self.mDeleteTileset = QAction(self) self.mEditTerrain = QAction(self) self.mAddTiles = QAction(self) self.mRemoveTiles = QAction(self) self.mTilesetMenuButton = TilesetMenuButton(self) self.mTilesetMenu = QMenu(self) # opens on click of mTilesetMenu self.mTilesetActionGroup = QActionGroup(self) self.mTilesetMenuMapper = None # needed due to dynamic content self.mEmittingStampCaptured = False self.mSynchronizingSelection = False self.setObjectName("TilesetDock") self.mTabBar.setMovable(True) self.mTabBar.setUsesScrollButtons(True) self.mTabBar.currentChanged.connect(self.updateActions) self.mTabBar.tabMoved.connect(self.moveTileset) w = QWidget(self) horizontal = QHBoxLayout() horizontal.setSpacing(0) horizontal.addWidget(self.mTabBar) horizontal.addWidget(self.mTilesetMenuButton) vertical = QVBoxLayout(w) vertical.setSpacing(0) vertical.setContentsMargins(5, 5, 5, 5) vertical.addLayout(horizontal) vertical.addWidget(self.mViewStack) horizontal = QHBoxLayout() horizontal.setSpacing(0) horizontal.addWidget(self.mToolBar, 1) vertical.addLayout(horizontal) self.mNewTileset.setIcon(QIcon(":images/16x16/document-new.png")) self.mImportTileset.setIcon(QIcon(":images/16x16/document-import.png")) self.mExportTileset.setIcon(QIcon(":images/16x16/document-export.png")) self.mPropertiesTileset.setIcon( QIcon(":images/16x16/document-properties.png")) self.mDeleteTileset.setIcon(QIcon(":images/16x16/edit-delete.png")) self.mEditTerrain.setIcon(QIcon(":images/16x16/terrain.png")) self.mAddTiles.setIcon(QIcon(":images/16x16/add.png")) self.mRemoveTiles.setIcon(QIcon(":images/16x16/remove.png")) Utils.setThemeIcon(self.mNewTileset, "document-new") Utils.setThemeIcon(self.mImportTileset, "document-import") Utils.setThemeIcon(self.mExportTileset, "document-export") Utils.setThemeIcon(self.mPropertiesTileset, "document-properties") Utils.setThemeIcon(self.mDeleteTileset, "edit-delete") Utils.setThemeIcon(self.mAddTiles, "add") Utils.setThemeIcon(self.mRemoveTiles, "remove") self.mNewTileset.triggered.connect(self.newTileset) self.mImportTileset.triggered.connect(self.importTileset) self.mExportTileset.triggered.connect(self.exportTileset) self.mPropertiesTileset.triggered.connect(self.editTilesetProperties) self.mDeleteTileset.triggered.connect(self.removeTileset) self.mEditTerrain.triggered.connect(self.editTerrain) self.mAddTiles.triggered.connect(self.addTiles) self.mRemoveTiles.triggered.connect(self.removeTiles) self.mToolBar.addAction(self.mNewTileset) self.mToolBar.setIconSize(QSize(16, 16)) self.mToolBar.addAction(self.mImportTileset) self.mToolBar.addAction(self.mExportTileset) self.mToolBar.addAction(self.mPropertiesTileset) self.mToolBar.addAction(self.mDeleteTileset) self.mToolBar.addAction(self.mEditTerrain) self.mToolBar.addAction(self.mAddTiles) self.mToolBar.addAction(self.mRemoveTiles) self.mZoomable = Zoomable(self) self.mZoomComboBox = QComboBox() self.mZoomable.connectToComboBox(self.mZoomComboBox) horizontal.addWidget(self.mZoomComboBox) self.mViewStack.currentChanged.connect(self.updateCurrentTiles) TilesetManager.instance().tilesetChanged.connect(self.tilesetChanged) DocumentManager.instance().documentAboutToClose.connect( self.documentAboutToClose) self.mTilesetMenuButton.setMenu(self.mTilesetMenu) self.mTilesetMenu.aboutToShow.connect(self.refreshTilesetMenu) self.setWidget(w) self.retranslateUi() self.setAcceptDrops(True) self.updateActions()
def pointsOnEllipse(*args): l = len(args) if l==2: a, b = args return pointsOnLine(a.x(), a.y(), b.x(), b.y()) elif l==4: x0, y0, x1, y1 = args ret = QVector() ellipseError = 0 if x0 > x1: radiusX = x0 - x1 else: radiusX = x1 - x0 if y0 > y1: radiusY = y0 - y1 else: radiusY = y1 - y0 if (radiusX == 0 and radiusY == 0): return ret twoXSquare = 2 * radiusX * radiusX twoYSquare = 2 * radiusY * radiusY x = radiusX y = 0 xChange = radiusY * radiusY * (1 - 2 * radiusX) yChange = radiusX * radiusX ellipseError = 0 stoppingX = twoYSquare*radiusX stoppingY = 0 while (stoppingX >= stoppingY): ret += QPoint(x0 + x, y0 + y) ret += QPoint(x0 - x, y0 + y) ret += QPoint(x0 + x, y0 - y) ret += QPoint(x0 - x, y0 - y) y += 1 stoppingY += twoXSquare ellipseError += yChange yChange += twoXSquare if ((2 * ellipseError + xChange) > 0): x -= 1 stoppingX -= twoYSquare ellipseError += xChange xChange += twoYSquare x = 0 y = radiusY xChange = radiusY * radiusY yChange = radiusX * radiusX * (1 - 2 * radiusY) ellipseError = 0 stoppingX = 0 stoppingY = twoXSquare * radiusY while (stoppingX <= stoppingY): ret += QPoint(x0 + x, y0 + y) ret += QPoint(x0 - x, y0 + y) ret += QPoint(x0 + x, y0 - y) ret += QPoint(x0 - x, y0 - y) x += 1 stoppingX += twoYSquare ellipseError += xChange xChange += twoYSquare if ((2 * ellipseError + yChange) > 0): y -= 1 stoppingY -= twoXSquare ellipseError += yChange yChange += twoXSquare return ret
def recalculateTerrainDistances(self): # some fancy macros which can search for a value in each byte of a word simultaneously def hasZeroByte(dword): return (dword - 0x01010101) & ~dword & 0x80808080 def hasByteEqualTo(dword, value): return hasZeroByte(dword ^ int(~0/255 * value)) # Terrain distances are the number of transitions required before one terrain may meet another # Terrains that have no transition path have a distance of -1 for i in range(self.terrainCount()): type = self.terrain(i) distance = QVector() for _x in range(self.terrainCount() + 1): distance.append(-1) # Check all tiles for transitions to other terrain types for j in range(self.tileCount()): t = self.tileAt(j) if (not hasByteEqualTo(t.terrain(), i)): continue # This tile has transitions, add the transitions as neightbours (distance 1) tl = t.cornerTerrainId(0) tr = t.cornerTerrainId(1) bl = t.cornerTerrainId(2) br = t.cornerTerrainId(3) # Terrain on diagonally opposite corners are not actually a neighbour if (tl == i or br == i): distance[tr + 1] = 1 distance[bl + 1] = 1 if (tr == i or bl == i): distance[tl + 1] = 1 distance[br + 1] = 1 # terrain has at least one tile of its own type distance[i + 1] = 0 type.setTransitionDistances(distance) # Calculate indirect transition distances bNewConnections = False # Repeat while we are still making new connections (could take a # number of iterations for distant terrain types to connect) while bNewConnections: bNewConnections = False # For each combination of terrain types for i in range(self.terrainCount()): t0 = self.terrain(i) for j in range(self.terrainCount()): if (i == j): continue t1 = self.terrain(j) # Scan through each terrain type, and see if we have any in common for t in range(-1, self.terrainCount()): d0 = t0.transitionDistance(t) d1 = t1.transitionDistance(t) if (d0 == -1 or d1 == -1): continue # We have cound a common connection d = t0.transitionDistance(j) # If the new path is shorter, record the new distance if (d == -1 or d0 + d1 < d): d = d0 + d1 t0.setTransitionDistance(j, d) t1.setTransitionDistance(i, d) # We're making progress, flag for another iteration... bNewConnections = True
def __init__(self, parent): super().__init__(parent) self.mObjectTypes = QVector()
def fillRegion(layer, fillOrigin): # Create that region that will hold the fill fillRegion = QRegion() # Silently quit if parameters are unsatisfactory if (not layer.contains(fillOrigin)): return fillRegion # Cache cell that we will match other cells against matchCell = layer.cellAt(fillOrigin) # Grab map dimensions for later use. layerWidth = layer.width() layerHeight = layer.height() layerSize = layerWidth * layerHeight # Create a queue to hold cells that need filling fillPositions = QList() fillPositions.append(fillOrigin) # Create an array that will store which cells have been processed # This is faster than checking if a given cell is in the region/list processedCellsVec = QVector() for i in range(layerSize): processedCellsVec.append(0xff) processedCells = processedCellsVec # Loop through queued positions and fill them, while at the same time # checking adjacent positions to see if they should be added while (not fillPositions.empty()): currentPoint = fillPositions.takeFirst() startOfLine = currentPoint.y() * layerWidth # Seek as far left as we can left = currentPoint.x() while (left > 0 and layer.cellAt(left - 1, currentPoint.y()) == matchCell): left -= 1 # Seek as far right as we can right = currentPoint.x() while (right + 1 < layerWidth and layer.cellAt(right + 1, currentPoint.y()) == matchCell): right += 1 # Add cells between left and right to the region fillRegion += QRegion(left, currentPoint.y(), right - left + 1, 1) # Add cell strip to processed cells for i in range(startOfLine + left, right + startOfLine, 1): processedCells[i] = 1 # These variables cache whether the last cell was added to the queue # or not as an optimization, since adjacent cells on the x axis # do not need to be added to the queue. lastAboveCell = False lastBelowCell = False # Loop between left and right and check if cells above or # below need to be added to the queue for x in range(left, right+1): fillPoint = QPoint(x, currentPoint.y()) # Check cell above if (fillPoint.y() > 0): aboveCell = QPoint(fillPoint.x(), fillPoint.y() - 1) if (not processedCells[aboveCell.y() * layerWidth + aboveCell.x()] and layer.cellAt(aboveCell) == matchCell): # Do not add the above cell to the queue if its # x-adjacent cell was added. if (not lastAboveCell): fillPositions.append(aboveCell) lastAboveCell = True else: lastAboveCell = False processedCells[aboveCell.y() * layerWidth + aboveCell.x()] = 1 # Check cell below if (fillPoint.y() + 1 < layerHeight): belowCell = QPoint(fillPoint.x(), fillPoint.y() + 1) if (not processedCells[belowCell.y() * layerWidth + belowCell.x()] and layer.cellAt(belowCell) == matchCell): # Do not add the below cell to the queue if its # x-adjacent cell was added. if (not lastBelowCell): fillPositions.append(belowCell) lastBelowCell = True else: lastBelowCell = False processedCells[belowCell.y() * layerWidth + belowCell.x()] = 1 return fillRegion
def read(self, fileName): # Read data. file = QFile(fileName) if (not file.open(QIODevice.ReadOnly)): self.mError = self.tr("Cannot open Replica Island map file!") return 0 _in = QDataStream(file) _in.setByteOrder(QDataStream.LittleEndian) _in.setFloatingPointPrecision(QDataStream.SinglePrecision) # Parse file header. mapSignature = _in.readUInt8() layerCount = _in.readUInt8() backgroundIndex = _in.readUInt8() if (_in.status() == QDataStream.ReadPastEnd or mapSignature != 96): self.mError = self.tr("Can't parse file header!") return 0 # Create our map, setting width and height to 0 until we load a layer. map = Map(Map.Orientation.Orthogonal, 0, 0, 32, 32) map.setProperty("background_index", QString.number(backgroundIndex)) # Load our Tilesets. typeTilesets = QVector() tileIndexTilesets = QVector() self.loadTilesetsFromResources(map, typeTilesets, tileIndexTilesets) # Load each of our layers. for i in range(layerCount): # Parse layer header. _type = _in.readUInt8() tileIndex = _in.readUInt8() scrollSpeed = _in.readFloat() levelSignature = _in.readUInt8() width = _in.readUInt32() height = _in.readUInt32() if (_in.status() == QDataStream.ReadPastEnd or levelSignature != 42): self.mError = self.tr("Can't parse layer header!") return 0 # Make sure our width and height are consistent. if (map.width() == 0): map.setWidth(width) if (map.height() == 0): map.setHeight(height) if (map.width() != width or map.height() != height): self.mError = self.tr("Inconsistent layer sizes!") return 0 # Create a layer object. layer = TileLayer(self.layerTypeToName(_type), 0, 0, width, height) layer.setProperty("type", QString.number(_type)) layer.setProperty("tile_index", QString.number(tileIndex)) layer.setProperty("scroll_speed", QString.number(scrollSpeed, 'f')) map.addLayer(layer) # Look up the tileset for this layer. tileset = tilesetForLayer(_type, tileIndex, typeTilesets, tileIndexTilesets) # Read our tile data all at once. #tileData = QByteArray(width*height, b'\x00') bytesNeeded = width * height tileData = _in.readRawData(bytesNeeded) bytesRead = len(tileData) if (bytesRead != bytesNeeded): self.mError = self.tr("File ended in middle of layer!") return 0 i = 0 # Add the tiles to our layer. for y in range(0, height): for x in range(0, width): tile_id = tileData[i] & 0xff i += 1 if (tile_id != 255): tile = tileset.tileAt(tile_id) layer.setCell(x, y, Cell(tile)) # Make sure we read the entire *.bin file. if (_in.status() != QDataStream.Ok or not _in.atEnd()): self.mError = self.tr("Unexpected data at end of file!") return 0 return map
class Tile(Object): def __init__(self, *args): super().__init__(Object.TileType) l = len(args) if l==3: image, id, tileset = args self.mImageSource = QString() elif l==4: image, imageSource, id, tileset = args self.mImageSource = imageSource self.mId = id self.mTileset = tileset self.mImage = image self.mTerrain = 0xffffffff self.mProbability = 1.0 self.mObjectGroup = None self.mFrames = QVector() self.mCurrentFrameIndex = 0 self.mUnusedTime = 0 def __del__(self): del self.mObjectGroup ## # Returns the tileset that this tile is part of as a shared pointer. ## def sharedTileset(self): return self.mTileset.sharedPointer() ## # Returns ID of this tile within its tileset. ## def id(self): return self.mId ## # Returns the tileset that this tile is part of. ## def tileset(self): return self.mTileset ## # Returns the image of this tile. ## def image(self): return QPixmap(self.mImage) ## # Returns the image for rendering this tile, taking into account tile # animations. ## def currentFrameImage(self): if (self.isAnimated()): frame = self.mFrames.at(self.mCurrentFrameIndex) return self.mTileset.tileAt(frame.tileId).image() else: return QPixmap(self.mImage) ## # Returns the drawing offset of the tile (in pixels). ## def offset(self): return self.mTileset.tileOffset() ## # Sets the image of this tile. ## def setImage(self, image): self.mImage = image ## # Returns the file name of the external image that represents this tile. # When this tile doesn't refer to an external image, an empty string is # returned. ## def imageSource(self): return self.mImageSource ## # Returns the file name of the external image that represents this tile. # When this tile doesn't refer to an external image, an empty string is # returned. ## def setImageSource(self, imageSource): self.mImageSource = imageSource ## # Returns the width of this tile. ## def width(self): return self.mImage.width() ## # Returns the height of this tile. ## def height(self): return self.mImage.height() ## # Returns the size of this tile. ## def size(self): return self.mImage.size() ## # Returns the Terrain of a given corner. ## def terrainAtCorner(self, corner): return self.mTileset.terrain(self.cornerTerrainId(corner)) ## # Returns the terrain id at a given corner. ## def cornerTerrainId(self, corner): t = (self.terrain() >> (3 - corner)*8) & 0xFF if t == 0xFF: return -1 return t ## # Set the terrain type of a given corner. ## def setCornerTerrainId(self, corner, terrainId): self.setTerrain(setTerrainCorner(self.mTerrain, corner, terrainId)) ## # Returns the terrain for each corner of this tile. ## def terrain(self): return self.mTerrain ## # Set the terrain for each corner of the tile. ## def setTerrain(self, terrain): if (self.mTerrain == terrain): return self.mTerrain = terrain self.mTileset.markTerrainDistancesDirty() ## # Returns the probability of this terrain type appearing while painting (0-100%). ## def probability(self): return self.mProbability ## # Set the relative probability of this tile appearing while painting. ## def setProbability(self, probability): self.mProbability = probability ## # @return The group of objects associated with this tile. This is generally # expected to be used for editing collision shapes. ## def objectGroup(self): return self.mObjectGroup ## # Sets \a objectGroup to be the group of objects associated with this tile. # The Tile takes ownership over the ObjectGroup and it can't also be part of # a map. ## def setObjectGroup(self, objectGroup): if (self.mObjectGroup == objectGroup): return del self.mObjectGroup self.mObjectGroup = objectGroup ## # Swaps the object group of this tile with \a objectGroup. The tile releases # ownership over its existing object group and takes ownership over the new # one. # # @return The previous object group referenced by this tile. ## def swapObjectGroup(self, objectGroup): previousObjectGroup = self.mObjectGroup self.mObjectGroup = objectGroup return previousObjectGroup def frames(self): return self.mFrames ## # Sets the animation frames to be used by this tile. Resets any currently # running animation. ## def setFrames(self, frames): self.mFrames = frames self.mCurrentFrameIndex = 0 self.mUnusedTime = 0 def isAnimated(self): return not self.mFrames.isEmpty() def currentFrameIndex(self): return self.mCurrentFrameIndex ## # Advances this tile animation by the given amount of milliseconds. Returns # whether this caused the current tileId to change. ## def advanceAnimation(self, ms): if (not self.isAnimated()): return False self.mUnusedTime += ms frame = self.mFrames.at(self.mCurrentFrameIndex) previousTileId = frame.tileId while (frame.duration > 0 and self.mUnusedTime > frame.duration): self.mUnusedTime -= frame.duration self.mCurrentFrameIndex = (self.mCurrentFrameIndex + 1) % self.mFrames.size() frame = self.mFrames.at(self.mCurrentFrameIndex) return previousTileId != frame.tileId
class ObjectSelectionTool(AbstractObjectTool): def __init__(self, parent=None): super().__init__(self.tr("Select Objects"), QIcon(":images/22x22/tool-select-objects.png"), QKeySequence(self.tr("S")), parent) self.mSelectionRectangle = SelectionRectangle() self.mOriginIndicator = OriginIndicator() self.mMousePressed = False self.mHoveredObjectItem = None self.mClickedObjectItem = None self.mClickedRotateHandle = None self.mClickedResizeHandle = None self.mResizingLimitHorizontal = False self.mResizingLimitVertical = False self.mMode = Mode.Resize self.mAction = Action.NoAction self.mRotateHandles = [0, 0, 0, 0] self.mResizeHandles = [0, 0, 0, 0, 0, 0, 0, 0] self.mAlignPosition = QPointF() self.mMovingObjects = QVector() self.mScreenStart = QPoint() self.mStart = QPointF() self.mModifiers = 0 self.mOrigin = QPointF() for i in range(AnchorPosition.CornerAnchorCount): self.mRotateHandles[i] = RotateHandle(i) for i in range(AnchorPosition.AnchorCount): self.mResizeHandles[i] = ResizeHandle(i) def __del__(self): if self.mSelectionRectangle.scene(): self.mSelectionRectangle.scene().removeItem( self.mSelectionRectangle) if self.mOriginIndicator.scene(): self.mOriginIndicator.scene().removeItem(self.mOriginIndicator) for i in range(AnchorPosition.CornerAnchorCount): handle = self.mRotateHandles[i] scene = handle.scene() if scene: scene.removeItem(handle) self.mRotateHandles.clear() for i in range(AnchorPosition.AnchorCount): handle = self.mResizeHandles[i] scene = handle.scene() if scene: scene.removeItem(handle) self.mResizeHandles.clear() def tr(self, sourceText, disambiguation='', n=-1): return QCoreApplication.translate('ObjectSelectionTool', sourceText, disambiguation, n) def activate(self, scene): super().activate(scene) self.updateHandles() self.mapDocument().objectsChanged.connect(self.updateHandles) self.mapDocument().mapChanged.connect(self.updateHandles) scene.selectedObjectItemsChanged.connect(self.updateHandles) self.mapDocument().objectsRemoved.connect(self.objectsRemoved) if self.mOriginIndicator.scene() != scene: scene.addItem(self.mOriginIndicator) for i in range(AnchorPosition.CornerAnchorCount): handle = self.mRotateHandles[i] if handle.scene() != scene: scene.addItem(handle) for i in range(AnchorPosition.AnchorCount): handle = self.mResizeHandles[i] if handle.scene() != scene: scene.addItem(handle) def deactivate(self, scene): if self.mOriginIndicator.scene() == scene: scene.removeItem(self.mOriginIndicator) for i in range(AnchorPosition.CornerAnchorCount): handle = self.mRotateHandles[i] if handle.scene() == scene: scene.removeItem(handle) for i in range(AnchorPosition.AnchorCount): handle = self.mResizeHandles[i] if handle.scene() == scene: scene.removeItem(handle) self.mapDocument().objectsChanged.disconnect(self.updateHandles) self.mapDocument().mapChanged.disconnect(self.updateHandles) scene.selectedObjectItemsChanged.disconnect(self.updateHandles) super().deactivate(scene) def keyPressed(self, event): if (self.mAction != Action.NoAction): event.ignore() return moveBy = QPointF() x = event.key() if x == Qt.Key_Up: moveBy = QPointF(0, -1) elif x == Qt.Key_Down: moveBy = QPointF(0, 1) elif x == Qt.Key_Left: moveBy = QPointF(-1, 0) elif x == Qt.Key_Right: moveBy = QPointF(1, 0) else: super().keyPressed(event) return items = self.mapScene().selectedObjectItems() modifiers = event.modifiers() if (moveBy.isNull() or items.isEmpty() or (modifiers & Qt.ControlModifier)): event.ignore() return moveFast = modifiers & Qt.ShiftModifier snapToFineGrid = preferences.Preferences.instance().snapToFineGrid() if (moveFast): # TODO: This only makes sense for orthogonal maps moveBy.setX(moveBy.x() * self.mapDocument().map().tileWidth()) moveBy.setX(moveBy.y() * self.mapDocument().map().tileHeight()) if (snapToFineGrid): moveBy /= preferences.Preferences.instance().gridFine() undoStack = self.mapDocument().undoStack() undoStack.beginMacro(self.tr("Move %n Object(s)", "", items.size())) i = 0 for objectItem in items: object = objectItem.mapObject() oldPos = object.position() newPos = oldPos + moveBy undoStack.push( MoveMapObject(self.mapDocument(), object, newPos, oldPos)) i += 1 undoStack.endMacro() def mouseEntered(self): pass def mouseMoved(self, pos, modifiers): super().mouseMoved(pos, modifiers) # Update the hovered item (for mouse cursor) hoveredRotateHandle = None hoveredResizeHandle = None hoveredObjectItem = None view = self.mapScene().views()[0] if view: hoveredItem = self.mapScene().itemAt(pos, view.transform()) hoveredRotateHandle = None hoveredResizeHandle = None tp = type(hoveredItem) if tp == RotateHandle: hoveredRotateHandle = hoveredItem elif tp == ResizeHandle: hoveredResizeHandle = hoveredItem if (not hoveredRotateHandle and not hoveredResizeHandle): hoveredObjectItem = self.topMostObjectItemAt(pos) self.mHoveredObjectItem = hoveredObjectItem if (self.mAction == Action.NoAction and self.mMousePressed): screenPos = QCursor.pos() dragDistance = (self.mScreenStart - screenPos).manhattanLength() if (dragDistance >= QApplication.startDragDistance()): hasSelection = not self.mapScene().selectedObjectItems( ).isEmpty() # Holding Alt forces moving current selection # Holding Shift forces selection rectangle if ((self.mClickedObjectItem or (modifiers & Qt.AltModifier) and hasSelection) and not (modifiers & Qt.ShiftModifier)): self.startMoving(modifiers) elif (self.mClickedRotateHandle): self.startRotating() elif (self.mClickedResizeHandle): self.startResizing() else: self.startSelecting() x = self.mAction if x == Action.Selecting: self.mSelectionRectangle.setRectangle( QRectF(self.mStart, pos).normalized()) elif x == Action.Moving: self.updateMovingItems(pos, modifiers) elif x == Action.Rotating: self.updateRotatingItems(pos, modifiers) elif x == Action.Resizing: self.updateResizingItems(pos, modifiers) elif x == Action.NoAction: pass self.refreshCursor() def mousePressed(self, event): if (self.mAction != Action.NoAction ): # Ignore additional presses during select/move return x = event.button() if x == Qt.LeftButton: self.mMousePressed = True self.mStart = event.scenePos() self.mScreenStart = event.screenPos() clickedRotateHandle = 0 clickedResizeHandle = 0 view = findView(event) if view: clickedItem = self.mapScene().itemAt(event.scenePos(), view.transform()) clickedRotateHandle = None clickedResizeHandle = None tp = type(clickedItem) if tp == RotateHandle: clickedRotateHandle = clickedItem elif tp == ResizeHandle: clickedResizeHandle = clickedItem self.mClickedRotateHandle = clickedRotateHandle self.mClickedResizeHandle = clickedResizeHandle if (not clickedRotateHandle and not clickedResizeHandle): self.mClickedObjectItem = self.topMostObjectItemAt(self.mStart) else: super().mousePressed(event) def mouseReleased(self, event): if (event.button() != Qt.LeftButton): return x = self.mAction if x == Action.NoAction: if (not self.mClickedRotateHandle and not self.mClickedResizeHandle): # Don't change selection as a result of clicking on a handle modifiers = event.modifiers() if (self.mClickedObjectItem): selection = self.mapScene().selectedObjectItems() if (modifiers & (Qt.ShiftModifier | Qt.ControlModifier)): if (selection.contains(self.mClickedObjectItem)): selection.remove(self.mClickedObjectItem) else: selection.insert(self.mClickedObjectItem) elif (selection.contains(self.mClickedObjectItem)): # Clicking one of the selected items changes the edit mode if self.mMode == Mode.Resize: _x = Mode.Rotate else: _x = Mode.Resize self.setMode(_x) else: selection.clear() selection.insert(self.mClickedObjectItem) self.setMode(Mode.Resize) self.mapScene().setSelectedObjectItems(selection) elif (not (modifiers & Qt.ShiftModifier)): self.mapScene().setSelectedObjectItems(QSet()) elif x == Action.Selecting: self.updateSelection(event.scenePos(), event.modifiers()) self.mapScene().removeItem(self.mSelectionRectangle) self.mAction = Action.NoAction elif x == Action.Moving: self.finishMoving(event.scenePos()) elif x == Action.Rotating: self.finishRotating(event.scenePos()) elif x == Action.Resizing: self.finishResizing(event.scenePos()) self.mMousePressed = False self.mClickedObjectItem = None self.mClickedRotateHandle = None self.mClickedResizeHandle = None self.refreshCursor() def modifiersChanged(self, modifiers): self.mModifiers = modifiers self.refreshCursor() def languageChanged(self): self.setName(self.tr("Select Objects")) self.setShortcut(QKeySequence(self.tr("S"))) def updateHandles(self): if (self.mAction == Action.Moving or self.mAction == Action.Rotating or self.mAction == Action.Resizing): return objects = self.mapDocument().selectedObjects() showHandles = objects.size() > 0 if (showHandles): renderer = self.mapDocument().renderer() boundingRect = objectBounds( objects.first(), renderer, objectTransform(objects.first(), renderer)) for i in range(1, objects.size()): object = objects.at(i) boundingRect |= objectBounds(object, renderer, objectTransform(object, renderer)) topLeft = boundingRect.topLeft() topRight = boundingRect.topRight() bottomLeft = boundingRect.bottomLeft() bottomRight = boundingRect.bottomRight() center = boundingRect.center() handleRotation = 0 # If there is only one object selected, align to its orientation. if (objects.size() == 1): object = objects.first() handleRotation = object.rotation() if (resizeInPixelSpace(object)): bounds = pixelBounds(object) transform = QTransform(objectTransform(object, renderer)) topLeft = transform.map( renderer.pixelToScreenCoords_(bounds.topLeft())) topRight = transform.map( renderer.pixelToScreenCoords_(bounds.topRight())) bottomLeft = transform.map( renderer.pixelToScreenCoords_(bounds.bottomLeft())) bottomRight = transform.map( renderer.pixelToScreenCoords_(bounds.bottomRight())) center = transform.map( renderer.pixelToScreenCoords_(bounds.center())) # Ugly hack to make handles appear nicer in this case if (self.mapDocument().map().orientation() == Map.Orientation.Isometric): handleRotation += 45 else: bounds = objectBounds(object, renderer, QTransform()) transform = QTransform(objectTransform(object, renderer)) topLeft = transform.map(bounds.topLeft()) topRight = transform.map(bounds.topRight()) bottomLeft = transform.map(bounds.bottomLeft()) bottomRight = transform.map(bounds.bottomRight()) center = transform.map(bounds.center()) self.mOriginIndicator.setPos(center) self.mRotateHandles[AnchorPosition.TopLeftAnchor].setPos(topLeft) self.mRotateHandles[AnchorPosition.TopRightAnchor].setPos(topRight) self.mRotateHandles[AnchorPosition.BottomLeftAnchor].setPos( bottomLeft) self.mRotateHandles[AnchorPosition.BottomRightAnchor].setPos( bottomRight) top = (topLeft + topRight) / 2 left = (topLeft + bottomLeft) / 2 right = (topRight + bottomRight) / 2 bottom = (bottomLeft + bottomRight) / 2 self.mResizeHandles[AnchorPosition.TopAnchor].setPos(top) self.mResizeHandles[AnchorPosition.TopAnchor].setResizingOrigin( bottom) self.mResizeHandles[AnchorPosition.LeftAnchor].setPos(left) self.mResizeHandles[AnchorPosition.LeftAnchor].setResizingOrigin( right) self.mResizeHandles[AnchorPosition.RightAnchor].setPos(right) self.mResizeHandles[AnchorPosition.RightAnchor].setResizingOrigin( left) self.mResizeHandles[AnchorPosition.BottomAnchor].setPos(bottom) self.mResizeHandles[AnchorPosition.BottomAnchor].setResizingOrigin( top) self.mResizeHandles[AnchorPosition.TopLeftAnchor].setPos(topLeft) self.mResizeHandles[ AnchorPosition.TopLeftAnchor].setResizingOrigin(bottomRight) self.mResizeHandles[AnchorPosition.TopRightAnchor].setPos(topRight) self.mResizeHandles[ AnchorPosition.TopRightAnchor].setResizingOrigin(bottomLeft) self.mResizeHandles[AnchorPosition.BottomLeftAnchor].setPos( bottomLeft) self.mResizeHandles[ AnchorPosition.BottomLeftAnchor].setResizingOrigin(topRight) self.mResizeHandles[AnchorPosition.BottomRightAnchor].setPos( bottomRight) self.mResizeHandles[ AnchorPosition.BottomRightAnchor].setResizingOrigin(topLeft) for i in range(AnchorPosition.CornerAnchorCount): self.mRotateHandles[i].setRotation(handleRotation) for i in range(AnchorPosition.AnchorCount): self.mResizeHandles[i].setRotation(handleRotation) self.updateHandleVisibility() def updateHandleVisibility(self): hasSelection = not self.mapDocument().selectedObjects().isEmpty() showHandles = hasSelection and (self.mAction == Action.NoAction or self.mAction == Action.Selecting) showOrigin = hasSelection and self.mAction != Action.Moving and ( self.mMode == Mode.Rotate or self.mAction == Action.Resizing) for i in range(AnchorPosition.CornerAnchorCount): self.mRotateHandles[i].setVisible(showHandles and self.mMode == Mode.Rotate) for i in range(AnchorPosition.AnchorCount): self.mResizeHandles[i].setVisible(showHandles and self.mMode == Mode.Resize) self.mOriginIndicator.setVisible(showOrigin) def objectsRemoved(self, objects): if (self.mAction != Action.Moving and self.mAction != Action.Rotating and self.mAction != Action.Resizing): return # Abort move/rotate/resize to avoid crashing... # TODO: This should really not be allowed to happen in the first place. # since it breaks the undo history, for example. for i in range(self.mMovingObjects.size() - 1, -1, -1): object = self.mMovingObjects[i] mapObject = object.item.mapObject() if objects.contains(mapObject): # Avoid referencing the removed object self.mMovingObjects.remove(i) else: mapObject.setPosition(object.oldPosition) mapObject.setSize(object.oldSize) mapObject.setPolygon(object.oldPolygon) mapObject.setRotation(object.oldRotation) self.mapDocument().mapObjectModel().emitObjectsChanged( self.changingObjects) self.mMovingObjects.clear() def updateSelection(self, pos, modifiers): rect = QRectF(self.mStart, pos).normalized() # Make sure the rect has some contents, otherwise intersects returns False rect.setWidth(max(1.0, rect.width())) rect.setHeight(max(1.0, rect.height())) selectedItems = QSet() for item in self.mapScene().items(rect): if type(item) == MapObjectItem: selectedItems.insert(item) if (modifiers & (Qt.ControlModifier | Qt.ShiftModifier)): selectedItems |= self.mapScene().selectedObjectItems() else: self.setMode(Mode.Resize) self.mapScene().setSelectedObjectItems(selectedItems) def startSelecting(self): self.mAction = Action.Selecting self.mapScene().addItem(self.mSelectionRectangle) def startMoving(self, modifiers): # Move only the clicked item, if it was not part of the selection if (self.mClickedObjectItem and not (modifiers & Qt.AltModifier)): if (not self.mapScene().selectedObjectItems().contains( self.mClickedObjectItem)): self.mapScene().setSelectedObjectItems( QSet([self.mClickedObjectItem])) self.saveSelectionState() self.mAction = Action.Moving self.mAlignPosition = self.mMovingObjects[0].oldPosition for object in self.mMovingObjects: pos = object.oldPosition if (pos.x() < self.mAlignPosition.x()): self.mAlignPosition.setX(pos.x()) if (pos.y() < self.mAlignPosition.y()): self.mAlignPosition.setY(pos.y()) self.updateHandleVisibility() def updateMovingItems(self, pos, modifiers): renderer = self.mapDocument().renderer() diff = self.snapToGrid(pos - self.mStart, modifiers) for object in self.mMovingObjects: newPixelPos = object.oldItemPosition + diff newPos = renderer.screenToPixelCoords_(newPixelPos) mapObject = object.item.mapObject() mapObject.setPosition(newPos) self.mapDocument().mapObjectModel().emitObjectsChanged( self.changingObjects()) def finishMoving(self, pos): self.mAction = Action.NoAction self.updateHandles() if (self.mStart == pos): # Move is a no-op return undoStack = self.mapDocument().undoStack() undoStack.beginMacro( self.tr("Move %n Object(s)", "", self.mMovingObjects.size())) for object in self.mMovingObjects: undoStack.push( MoveMapObject(self.mapDocument(), object.item.mapObject(), object.oldPosition)) undoStack.endMacro() self.mMovingObjects.clear() def startRotating(self): self.mAction = Action.Rotating self.mOrigin = self.mOriginIndicator.pos() self.saveSelectionState() self.updateHandleVisibility() def updateRotatingItems(self, pos, modifiers): renderer = self.mapDocument().renderer() startDiff = self.mOrigin - self.mStart currentDiff = self.mOrigin - pos startAngle = math.atan2(startDiff.y(), startDiff.x()) currentAngle = math.atan2(currentDiff.y(), currentDiff.x()) angleDiff = currentAngle - startAngle snap = 15 * M_PI / 180 # 15 degrees in radians if (modifiers & Qt.ControlModifier): angleDiff = math.floor((angleDiff + snap / 2) / snap) * snap for object in self.mMovingObjects: mapObject = object.item.mapObject() offset = mapObject.objectGroup().offset() oldRelPos = object.oldItemPosition + offset - self.mOrigin sn = math.sin(angleDiff) cs = math.cos(angleDiff) newRelPos = QPointF(oldRelPos.x() * cs - oldRelPos.y() * sn, oldRelPos.x() * sn + oldRelPos.y() * cs) newPixelPos = self.mOrigin + newRelPos - offset newPos = renderer.screenToPixelCoords_(newPixelPos) newRotation = object.oldRotation + angleDiff * 180 / M_PI mapObject.setPosition(newPos) mapObject.setRotation(newRotation) self.mapDocument().mapObjectModel().emitObjectsChanged( self.changingObjects()) def finishRotating(self, pos): self.mAction = Action.NoAction self.updateHandles() if (self.mStart == pos): # No rotation at all return undoStack = self.mapDocument().undoStack() undoStack.beginMacro( self.tr("Rotate %n Object(s)", "", self.mMovingObjects.size())) for object in self.mMovingObjects: mapObject = object.item.mapObject() undoStack.push( MoveMapObject(self.mapDocument(), mapObject, object.oldPosition)) undoStack.push( RotateMapObject(self.mapDocument(), mapObject, object.oldRotation)) undoStack.endMacro() self.mMovingObjects.clear() def startResizing(self): self.mAction = Action.Resizing self.mOrigin = self.mOriginIndicator.pos() self.mResizingLimitHorizontal = self.mClickedResizeHandle.resizingLimitHorizontal( ) self.mResizingLimitVertical = self.mClickedResizeHandle.resizingLimitVertical( ) self.mStart = self.mClickedResizeHandle.pos() self.saveSelectionState() self.updateHandleVisibility() def updateResizingItems(self, pos, modifiers): renderer = self.mapDocument().renderer() resizingOrigin = self.mClickedResizeHandle.resizingOrigin() if (modifiers & Qt.ShiftModifier): resizingOrigin = self.mOrigin self.mOriginIndicator.setPos(resizingOrigin) ## Alternative toggle snap modifier, since Control is taken by the preserve # aspect ratio option. ## snapHelper = SnapHelper(renderer) if (modifiers & Qt.AltModifier): snapHelper.toggleSnap() pixelPos = renderer.screenToPixelCoords_(pos) snapHelper.snap(pixelPos) snappedScreenPos = renderer.pixelToScreenCoords_(pixelPos) diff = snappedScreenPos - resizingOrigin startDiff = self.mStart - resizingOrigin if (self.mMovingObjects.size() == 1): ## For single items the resizing is performed in object space in order # to handle different scaling on X and Y axis as well as to improve # handling of 0-sized objects. ## self.updateResizingSingleItem(resizingOrigin, snappedScreenPos, modifiers) return ## Calculate the scaling factor. Minimum is 1% to protect against making # everything 0-sized and non-recoverable (it's still possibly to run into # problems by repeatedly scaling down to 1%, but that's asking for it) ## scale = 0.0 if (self.mResizingLimitHorizontal): scale = max(0.01, diff.y() / startDiff.y()) elif (self.mResizingLimitVertical): scale = max(0.01, diff.x() / startDiff.x()) else: scale = min(max(0.01, diff.x() / startDiff.x()), max(0.01, diff.y() / startDiff.y())) if not math.isfinite(scale): scale = 1 for object in self.mMovingObjects: mapObject = object.item.mapObject() offset = mapObject.objectGroup().offset() oldRelPos = object.oldItemPosition + offset - resizingOrigin scaledRelPos = QPointF(oldRelPos.x() * scale, oldRelPos.y() * scale) newScreenPos = resizingOrigin + scaledRelPos - offset newPos = renderer.screenToPixelCoords_(newScreenPos) origSize = object.oldSize newSize = QSizeF(origSize.width() * scale, origSize.height() * scale) if (mapObject.polygon().isEmpty() == False): # For polygons, we have to scale in object space. rotation = object.item.rotation() * M_PI / -180 sn = math.sin(rotation) cs = math.cos(rotation) oldPolygon = object.oldPolygon newPolygon = QPolygonF(oldPolygon.size()) for n in range(oldPolygon.size()): oldPoint = QPointF(oldPolygon[n]) rotPoint = QPointF(oldPoint.x() * cs + oldPoint.y() * sn, oldPoint.y() * cs - oldPoint.x() * sn) scaledPoint = QPointF(rotPoint.x() * scale, rotPoint.y() * scale) newPoint = QPointF( scaledPoint.x() * cs - scaledPoint.y() * sn, scaledPoint.y() * cs + scaledPoint.x() * sn) newPolygon[n] = newPoint mapObject.setPolygon(newPolygon) mapObject.setSize(newSize) mapObject.setPosition(newPos) self.mapDocument().mapObjectModel().emitObjectsChanged( self.changingObjects()) def updateResizingSingleItem(self, resizingOrigin, screenPos, modifiers): renderer = self.mapDocument().renderer() object = self.mMovingObjects.first() mapObject = object.item.mapObject() ## The resizingOrigin, screenPos and mStart are affected by the ObjectGroup # offset. We will un-apply it to these variables since the resize for # single items happens in local coordinate space. ## offset = mapObject.objectGroup().offset() ## These transformations undo and redo the object rotation, which is always # applied in screen space. ## unrotate = rotateAt(object.oldItemPosition, -object.oldRotation) rotate = rotateAt(object.oldItemPosition, object.oldRotation) origin = (resizingOrigin - offset) * unrotate pos = (screenPos - offset) * unrotate start = (self.mStart - offset) * unrotate oldPos = object.oldItemPosition ## In order for the resizing to work somewhat sanely in isometric mode, # the resizing is performed in pixel space except for tile objects, which # are not affected by isometric projection apart from their position. ## pixelSpace = resizeInPixelSpace(mapObject) preserveAspect = modifiers & Qt.ControlModifier if (pixelSpace): origin = renderer.screenToPixelCoords_(origin) pos = renderer.screenToPixelCoords_(pos) start = renderer.screenToPixelCoords_(start) oldPos = object.oldPosition newPos = oldPos newSize = object.oldSize ## In case one of the anchors was used as-is, the desired size can be # derived directly from the distance from the origin for rectangle # and ellipse objects. This allows scaling up a 0-sized object without # dealing with infinite scaling factor issues. # # For obvious reasons this can't work on polygons or polylines, nor when # preserving the aspect ratio. ## if (self.mClickedResizeHandle.resizingOrigin() == resizingOrigin and (mapObject.shape() == MapObject.Rectangle or mapObject.shape() == MapObject.Ellipse) and not preserveAspect): newBounds = QRectF(newPos, newSize) newBounds = align(newBounds, mapObject.alignment()) x = self.mClickedResizeHandle.anchorPosition() if x == AnchorPosition.LeftAnchor or x == AnchorPosition.TopLeftAnchor or x == AnchorPosition.BottomLeftAnchor: newBounds.setLeft(min(pos.x(), origin.x())) elif x == AnchorPosition.RightAnchor or x == AnchorPosition.TopRightAnchor or x == AnchorPosition.BottomRightAnchor: newBounds.setRight(max(pos.x(), origin.x())) else: # nothing to do on this axis pass x = self.mClickedResizeHandle.anchorPosition() if x == AnchorPosition.TopAnchor or x == AnchorPosition.TopLeftAnchor or x == AnchorPosition.TopRightAnchor: newBounds.setTop(min(pos.y(), origin.y())) elif x == AnchorPosition.BottomAnchor or x == AnchorPosition.BottomLeftAnchor or x == AnchorPosition.BottomRightAnchor: newBounds.setBottom(max(pos.y(), origin.y())) else: # nothing to do on this axis pass newBounds = unalign(newBounds, mapObject.alignment()) newSize = newBounds.size() newPos = newBounds.topLeft() else: relPos = pos - origin startDiff = start - origin try: newx = relPos.x() / startDiff.x() except: newx = 0 try: newy = relPos.y() / startDiff.y() except: newy = 0 scalingFactor = QSizeF(max(0.01, newx), max(0.01, newy)) if not math.isfinite(scalingFactor.width()): scalingFactor.setWidth(1) if not math.isfinite(scalingFactor.height()): scalingFactor.setHeight(1) if (self.mResizingLimitHorizontal): if preserveAspect: scalingFactor.setWidth(scalingFactor.height()) else: scalingFactor.setWidth(1) elif (self.mResizingLimitVertical): if preserveAspect: scalingFactor.setHeight(scalingFactor.width()) else: scalingFactor.setHeight(1) elif (preserveAspect): scale = min(scalingFactor.width(), scalingFactor.height()) scalingFactor.setWidth(scale) scalingFactor.setHeight(scale) oldRelPos = oldPos - origin newPos = origin + QPointF(oldRelPos.x() * scalingFactor.width(), oldRelPos.y() * scalingFactor.height()) newSize.setWidth(newSize.width() * scalingFactor.width()) newSize.setHeight(newSize.height() * scalingFactor.height()) if (not object.oldPolygon.isEmpty()): newPolygon = QPolygonF(object.oldPolygon.size()) for n in range(object.oldPolygon.size()): point = object.oldPolygon[n] newPolygon[n] = QPointF(point.x() * scalingFactor.width(), point.y() * scalingFactor.height()) mapObject.setPolygon(newPolygon) if (pixelSpace): newPos = renderer.pixelToScreenCoords_(newPos) newPos = renderer.screenToPixelCoords_(newPos * rotate) mapObject.setSize(newSize) mapObject.setPosition(newPos) self.mapDocument().mapObjectModel().emitObjectsChanged( self.changingObjects()) def finishResizing(self, pos): self.mAction = Action.NoAction self.updateHandles() if (self.mStart == pos): # No scaling at all return undoStack = self.mapDocument().undoStack() undoStack.beginMacro( self.tr("Resize %n Object(s)", "", self.mMovingObjects.size())) for object in self.mMovingObjects: mapObject = object.item.mapObject() undoStack.push( MoveMapObject(self.mapDocument(), mapObject, object.oldPosition)) undoStack.push( ResizeMapObject(self.mapDocument(), mapObject, object.oldSize)) if (not object.oldPolygon.isEmpty()): undoStack.push( ChangePolygon(self.mapDocument(), mapObject, object.oldPolygon)) undoStack.endMacro() self.mMovingObjects.clear() def setMode(self, mode): if (self.mMode != mode): self.mMode = mode self.updateHandles() def saveSelectionState(self): self.mMovingObjects.clear() # Remember the initial state before moving, resizing or rotating for item in self.mapScene().selectedObjectItems(): mapObject = item.mapObject() object = MovingObject() object.item = item object.oldItemPosition = item.pos() object.oldPosition = mapObject.position() object.oldSize = mapObject.size() object.oldPolygon = mapObject.polygon() object.oldRotation = mapObject.rotation() self.mMovingObjects.append(object) def refreshCursor(self): cursorShape = Qt.ArrowCursor if self.mAction == Action.NoAction: hasSelection = not self.mapScene().selectedObjectItems().isEmpty() if ((self.mHoveredObjectItem or ((self.mModifiers & Qt.AltModifier) and hasSelection)) and not (self.mModifiers & Qt.ShiftModifier)): cursorShape = Qt.SizeAllCursor elif self.mAction == Action.Moving: cursorShape = Qt.SizeAllCursor if self.cursor.shape != cursorShape: self.setCursor(cursorShape) def snapToGrid(self, diff, modifiers): renderer = self.mapDocument().renderer() snapHelper = SnapHelper(renderer, modifiers) if (snapHelper.snaps()): alignScreenPos = renderer.pixelToScreenCoords_(self.mAlignPosition) newAlignScreenPos = alignScreenPos + diff newAlignPixelPos = renderer.screenToPixelCoords_(newAlignScreenPos) snapHelper.snap(newAlignPixelPos) return renderer.pixelToScreenCoords_( newAlignPixelPos) - alignScreenPos return diff def changingObjects(self): changingObjects = QList() for movingObject in self.mMovingObjects: changingObjects.append(movingObject.item.mapObject()) return changingObjects
def __init__(self): self.mCurrentProgramName = QString() self.mOptions = QVector() self.mShowHelp = False self.mLongestArgument = 0 self.mFilesToOpen = QStringList()
class Zoomable(QObject): scaleChanged = pyqtSignal(float) def __init__(self, parent=None): super().__init__(parent) self.mScale = 1 self.mZoomFactors = QVector() self.mGestureStartScale = 0 self.mComboBox = None self.mComboRegExp = QRegExp("^\\s*(\\d+)\\s*%?\\s*$") self.mComboValidator = None for i in range(zoomFactorCount): self.mZoomFactors.append(zoomFactors[i]) def setScale(self, scale): if (scale == self.mScale): return self.mScale = scale self.syncComboBox() self.scaleChanged.emit(self.mScale) def scale(self): return self.mScale def canZoomIn(self): return self.mScale < self.mZoomFactors.last() def canZoomOut(self): return self.mScale > self.mZoomFactors.first() ## # Changes the current scale based on the given mouse wheel \a delta. # # For convenience, the delta is assumed to be in the same units as # QWheelEvent.delta, which is the distance that the wheel is rotated, # in eighths of a degree. ## def handleWheelDelta(self, delta): if (delta <= -120): self.zoomOut() elif (delta >= 120): self.zoomIn() else: # We're dealing with a finer-resolution mouse. Allow it to have finer # control over the zoom level. factor = 1 + 0.3 * qAbs(delta / 8 / 15) if (delta < 0): factor = 1 / factor scale = qBound(self.mZoomFactors.first(), self.mScale * factor, self.mZoomFactors.back()) # Round to at most four digits after the decimal point self.setScale(math.floor(scale * 10000 + 0.5) / 10000) ## # Changes the current scale based on the given pinch gesture. ## def handlePinchGesture(self, pinch): if (not (pinch.changeFlags() & QPinchGesture.ScaleFactorChanged)): return x = pinch.state() if x == Qt.NoGesture: pass elif x == Qt.GestureStarted: self.mGestureStartScale = self.mScale # fall through elif x == Qt.GestureUpdated: factor = pinch.scaleFactor() scale = qBound(self.mZoomFactors.first(), self.mGestureStartScale * factor, self.mZoomFactors.back()) self.setScale(math.floor(scale * 10000 + 0.5) / 10000) elif x == Qt.GestureFinished: pass elif x == Qt.GestureCanceled: pass ## # Returns whether images should be smoothly transformed when drawn at the # current scale. This is the case when the scale is not 1 and smaller than # 2. ## def smoothTransform(self): return self.mScale != 1.0 and self.mScale < 2.0 def setZoomFactors(self, factors): self.mZoomFactors = factors def connectToComboBox(self, comboBox): if (self.mComboBox): self.mComboBox.disconnect() if (self.mComboBox.lineEdit()): self.mComboBox.lineEdit().disconnect() self.mComboBox.setValidator(None) self.mComboBox = comboBox if type(comboBox) is QComboBox: self.mComboBox.clear() for scale in self.mZoomFactors: self.mComboBox.addItem(scaleToString(scale), scale) self.syncComboBox() self.mComboBox.activated.connect(self.comboActivated) self.mComboBox.setEditable(True) self.mComboBox.setInsertPolicy(QComboBox.NoInsert) self.mComboBox.lineEdit().editingFinished.connect(self.comboEdited) if (not self.mComboValidator): self.mComboValidator = QRegExpValidator( self.mComboRegExp, self) self.mComboBox.setValidator(self.mComboValidator) def zoomIn(self): for scale in self.mZoomFactors: if (scale > self.mScale): self.setScale(scale) break def zoomOut(self): for i in range(self.mZoomFactors.count() - 1, -1, -1): if (self.mZoomFactors[i] < self.mScale): self.setScale(self.mZoomFactors[i]) break def resetZoom(self): self.setScale(1) def comboActivated(self, index): self.setScale(self.mComboBox.itemData(index)) def comboEdited(self): pos = self.mComboRegExp.indexIn(self.mComboBox.currentText()) pos != -1 scale = qBound(self.mZoomFactors.first(), Float(self.mComboRegExp.cap(1)) / 100.0, self.mZoomFactors.last()) self.setScale(scale) def syncComboBox(self): if (not self.mComboBox): return index = self.mComboBox.findData(self.mScale) # For a custom scale, the current index must be set to -1 self.mComboBox.setCurrentIndex(index) self.mComboBox.setEditText(scaleToString(self.mScale))
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()
def __init__(self, painter): self.mPainter = painter self.mTile = None self.mIsOpenGL = hasOpenGLEngine(painter) self.mFragments = QVector()
class TileLayer(Layer): ## # Constructor. ## def __init__(self, name, x, y, width, height): super().__init__(Layer.TileLayerType, name, x, y, width, height) self.mMaxTileSize = QSize(0, 0) self.mGrid = QVector() for i in range(width * height): self.mGrid.append(Cell()) self.mOffsetMargins = QMargins() def __iter__(self): return self.mGrid.__iter__() ## # Returns the maximum tile size of this layer. ## def maxTileSize(self): return self.mMaxTileSize ## # Returns the margins that have to be taken into account while drawing # this tile layer. The margins depend on the maximum tile size and the # offset applied to the tiles. ## def drawMargins(self): return QMargins( self.mOffsetMargins.left(), self.mOffsetMargins.top() + self.mMaxTileSize.height(), self.mOffsetMargins.right() + self.mMaxTileSize.width(), self.mOffsetMargins.bottom()) ## # Recomputes the draw margins. Needed after the tile offset of a tileset # has changed for example. # # Generally you want to call Map.recomputeDrawMargins instead. ## def recomputeDrawMargins(self): maxTileSize = QSize(0, 0) offsetMargins = QMargins() i = 0 while (i < self.mGrid.size()): cell = self.mGrid.at(i) tile = cell.tile if tile: size = tile.size() if (cell.flippedAntiDiagonally): size.transpose() offset = tile.offset() maxTileSize = maxSize(size, maxTileSize) offsetMargins = maxMargins( QMargins(-offset.x(), -offset.y(), offset.x(), offset.y()), offsetMargins) i += 1 self.mMaxTileSize = maxTileSize self.mOffsetMargins = offsetMargins if (self.mMap): self.mMap.adjustDrawMargins(self.drawMargins()) ## # Returns whether (x, y) is inside this map layer. ## def contains(self, *args): l = len(args) if l == 2: x, y = args return x >= 0 and y >= 0 and x < self.mWidth and y < self.mHeight elif l == 1: point = args[0] return self.contains(point.x(), point.y()) ## # Calculates the region of cells in this tile layer for which the given # \a condition returns True. ## def region(self, *args): l = len(args) if l == 1: condition = args[0] region = QRegion() for y in range(self.mHeight): for x in range(self.mWidth): if (condition(self.cellAt(x, y))): rangeStart = x x += 1 while (x <= self.mWidth): if (x == self.mWidth or not condition(self.cellAt(x, y))): rangeEnd = x region += QRect(rangeStart + self.mX, y + self.mY, rangeEnd - rangeStart, 1) break x += 1 return region elif l == 0: ## # Calculates the region occupied by the tiles of this layer. Similar to # Layer.bounds(), but leaves out the regions without tiles. ## return self.region(lambda cell: not cell.isEmpty()) ## # Returns a read-only reference to the cell at the given coordinates. The # coordinates have to be within this layer. ## def cellAt(self, *args): l = len(args) if l == 2: x, y = args return self.mGrid.at(x + y * self.mWidth) elif l == 1: point = args[0] return self.cellAt(point.x(), point.y()) ## # Sets the cell at the given coordinates. ## def setCell(self, x, y, cell): if (cell.tile): size = cell.tile.size() if (cell.flippedAntiDiagonally): size.transpose() offset = cell.tile.offset() self.mMaxTileSize = maxSize(size, self.mMaxTileSize) self.mOffsetMargins = maxMargins( QMargins(-offset.x(), -offset.y(), offset.x(), offset.y()), self.mOffsetMargins) if (self.mMap): self.mMap.adjustDrawMargins(self.drawMargins()) self.mGrid[x + y * self.mWidth] = cell ## # Returns a copy of the area specified by the given \a region. The # caller is responsible for the returned tile layer. ## def copy(self, *args): l = len(args) if l == 1: region = args[0] if type(region) != QRegion: region = QRegion(region) area = region.intersected(QRect(0, 0, self.width(), self.height())) bounds = region.boundingRect() areaBounds = area.boundingRect() offsetX = max(0, areaBounds.x() - bounds.x()) offsetY = max(0, areaBounds.y() - bounds.y()) copied = TileLayer(QString(), 0, 0, bounds.width(), bounds.height()) for rect in area.rects(): for x in range(rect.left(), rect.right() + 1): for y in range(rect.top(), rect.bottom() + 1): copied.setCell(x - areaBounds.x() + offsetX, y - areaBounds.y() + offsetY, self.cellAt(x, y)) return copied elif l == 4: x, y, width, height = args return self.copy(QRegion(x, y, width, height)) ## # Merges the given \a layer onto this layer at position \a pos. Parts that # fall outside of this layer will be lost and empty tiles in the given # layer will have no effect. ## def merge(self, pos, layer): # Determine the overlapping area area = QRect(pos, QSize(layer.width(), layer.height())) area &= QRect(0, 0, self.width(), self.height()) for y in range(area.top(), area.bottom() + 1): for x in range(area.left(), area.right() + 1): cell = layer.cellAt(x - pos.x(), y - pos.y()) if (not cell.isEmpty()): self.setCell(x, y, cell) ## # Removes all cells in the specified region. ## def erase(self, area): emptyCell = Cell() for rect in area.rects(): for x in range(rect.left(), rect.right() + 1): for y in range(rect.top(), rect.bottom() + 1): self.setCell(x, y, emptyCell) ## # Sets the cells starting at the given position to the cells in the given # \a tileLayer. Parts that fall outside of this layer will be ignored. # # When a \a mask is given, only cells that fall within this mask are set. # The mask is applied in local coordinates. ## def setCells(self, x, y, layer, mask=QRegion()): # Determine the overlapping area area = QRegion(QRect(x, y, layer.width(), layer.height())) area &= QRect(0, 0, self.width(), self.height()) if (not mask.isEmpty()): area &= mask for rect in area.rects(): for _x in range(rect.left(), rect.right() + 1): for _y in range(rect.top(), rect.bottom() + 1): self.setCell(_x, _y, layer.cellAt(_x - x, _y - y)) ## # Flip this tile layer in the given \a direction. Direction must be # horizontal or vertical. This doesn't change the dimensions of the # tile layer. ## def flip(self, direction): newGrid = QVector() for i in range(self.mWidth * self.mHeight): newGrid.append(Cell()) for y in range(self.mHeight): for x in range(self.mWidth): dest = newGrid[x + y * self.mWidth] if (direction == FlipDirection.FlipHorizontally): source = self.cellAt(self.mWidth - x - 1, y) dest = source dest.flippedHorizontally = not source.flippedHorizontally elif (direction == FlipDirection.FlipVertically): source = self.cellAt(x, self.mHeight - y - 1) dest = source dest.flippedVertically = not source.flippedVertically self.mGrid = newGrid ## # Rotate this tile layer by 90 degrees left or right. The tile positions # are rotated within the layer, and the tiles themselves are rotated. The # dimensions of the tile layer are swapped. ## def rotate(self, direction): rotateRightMask = [5, 4, 1, 0, 7, 6, 3, 2] rotateLeftMask = [3, 2, 7, 6, 1, 0, 5, 4] if direction == RotateDirection.RotateRight: rotateMask = rotateRightMask else: rotateMask = rotateLeftMask newWidth = self.mHeight newHeight = self.mWidth newGrid = QVector(newWidth * newHeight) for y in range(self.mHeight): for x in range(self.mWidth): source = self.cellAt(x, y) dest = source mask = (dest.flippedHorizontally << 2) | ( dest.flippedVertically << 1) | ( dest.flippedAntiDiagonally << 0) mask = rotateMask[mask] dest.flippedHorizontally = (mask & 4) != 0 dest.flippedVertically = (mask & 2) != 0 dest.flippedAntiDiagonally = (mask & 1) != 0 if (direction == RotateDirection.RotateRight): newGrid[x * newWidth + (self.mHeight - y - 1)] = dest else: newGrid[(self.mWidth - x - 1) * newWidth + y] = dest t = self.mMaxTileSize.width() self.mMaxTileSize.setWidth(self.mMaxTileSize.height()) self.mMaxTileSize.setHeight(t) self.mWidth = newWidth self.mHeight = newHeight self.mGrid = newGrid ## # Computes and returns the set of tilesets used by this tile layer. ## def usedTilesets(self): tilesets = QSet() i = 0 while (i < self.mGrid.size()): tile = self.mGrid.at(i).tile if tile: tilesets.insert(tile.tileset()) i += 1 return tilesets ## # Returns whether this tile layer has any cell for which the given # \a condition returns True. ## def hasCell(self, condition): i = 0 for cell in self.mGrid: if (condition(cell)): return True i += 1 return False ## # Returns whether this tile layer is referencing the given tileset. ## def referencesTileset(self, tileset): i = 0 while (i < self.mGrid.size()): tile = self.mGrid.at(i).tile if (tile and tile.tileset() == tileset): return True i += 1 return False ## # Removes all references to the given tileset. This sets all tiles on this # layer that are from the given tileset to null. ## def removeReferencesToTileset(self, tileset): i = 0 while (i < self.mGrid.size()): tile = self.mGrid.at(i).tile if (tile and tile.tileset() == tileset): self.mGrid.replace(i, Cell()) i += 1 ## # Replaces all tiles from \a oldTileset with tiles from \a newTileset. ## def replaceReferencesToTileset(self, oldTileset, newTileset): i = 0 while (i < self.mGrid.size()): tile = self.mGrid.at(i).tile if (tile and tile.tileset() == oldTileset): self.mGrid[i].tile = newTileset.tileAt(tile.id()) i += 1 ## # Resizes this tile layer to \a size, while shifting all tiles by # \a offset. ## def resize(self, size, offset): if (self.size() == size and offset.isNull()): return newGrid = QVector() for i in range(size.width() * size.height()): newGrid.append(Cell()) # Copy over the preserved part startX = max(0, -offset.x()) startY = max(0, -offset.y()) endX = min(self.mWidth, size.width() - offset.x()) endY = min(self.mHeight, size.height() - offset.y()) for y in range(startY, endY): for x in range(startX, endX): index = x + offset.x() + (y + offset.y()) * size.width() newGrid[index] = self.cellAt(x, y) self.mGrid = newGrid self.setSize(size) ## # Offsets the tiles in this layer within \a bounds by \a offset, # and optionally wraps them. # # \sa ObjectGroup.offset() ## def offsetTiles(self, offset, bounds, wrapX, wrapY): newGrid = QVector() for i in range(self.mWidth * self.mHeight): newGrid.append(Cell()) for y in range(self.mHeight): for x in range(self.mWidth): # Skip out of bounds tiles if (not bounds.contains(x, y)): newGrid[x + y * self.mWidth] = self.cellAt(x, y) continue # Get position to pull tile value from oldX = x - offset.x() oldY = y - offset.y() # Wrap x value that will be pulled from if (wrapX and bounds.width() > 0): while oldX < bounds.left(): oldX += bounds.width() while oldX > bounds.right(): oldX -= bounds.width() # Wrap y value that will be pulled from if (wrapY and bounds.height() > 0): while oldY < bounds.top(): oldY += bounds.height() while oldY > bounds.bottom(): oldY -= bounds.height() # Set the new tile if (self.contains(oldX, oldY) and bounds.contains(oldX, oldY)): newGrid[x + y * self.mWidth] = self.cellAt(oldX, oldY) else: newGrid[x + y * self.mWidth] = Cell() self.mGrid = newGrid def canMergeWith(self, other): return other.isTileLayer() def mergedWith(self, other): o = other unitedBounds = self.bounds().united(o.bounds()) offset = self.position() - unitedBounds.topLeft() merged = self.clone() merged.resize(unitedBounds.size(), offset) merged.merge(o.position() - unitedBounds.topLeft(), o) return merged ## # Returns the region where this tile layer and the given tile layer # are different. The relative positions of the layers are taken into # account. The returned region is relative to this tile layer. ## def computeDiffRegion(self, other): ret = QRegion() dx = other.x() - self.mX dy = other.y() - self.mY r = QRect(0, 0, self.width(), self.height()) r &= QRect(dx, dy, other.width(), other.height()) for y in range(r.top(), r.bottom() + 1): for x in range(r.left(), r.right() + 1): if (self.cellAt(x, y) != other.cellAt(x - dx, y - dy)): rangeStart = x while (x <= r.right() and self.cellAt(x, y) != other.cellAt(x - dx, y - dy)): x += 1 rangeEnd = x ret += QRect(rangeStart, y, rangeEnd - rangeStart, 1) return ret ## # Returns True if all tiles in the layer are empty. ## def isEmpty(self): i = 0 while (i < self.mGrid.size()): if (not self.mGrid.at(i).isEmpty()): return False i += 1 return True ## # Returns a duplicate of this TileLayer. # # \sa Layer.clone() ## def clone(self): return self.initializeClone( TileLayer(self.mName, self.mX, self.mY, self.mWidth, self.mHeight)) def begin(self): return self.mGrid.begin() def end(self): return self.mGrid.end() def initializeClone(self, clone): super().initializeClone(clone) clone.mGrid = self.mGrid clone.mMaxTileSize = self.mMaxTileSize clone.mOffsetMargins = self.mOffsetMargins return clone
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
def __init__(self): super().__init__() self.mSettings = QSettings(self) self.mObjectTypes = QVector() # Retrieve storage settings self.mSettings.beginGroup("Storage") self.mLayerDataFormat = Map.LayerDataFormat(self.intValue("LayerDataFormat", Map.LayerDataFormat.Base64Zlib.value)) self.mMapRenderOrder = Map.RenderOrder(self.intValue("MapRenderOrder", Map.RenderOrder.RightDown.value)) self.mDtdEnabled = self.boolValue("DtdEnabled") self.mReloadTilesetsOnChange = self.boolValue("ReloadTilesets", True) self.mStampsDirectory = self.stringValue("StampsDirectory") self.mSettings.endGroup() # Retrieve interface settings self.mSettings.beginGroup("Interface") self.mShowGrid = self.boolValue("ShowGrid") self.mShowTileObjectOutlines = self.boolValue("ShowTileObjectOutlines") self.mShowTileAnimations = self.boolValue("ShowTileAnimations", True) self.mSnapToGrid = self.boolValue("SnapToGrid") self.mSnapToFineGrid = self.boolValue("SnapToFineGrid") self.mGridColor = self.colorValue("GridColor", Qt.black) self.mGridFine = self.intValue("GridFine", 4) self.mObjectLineWidth = self.realValue("ObjectLineWidth", 2) self.mHighlightCurrentLayer = self.boolValue("HighlightCurrentLayer") self.mShowTilesetGrid = self.boolValue("ShowTilesetGrid", True) self.mLanguage = self.stringValue("Language") self.mUseOpenGL = self.boolValue("OpenGL") self.mObjectLabelVisibility = self.intValue("ObjectLabelVisibility", ObjectLabelVisiblity.AllObjectLabels) self.mSettings.endGroup() # Retrieve defined object types self.mSettings.beginGroup("ObjectTypes") names = self.mSettings.value("Names", QStringList()) colors = self.mSettings.value("Colors", QStringList()) self.mSettings.endGroup() count = min(len(names), len(colors)) for i in range(count): self.mObjectTypes.append(ObjectType(names[i], QColor(colors[i]))) self.mSettings.beginGroup("Automapping") self.mAutoMapDrawing = self.boolValue("WhileDrawing") self.mSettings.endGroup() self.mSettings.beginGroup("MapsDirectory") self.mMapsDirectory = self.stringValue("Current") self.mSettings.endGroup() tilesetManager = TilesetManager.instance() tilesetManager.setReloadTilesetsOnChange(self.mReloadTilesetsOnChange) tilesetManager.setAnimateTiles(self.mShowTileAnimations) # Keeping track of some usage information self.mSettings.beginGroup("Install") self.mFirstRun = QDate.fromString(self.mSettings.value("FirstRun")) self.mRunCount = self.intValue("RunCount", 0) + 1 self.mIsPatron = self.boolValue("IsPatron") if (not self.mFirstRun.isValid()): self.mFirstRun = QDate.currentDate() self.mSettings.setValue("FirstRun", self.mFirstRun.toString(Qt.ISODate)) self.mSettings.setValue("RunCount", self.mRunCount) self.mSettings.endGroup() # Retrieve startup settings self.mSettings.beginGroup("Startup") self.mOpenLastFilesOnStartup = self.boolValue("OpenLastFiles", True) self.mSettings.endGroup()
def drawPreviewLayer(self, _list): self.mPreviewLayer = None if self.mStamp.isEmpty(): return if self.mIsRandom: if self.mRandomCellPicker.isEmpty(): return paintedRegion = QRegion() for p in _list: paintedRegion += QRect(p, QSize(1, 1)) bounds = paintedRegion.boundingRect() preview = TileLayer(QString(), bounds.x(), bounds.y(), bounds.width(), bounds.height()) for p in _list: cell = self.mRandomCellPicker.pick() preview.setCell(p.x() - bounds.left(), p.y() - bounds.top(), cell) self.mPreviewLayer = preview else: self.mMissingTilesets.clear() paintedRegion = QRegion() operations = QVector() regionCache = QHash() for p in _list: variation = self.mStamp.randomVariation() self.mapDocument().unifyTilesets(variation.map, self.mMissingTilesets) stamp = variation.tileLayer() stampRegion = QRegion() if regionCache.contains(stamp): stampRegion = regionCache.value(stamp) else: stampRegion = stamp.region() regionCache.insert(stamp, stampRegion) centered = QPoint(p.x() - int(stamp.width() / 2), p.y() - int(stamp.height() / 2)) region = stampRegion.translated(centered.x(), centered.y()) if not paintedRegion.intersects(region): paintedRegion += region op = PaintOperation(centered, stamp) operations.append(op) bounds = paintedRegion.boundingRect() preview = TileLayer(QString(), bounds.x(), bounds.y(), bounds.width(), bounds.height()) for op in operations: preview.merge(op.pos - bounds.topLeft(), op.stamp) self.mPreviewLayer = preview
class AutomappingManager(QObject): ## # This signal is emited after automapping was done and an error occurred. ## errorsOccurred = pyqtSignal(bool) ## # This signal is emited after automapping was done and a warning occurred. ## warningsOccurred = pyqtSignal(bool) ## # Constructor. ## def __init__(self, parent = None): super().__init__(parent) ## # The current map document. ## self.mMapDocument = None ## # For each new file of rules a new AutoMapper is setup. In this vector we # can store all of the AutoMappers in order. ## self.mAutoMappers = QVector() ## # This tells you if the rules for the current map document were already # loaded. ## self.mLoaded = False ## # Contains all errors which occurred until canceling. # If mError is not empty, no serious result can be expected. ## self.mError = '' ## # Contains all strings, which try to explain unusual and unexpected # behavior. ## self.mWarning = QString() def __del__(self): self.cleanUp() def setMapDocument(self, mapDocument): self.cleanUp() if (self.mMapDocument): self.mMapDocument.disconnect() self.mMapDocument = mapDocument if (self.mMapDocument): self.mMapDocument.regionEdited.connect(self.autoMap) self.mLoaded = False def errorString(self): return self.mError def warningString(self): return self.mWarning ## # This triggers an automapping on the whole current map document. ## def autoMap(self, *args): l = len(args) if l == 0: if (not self.mMapDocument): return map = self.mMapDocument.Map() w = map.width() h = map.height() self.autoMapInternal(QRect(0, 0, w, h), None) elif l==2: where, touchedLayer = args if (preferences.Preferences.instance().automappingDrawing()): self.autoMapInternal(where, touchedLayer) ## # This function parses a rules file. # For each path which is a rule, (fileextension is tmx) an AutoMapper # object is setup. # # If a fileextension is txt, this file will be opened and searched for # rules again. # # @return if the loading was successful: return True if it suceeded. ## def loadFile(self, filePath): ret = True absPath = QFileInfo(filePath).path() rulesFile = QFile(filePath) if (not rulesFile.exists()): self.mError += self.tr("No rules file found at:\n%s\n"%filePath) return False if (not rulesFile.open(QIODevice.ReadOnly | QIODevice.Text)): self.mError += self.tr("Error opening rules file:\n%s\n"%filePath) return False i = QTextStream(rulesFile) line = ' ' while line != '': line = i.readLine() rulePath = line.strip() if (rulePath=='' or rulePath.startswith('#') or rulePath.startswith("//")): continue if (QFileInfo(rulePath).isRelative()): rulePath = absPath + '/' + rulePath if (not QFileInfo(rulePath).exists()): self.mError += self.tr("File not found:\n%s"%rulePath) + '\n' ret = False continue if (rulePath.lower().endswith(".tmx")): tmxFormat = TmxMapFormat() rules = tmxFormat.read(rulePath) if (not rules): self.mError += self.tr("Opening rules map failed:\n%s"%tmxFormat.errorString()) + '\n' ret = False continue tilesetManager = TilesetManager.instance() tilesetManager.addReferences(rules.tilesets()) autoMapper = None autoMapper = AutoMapper(self.mMapDocument, rules, rulePath) self.mWarning += autoMapper.warningString() error = autoMapper.errorString() if error != '': self.mAutoMappers.append(autoMapper) else: self.mError += error del autoMapper if (rulePath.lower().endswith(".txt")): if (not self.loadFile(rulePath)): ret = False return ret ## # Applies automapping to the Region \a where, considering only layer # \a touchedLayer has changed. # There will only those Automappers be used which have a rule layer # touching the \a touchedLayer # If layer is 0, all Automappers are used. ## def autoMapInternal(self, where, touchedLayer): self.mError = '' self.mWarning = '' if (not self.mMapDocument): return automatic = touchedLayer != None if (not self.mLoaded): mapPath = QFileInfo(self.mMapDocument.fileName()).path() rulesFileName = mapPath + "/rules.txt" if (self.loadFile(rulesFileName)): self.mLoaded = True else: self.errorsOccurred.emit(automatic) return passedAutoMappers = QVector() if (touchedLayer): for a in self.mAutoMappers: if (a.ruleLayerNameUsed(touchedLayer.name())): passedAutoMappers.append(a) else: passedAutoMappers = self.mAutoMappers if (not passedAutoMappers.isEmpty()): # use a pointer to the region, so each automapper can manipulate it and the # following automappers do see the impact region = QRegion(where) undoStack = self.mMapDocument.undoStack() undoStack.beginMacro(self.tr("Apply AutoMap rules")) aw = AutoMapperWrapper(self.mMapDocument, passedAutoMappers, region) undoStack.push(aw) undoStack.endMacro() for automapper in self.mAutoMappers: self.mWarning += automapper.warningString() self.mError += automapper.errorString() if self.mWarning != '': self.warningsOccurred.emit(automatic) if self.mError != '': self.errorsOccurred.emit(automatic) ## # deletes all its data structures ## def cleanUp(self): self.mAutoMappers.clear()
class AutoMapperWrapper(QUndoCommand): def __init__(self, mapDocument, autoMapper, where): super().__init__() self.mLayersAfter = QVector() self.mLayersBefore = QVector() self.mMapDocument = mapDocument map = self.mMapDocument.Map() touchedLayers = QSet() index = 0 while (index < autoMapper.size()): a = autoMapper.at(index) if (a.prepareAutoMap()): touchedLayers|= a.getTouchedTileLayers() index += 1 else: autoMapper.remove(index) for layerName in touchedLayers: layerindex = map.indexOfLayer(layerName) self.mLayersBefore (map.layerAt(layerindex).clone()) for a in autoMapper: a.autoMap(where) for layerName in touchedLayers: layerindex = map.indexOfLayer(layerName) # layerindex exists, because AutoMapper is still alive, dont check self.mLayersAfter (map.layerAt(layerindex).clone()) # reduce memory usage by saving only diffs for i in range(self.mLayersAfter.size()): before = self.mLayersBefore.at(i) after = self.mLayersAfter.at(i) diffRegion = before.computeDiffRegion(after).boundingRect() before1 = before.copy(diffRegion) after1 = after.copy(diffRegion) before1.setPosition(diffRegion.topLeft()) after1.setPosition(diffRegion.topLeft()) before1.setName(before.name()) after1.setName(after.name()) self.mLayersBefore.replace(i, before1) self.mLayersAfter.replace(i, after1) del before del after for a in autoMapper: a.cleanAll() def __del__(self): for i in self.mLayersAfter: del i for i in self.mLayersBefore: del i def undo(self): map = self.mMapDocument.Map() for layer in self.mLayersBefore: layerindex = map.indexOfLayer(layer.name()) if (layerindex != -1): self.patchLayer(layerindex, layer) def redo(self): map = self.mMapDocument.Map() for layer in self.mLayersAfter: layerindex = (map.indexOfLayer(layer.name())) if (layerindex != -1): self.patchLayer(layerindex, layer) def patchLayer(self, layerIndex, layer): map = self.mMapDocument.Map() b = layer.bounds() t = map.layerAt(layerIndex) t.setCells(b.left() - t.x(), b.top() - t.y(), layer, b.translated(-t.position())) self.mMapDocument.emitRegionChanged(b, t)
def __readTilesetTile(self, tileset): atts = self.xml.attributes() id = Int(atts.value("id")) if (id < 0): self.xml.raiseError(self.tr("Invalid tile ID: %d"%id)) return hasImage = tileset.imageSource()!='' if (hasImage and id >= tileset.tileCount()): self.xml.raiseError(self.tr("Tile ID does not exist in tileset image: %d"%id)) return if (id > tileset.tileCount()): self.xml.raiseError(self.tr("Invalid (nonconsecutive) tile ID: %d"%id)) return # For tilesets without image source, consecutive tile IDs are allowed (for # tiles with individual images) if (id == tileset.tileCount()): tileset.addTile(QPixmap()) tile = tileset.tileAt(id) # Read tile quadrant terrain ids terrain = atts.value("terrain") if terrain != '': quadrants = terrain.split(",") if (len(quadrants) == 4): for i in range(4): if quadrants[i]=='': t = -1 else: t = Int(quadrants[i]) tile.setCornerTerrainId(i, t) # Read tile probability probability = atts.value("probability") if probability != '': tile.setProbability(Float(probability)) while (self.xml.readNextStartElement()): if (self.xml.name() == "properties"): tile.mergeProperties(self.__readProperties()) elif (self.xml.name() == "image"): source = self.xml.attributes().value("source") if source != '': source = self.p.resolveReference(source, self.mPath) tileset.setTileImage(id, QPixmap.fromImage(self.__readImage()), source) elif (self.xml.name() == "objectgroup"): tile.setObjectGroup(self.__readObjectGroup()) elif (self.xml.name() == "animation"): tile.setFrames(self.__readAnimationFrames()) else: self.__readUnknownElement() # Temporary code to support TMW-style animation frame properties if (not tile.isAnimated() and tile.hasProperty("animation-frame0")): frames = QVector() i = 0 while(i>=0): frameName = "animation-frame" + str(i) delayName = "animation-delay" + str(i) if (tile.hasProperty(frameName) and tile.hasProperty(delayName)): frame = Frame() frame.tileId = tile.property(frameName) frame.duration = tile.property(delayName) * 10 frames.append(frame) else: break i += 1 tile.setFrames(frames)
class ObjectTypesModel(QAbstractTableModel): ColorRole = Qt.UserRole def __init__(self, parent): super().__init__(parent) self.mObjectTypes = QVector() def setObjectTypes(self, objectTypes): self.beginResetModel() self.mObjectTypes = objectTypes self.endResetModel() def objectTypes(self): return self.mObjectTypes def rowCount(self, parent): if parent.isValid(): _x = 0 else: _x = self.mObjectTypes.size() return _x def columnCount(self, parent): if parent.isValid(): _x = 0 else: _x = 2 return _x def headerData(self, section, orientation, role): if (orientation == Qt.Horizontal): if (role == Qt.DisplayRole): x = section if x == 0: return self.tr("Type") elif x == 1: return self.tr("Color") elif (role == Qt.TextAlignmentRole): return Qt.AlignLeft return QVariant() def data(self, index, role): # QComboBox requests data for an invalid index when the model is empty if (not index.isValid()): return QVariant() objectType = self.mObjectTypes.at(index.row()) if (role == Qt.DisplayRole or role == Qt.EditRole): if (index.column() == 0): return objectType.name if (role == ObjectTypesModel.ColorRole and index.column() == 1): return objectType.color return QVariant() def setData(self, index, value, role): if (role == Qt.EditRole and index.column() == 0): self.mObjectTypes[index.row()].name = value.strip() self.dataChanged.emit(index, index) return True return False def flags(self, index): f = super().flags(index) if (index.column() == 0): f |= Qt.ItemIsEditable return f def setObjectTypeColor(self, objectIndex, color): self.mObjectTypes[objectIndex].color = color mi = self.index(objectIndex, 1) self.dataChanged.emit(mi, mi) def removeObjectTypes(self, indexes): rows = QVector() for index in indexes: rows.append(index.row()) rows = sorted(rows) for i in range(len(rows) - 1, -1, -1): row = rows[i] self.beginRemoveRows(QModelIndex(), row, row) self.mObjectTypes.remove(row) self.endRemoveRows() def appendNewObjectType(self): self.beginInsertRows(QModelIndex(), self.mObjectTypes.size(), self.mObjectTypes.size()) self.mObjectTypes.append(ObjectType()) self.endInsertRows()
class Preferences(QObject): showGridChanged = pyqtSignal(bool) showTileObjectOutlinesChanged = pyqtSignal(bool) showTileAnimationsChanged = pyqtSignal(bool) snapToGridChanged = pyqtSignal(bool) snapToFineGridChanged = pyqtSignal(bool) gridColorChanged = pyqtSignal(QColor) gridFineChanged = pyqtSignal(int) objectLineWidthChanged = pyqtSignal(float) highlightCurrentLayerChanged = pyqtSignal(bool) showTilesetGridChanged = pyqtSignal(bool) objectLabelVisibilityChanged = pyqtSignal(int) useOpenGLChanged = pyqtSignal(bool) objectTypesChanged = pyqtSignal() mapsDirectoryChanged = pyqtSignal() stampsDirectoryChanged = pyqtSignal(str) isPatronChanged = pyqtSignal() mInstance = None ObjectTypesFile, ImageFile, ExportedFile, ExternalTileset = range(4) def __init__(self): super().__init__() self.mSettings = QSettings(self) self.mObjectTypes = QVector() # Retrieve storage settings self.mSettings.beginGroup("Storage") self.mLayerDataFormat = Map.LayerDataFormat(self.intValue("LayerDataFormat", Map.LayerDataFormat.Base64Zlib.value)) self.mMapRenderOrder = Map.RenderOrder(self.intValue("MapRenderOrder", Map.RenderOrder.RightDown.value)) self.mDtdEnabled = self.boolValue("DtdEnabled") self.mReloadTilesetsOnChange = self.boolValue("ReloadTilesets", True) self.mStampsDirectory = self.stringValue("StampsDirectory") self.mSettings.endGroup() # Retrieve interface settings self.mSettings.beginGroup("Interface") self.mShowGrid = self.boolValue("ShowGrid") self.mShowTileObjectOutlines = self.boolValue("ShowTileObjectOutlines") self.mShowTileAnimations = self.boolValue("ShowTileAnimations", True) self.mSnapToGrid = self.boolValue("SnapToGrid") self.mSnapToFineGrid = self.boolValue("SnapToFineGrid") self.mGridColor = self.colorValue("GridColor", Qt.black) self.mGridFine = self.intValue("GridFine", 4) self.mObjectLineWidth = self.realValue("ObjectLineWidth", 2) self.mHighlightCurrentLayer = self.boolValue("HighlightCurrentLayer") self.mShowTilesetGrid = self.boolValue("ShowTilesetGrid", True) self.mLanguage = self.stringValue("Language") self.mUseOpenGL = self.boolValue("OpenGL") self.mObjectLabelVisibility = self.intValue("ObjectLabelVisibility", ObjectLabelVisiblity.AllObjectLabels) self.mSettings.endGroup() # Retrieve defined object types self.mSettings.beginGroup("ObjectTypes") names = self.mSettings.value("Names", QStringList()) colors = self.mSettings.value("Colors", QStringList()) self.mSettings.endGroup() count = min(len(names), len(colors)) for i in range(count): self.mObjectTypes.append(ObjectType(names[i], QColor(colors[i]))) self.mSettings.beginGroup("Automapping") self.mAutoMapDrawing = self.boolValue("WhileDrawing") self.mSettings.endGroup() self.mSettings.beginGroup("MapsDirectory") self.mMapsDirectory = self.stringValue("Current") self.mSettings.endGroup() tilesetManager = TilesetManager.instance() tilesetManager.setReloadTilesetsOnChange(self.mReloadTilesetsOnChange) tilesetManager.setAnimateTiles(self.mShowTileAnimations) # Keeping track of some usage information self.mSettings.beginGroup("Install") self.mFirstRun = QDate.fromString(self.mSettings.value("FirstRun")) self.mRunCount = self.intValue("RunCount", 0) + 1 self.mIsPatron = self.boolValue("IsPatron") if (not self.mFirstRun.isValid()): self.mFirstRun = QDate.currentDate() self.mSettings.setValue("FirstRun", self.mFirstRun.toString(Qt.ISODate)) self.mSettings.setValue("RunCount", self.mRunCount) self.mSettings.endGroup() # Retrieve startup settings self.mSettings.beginGroup("Startup") self.mOpenLastFilesOnStartup = self.boolValue("OpenLastFiles", True) self.mSettings.endGroup() def __del__(self): pass def setObjectLabelVisibility(self, visibility): if self.mObjectLabelVisibility == visibility: return self.mObjectLabelVisibility = visibility self.mSettings.setValue("Interface/ObjectLabelVisibility", visibility) self.objectLabelVisibilityChanged.emit(visibility) def instance(): if (not Preferences.mInstance): Preferences.mInstance = Preferences() return Preferences.mInstance def deleteInstance(): del Preferences.mInstance Preferences.mInstance = None def showGrid(self): return self.mShowGrid def showTileObjectOutlines(self): return self.mShowTileObjectOutlines def showTileAnimations(self): return self.mShowTileAnimations def snapToGrid(self): return self.mSnapToGrid def snapToFineGrid(self): return self.mSnapToFineGrid def gridColor(self): return self.mGridColor def gridFine(self): return self.mGridFine def objectLineWidth(self): return self.mObjectLineWidth def highlightCurrentLayer(self): return self.mHighlightCurrentLayer def showTilesetGrid(self): return self.mShowTilesetGrid def useOpenGL(self): return self.mUseOpenGL def objectTypes(self): return self.mObjectTypes def automappingDrawing(self): return self.mAutoMapDrawing ## # Provides access to the QSettings instance to allow storing/retrieving # arbitrary values. The naming style for groups and keys is CamelCase. ## def settings(self): return self.mSettings def layerDataFormat(self): return self.mLayerDataFormat def setLayerDataFormat(self, layerDataFormat): if (self.mLayerDataFormat == layerDataFormat): return self.mLayerDataFormat = layerDataFormat self.mSettings.setValue("Storage/LayerDataFormat", self.mLayerDataFormat) def mapRenderOrder(self): return self.mMapRenderOrder def setMapRenderOrder(self, mapRenderOrder): if (self.mMapRenderOrder == mapRenderOrder): return self.mMapRenderOrder = mapRenderOrder self.mSettings.setValue("Storage/MapRenderOrder", self.mMapRenderOrder) def dtdEnabled(self): return self.mDtdEnabled def setDtdEnabled(self, enabled): self.mDtdEnabled = enabled self.mSettings.setValue("Storage/DtdEnabled", enabled) def language(self): return self.mLanguage def setLanguage(self, language): if (self.mLanguage == language): return self.mLanguage = language self.mSettings.setValue("Interface/Language", self.mLanguage) languagemanager.LanguageManager.instance().installTranslators() def reloadTilesetsOnChange(self): return self.mReloadTilesetsOnChange def setReloadTilesetsOnChanged(self, value): if (self.mReloadTilesetsOnChange == value): return self.mReloadTilesetsOnChange = value self.mSettings.setValue("Storage/ReloadTilesets", self.mReloadTilesetsOnChange) tilesetManager = TilesetManager.instance() tilesetManager.setReloadTilesetsOnChange(self.mReloadTilesetsOnChange) def setUseOpenGL(self, useOpenGL): if (self.mUseOpenGL == useOpenGL): return self.mUseOpenGL = useOpenGL self.mSettings.setValue("Interface/OpenGL", self.mUseOpenGL) self.useOpenGLChanged.emit(self.mUseOpenGL) def setObjectTypes(self, objectTypes): self.mObjectTypes = objectTypes names = QStringList() colors = QStringList() for objectType in objectTypes: names.append(objectType.name) colors.append(objectType.color.name()) self.mSettings.beginGroup("ObjectTypes") self.mSettings.setValue("Names", names) self.mSettings.setValue("Colors", colors) self.mSettings.endGroup() self.objectTypesChanged.emit() def lastPath(self, fileType): path = self.mSettings.value(lastPathKey(fileType)) if path==None or path=='': documentManager = DocumentManager.instance() mapDocument = documentManager.currentDocument() if mapDocument: path = QFileInfo(mapDocument.fileName()).path() if path==None or path=='': path = QStandardPaths.writableLocation(QStandardPaths.DocumentsLocation) return path ## # \see lastPath() ## def setLastPath(self, fileType, path): self.mSettings.setValue(lastPathKey(fileType), path) def setAutomappingDrawing(self, enabled): self.mAutoMapDrawing = enabled self.mSettings.setValue("Automapping/WhileDrawing", enabled) def mapsDirectory(self): return self.mMapsDirectory def setMapsDirectory(self, path): if (self.mMapsDirectory == path): return self.mMapsDirectory = path self.mSettings.setValue("MapsDirectory/Current", path) self.mapsDirectoryChanged.emit() def objectLabelVisibility(self): return self.mObjectLabelVisibility def firstRun(self): return self.mFirstRun def runCount(self): return self.mRunCount def isPatron(self): return self.mIsPatron def openLastFilesOnStartup(self): return self.mOpenLastFilesOnStartup def setPatron(self, isPatron): if (self.mIsPatron == isPatron): return self.mIsPatron = isPatron self.mSettings.setValue("Install/IsPatron", isPatron) self.isPatronChanged.emit() def setShowGrid(self, showGrid): if (self.mShowGrid == showGrid): return self.mShowGrid = showGrid self.mSettings.setValue("Interface/ShowGrid", self.mShowGrid) self.showGridChanged.emit(self.mShowGrid) def setShowTileObjectOutlines(self, enabled): if (self.mShowTileObjectOutlines == enabled): return self.mShowTileObjectOutlines = enabled self.mSettings.setValue("Interface/ShowTileObjectOutlines", self.mShowTileObjectOutlines) self.showTileObjectOutlinesChanged.emit(self.mShowTileObjectOutlines) def setShowTileAnimations(self, enabled): if (self.mShowTileAnimations == enabled): return self.mShowTileAnimations = enabled self.mSettings.setValue("Interface/ShowTileAnimations", self.mShowTileAnimations) tilesetManager = TilesetManager.instance() tilesetManager.setAnimateTiles(self.mShowTileAnimations) self.showTileAnimationsChanged.emit(self.mShowTileAnimations) def setSnapToGrid(self, snapToGrid): if (self.mSnapToGrid == snapToGrid): return self.mSnapToGrid = snapToGrid self.mSettings.setValue("Interface/SnapToGrid", self.mSnapToGrid) self.snapToGridChanged.emit(self.mSnapToGrid) def setSnapToFineGrid(self, snapToFineGrid): if (self.mSnapToFineGrid == snapToFineGrid): return self.mSnapToFineGrid = snapToFineGrid self.mSettings.setValue("Interface/SnapToFineGrid", self.mSnapToFineGrid) self.snapToFineGridChanged.emit(self.mSnapToFineGrid) def setGridColor(self, gridColor): if (self.mGridColor == gridColor): return self.mGridColor = gridColor self.mSettings.setValue("Interface/GridColor", self.mGridColor.name()) self.gridColorChanged.emit(self.mGridColor) def setGridFine(self, gridFine): if (self.mGridFine == gridFine): return self.mGridFine = gridFine self.mSettings.setValue("Interface/GridFine", self.mGridFine) self.gridFineChanged.emit(self.mGridFine) def setObjectLineWidth(self, lineWidth): if (self.mObjectLineWidth == lineWidth): return self.mObjectLineWidth = lineWidth self.mSettings.setValue("Interface/ObjectLineWidth", self.mObjectLineWidth) self.objectLineWidthChanged.emit(self.mObjectLineWidth) def setHighlightCurrentLayer(self, highlight): if (self.mHighlightCurrentLayer == highlight): return self.mHighlightCurrentLayer = highlight self.mSettings.setValue("Interface/HighlightCurrentLayer", self.mHighlightCurrentLayer) self.highlightCurrentLayerChanged.emit(self.mHighlightCurrentLayer) def setShowTilesetGrid(self, showTilesetGrid): if (self.mShowTilesetGrid == showTilesetGrid): return self.mShowTilesetGrid = showTilesetGrid self.mSettings.setValue("Interface/ShowTilesetGrid", self.mShowTilesetGrid) self.showTilesetGridChanged.emit(self.mShowTilesetGrid) def setOpenLastFilesOnStartup(self, open): if self.mOpenLastFilesOnStartup == open: return self.mOpenLastFilesOnStartup = open self.mSettings.setValue("Startup/OpenLastFiles", open) def boolValue(self, key, defaultValue = False): b = self.mSettings.value(key, defaultValue) tp = type(b) if tp==bool: return b elif tp==str: return b.lower()=='true' return bool(b) def colorValue(self, key, default = QColor()): if type(default) != QColor: default = QColor(default) name = self.mSettings.value(key, default.name()) if (not QColor.isValidColor(name)): return QColor() return QColor(name) def stringValue(self, key, default = ''): return self.mSettings.value(key, default) def intValue(self, key, defaultValue): return Int(self.mSettings.value(key, defaultValue)) def realValue(self, key, defaultValue): return Float(self.mSettings.value(key, defaultValue)) def stampsDirectory(self): if self.mStampsDirectory == '': appData = QStandardPaths.writableLocation(QStandardPaths.AppDataLocation) return appData + "/stamps" return self.mStampsDirectory def setStampsDirectory(self, stampsDirectory): if self.mStampsDirectory == stampsDirectory: return self.mStampsDirectory = stampsDirectory self.mSettings.setValue("Storage/StampsDirectory", stampsDirectory) self.stampsDirectoryChanged.emit(stampsDirectory)
class AutoMapperWrapper(QUndoCommand): def __init__(self, mapDocument, autoMapper, where): super().__init__() self.mLayersAfter = QVector() self.mLayersBefore = QVector() self.mMapDocument = mapDocument map = self.mMapDocument.Map() touchedLayers = QSet() index = 0 while (index < autoMapper.size()): a = autoMapper.at(index) if (a.prepareAutoMap()): touchedLayers |= a.getTouchedTileLayers() index += 1 else: autoMapper.remove(index) for layerName in touchedLayers: layerindex = map.indexOfLayer(layerName) self.mLayersBefore(map.layerAt(layerindex).clone()) for a in autoMapper: a.autoMap(where) for layerName in touchedLayers: layerindex = map.indexOfLayer(layerName) # layerindex exists, because AutoMapper is still alive, dont check self.mLayersAfter(map.layerAt(layerindex).clone()) # reduce memory usage by saving only diffs for i in range(self.mLayersAfter.size()): before = self.mLayersBefore.at(i) after = self.mLayersAfter.at(i) diffRegion = before.computeDiffRegion(after).boundingRect() before1 = before.copy(diffRegion) after1 = after.copy(diffRegion) before1.setPosition(diffRegion.topLeft()) after1.setPosition(diffRegion.topLeft()) before1.setName(before.name()) after1.setName(after.name()) self.mLayersBefore.replace(i, before1) self.mLayersAfter.replace(i, after1) del before del after for a in autoMapper: a.cleanAll() def __del__(self): for i in self.mLayersAfter: del i for i in self.mLayersBefore: del i def undo(self): map = self.mMapDocument.Map() for layer in self.mLayersBefore: layerindex = map.indexOfLayer(layer.name()) if (layerindex != -1): self.patchLayer(layerindex, layer) def redo(self): map = self.mMapDocument.Map() for layer in self.mLayersAfter: layerindex = (map.indexOfLayer(layer.name())) if (layerindex != -1): self.patchLayer(layerindex, layer) def patchLayer(self, layerIndex, layer): map = self.mMapDocument.Map() b = layer.bounds() t = map.layerAt(layerIndex) t.setCells(b.left() - t.x(), b.top() - t.y(), layer, b.translated(-t.position())) self.mMapDocument.emitRegionChanged(b, t)
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
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())
def __init__(self, *args): self.mOrientation = 0 self.mRenderOrder = 0 self.mWidth = 0 self.mHeight = 0 self.mTileWidth = 0 self.mTileHeight = 0 self.mHexSideLength = 0 self.mStaggerAxis = 0 self.mStaggerIndex = 0 self.mBackgroundColor = QColor() self.mDrawMargins = QMargins() self.mLayers = QList() self.mTilesets = QVector() self.mLayerDataFormat = None self.mNextObjectId = 0 l = len(args) if l == 1: ## # Copy constructor. Makes sure that a deep-copy of the layers is created. ## map = args[0] super().__init__(map) self.mLayers = QList() self.mOrientation = map.mOrientation self.mRenderOrder = map.mRenderOrder self.mWidth = map.mWidth self.mHeight = map.mHeight self.mTileWidth = map.mTileWidth self.mTileHeight = map.mTileHeight self.mHexSideLength = map.mHexSideLength self.mStaggerAxis = map.mStaggerAxis self.mStaggerIndex = map.mStaggerIndex self.mBackgroundColor = map.mBackgroundColor self.mDrawMargins = map.mDrawMargins self.mTilesets = map.mTilesets self.mLayerDataFormat = map.mLayerDataFormat self.mNextObjectId = 1 for layer in map.mLayers: clone = layer.clone() clone.setMap(self) self.mLayers.append(clone) elif l == 5: ## # Constructor, taking map orientation, size and tile size as parameters. ## orientation, width, height, tileWidth, tileHeight = args super().__init__(Object.MapType) self.mLayers = QList() self.mTilesets = QList() self.mOrientation = orientation self.mRenderOrder = Map.RenderOrder.RightDown self.mWidth = width self.mHeight = height self.mTileWidth = tileWidth self.mTileHeight = tileHeight self.mHexSideLength = 0 self.mStaggerAxis = Map.StaggerAxis.StaggerY self.mStaggerIndex = Map.StaggerIndex.StaggerOdd self.mLayerDataFormat = Map.LayerDataFormat.Base64Zlib self.mNextObjectId = 1
def __init__(self, parent): super().__init__(parent) self.mFrames = QVector() self.mTileset = None
def __readTilesetTile(self, tileset): atts = self.xml.attributes() id = Int(atts.value("id")) if (id < 0): self.xml.raiseError(self.tr("Invalid tile ID: %d" % id)) return hasImage = tileset.imageSource() != '' if (hasImage and id >= tileset.tileCount()): self.xml.raiseError( self.tr("Tile ID does not exist in tileset image: %d" % id)) return if (id > tileset.tileCount()): self.xml.raiseError( self.tr("Invalid (nonconsecutive) tile ID: %d" % id)) return # For tilesets without image source, consecutive tile IDs are allowed (for # tiles with individual images) if (id == tileset.tileCount()): tileset.addTile(QPixmap()) tile = tileset.tileAt(id) # Read tile quadrant terrain ids terrain = atts.value("terrain") if terrain != '': quadrants = terrain.split(",") if (len(quadrants) == 4): for i in range(4): if quadrants[i] == '': t = -1 else: t = Int(quadrants[i]) tile.setCornerTerrainId(i, t) # Read tile probability probability = atts.value("probability") if probability != '': tile.setProbability(Float(probability)) while (self.xml.readNextStartElement()): if (self.xml.name() == "properties"): tile.mergeProperties(self.__readProperties()) elif (self.xml.name() == "image"): source = self.xml.attributes().value("source") if source != '': source = self.p.resolveReference(source, self.mPath) tileset.setTileImage(id, QPixmap.fromImage(self.__readImage()), source) elif (self.xml.name() == "objectgroup"): tile.setObjectGroup(self.__readObjectGroup()) elif (self.xml.name() == "animation"): tile.setFrames(self.__readAnimationFrames()) else: self.__readUnknownElement() # Temporary code to support TMW-style animation frame properties if (not tile.isAnimated() and tile.hasProperty("animation-frame0")): frames = QVector() i = 0 while (i >= 0): frameName = "animation-frame" + str(i) delayName = "animation-delay" + str(i) if (tile.hasProperty(frameName) and tile.hasProperty(delayName)): frame = Frame() frame.tileId = tile.property(frameName) frame.duration = tile.property(delayName) * 10 frames.append(frame) else: break i += 1 tile.setFrames(frames)
class FrameListModel(QAbstractListModel): DEFAULT_DURATION = 100 def __init__(self, parent): super().__init__(parent) self.mFrames = QVector() self.mTileset = None def rowCount(self, parent): if parent.isValid(): _x = 0 else: _x = self.mFrames.size() return _x def data(self, index, role): x = role if x == Qt.EditRole or x == Qt.DisplayRole: return self.mFrames.at(index.row()).duration elif x == Qt.DecorationRole: tileId = self.mFrames.at(index.row()).tileId tile = self.mTileset.tileAt(tileId) if tile: return tile.image() return QVariant() def setData(self, index, value, role): if (role == Qt.EditRole): duration = value if (duration >= 0): self.mFrames[index.row()].duration = duration self.dataChanged.emit(index, index) return True return False def flags(self, index): defaultFlags = super().flags(index) if (index.isValid()): return Qt.ItemIsDragEnabled | Qt.ItemIsEditable | defaultFlags else: return Qt.ItemIsDropEnabled | defaultFlags def removeRows(self, row, count, parent): if (not parent.isValid()): if (count > 0): self.beginRemoveRows(parent, row, row + count - 1) self.mFrames.remove(row, count) self.endRemoveRows() return True return False def mimeTypes(self): types = QStringList() types.append(TILES_MIMETYPE) types.append(FRAMES_MIMETYPE) return types def mimeData(self, indexes): mimeData = QMimeData() encodedData = QByteArray() stream = QDataStream(encodedData, QIODevice.WriteOnly) for index in indexes: if (index.isValid()): frame = self.mFrames.at(index.row()) stream.writeInt(frame.tileId) stream.writeInt(frame.duration) mimeData.setData(FRAMES_MIMETYPE, encodedData) return mimeData def dropMimeData(self, data, action, row, column, parent): if (action == Qt.IgnoreAction): return True if (column > 0): return False beginRow = 0 if (row != -1): beginRow = row elif parent.isValid(): beginRow = parent.row() else: beginRow = self.mFrames.size() newFrames = QVector() if (data.hasFormat(FRAMES_MIMETYPE)): encodedData = data.data(FRAMES_MIMETYPE) stream = QDataStream(encodedData, QIODevice.ReadOnly) while (not stream.atEnd()): frame = Frame() frame.tileId = stream.readInt() frame.duration = stream.readInt() newFrames.append(frame) elif (data.hasFormat(TILES_MIMETYPE)): encodedData = data.data(TILES_MIMETYPE) stream = QDataStream(encodedData, QIODevice.ReadOnly) while (not stream.atEnd()): frame = Frame() frame.tileId = stream.readInt() frame.duration = FrameListModel.DEFAULT_DURATION newFrames.append(frame) if (newFrames.isEmpty()): return False self.beginInsertRows(QModelIndex(), beginRow, beginRow + newFrames.size() - 1) self.mFrames.insert(beginRow, newFrames.size(), Frame()) for i in range(newFrames.size()): self.mFrames[i + beginRow] = newFrames[i] self.endInsertRows() return True def supportedDropActions(self): return Qt.CopyAction | Qt.MoveAction def setFrames(self, tileset, frames): self.beginResetModel() self.mTileset = tileset self.mFrames = frames self.endResetModel() def addTileIdAsFrame(self, id): frame = Frame() frame.tileId = id frame.duration = FrameListModel.DEFAULT_DURATION self.addFrame(frame) def frames(self): return self.mFrames def addFrame(self, frame): self.beginInsertRows(QModelIndex(), self.mFrames.size(), self.mFrames.size()) self.mFrames.append(frame) self.endInsertRows()
class TilesetDock(QDockWidget): ## # Emitted when the current tile changed. ## currentTileChanged = pyqtSignal(list) ## # Emitted when the currently selected tiles changed. ## stampCaptured = pyqtSignal(TileStamp) ## # Emitted when files are dropped at the tileset dock. ## tilesetsDropped = pyqtSignal(QStringList) newTileset = pyqtSignal() ## # Constructor. ## def __init__(self, parent=None): super().__init__(parent) # Shared tileset references because the dock wants to add new tiles self.mTilesets = QVector() self.mCurrentTilesets = QMap() self.mMapDocument = None self.mTabBar = QTabBar() self.mViewStack = QStackedWidget() self.mToolBar = QToolBar() self.mCurrentTile = None self.mCurrentTiles = None self.mNewTileset = QAction(self) self.mImportTileset = QAction(self) self.mExportTileset = QAction(self) self.mPropertiesTileset = QAction(self) self.mDeleteTileset = QAction(self) self.mEditTerrain = QAction(self) self.mAddTiles = QAction(self) self.mRemoveTiles = QAction(self) self.mTilesetMenuButton = TilesetMenuButton(self) self.mTilesetMenu = QMenu(self) # opens on click of mTilesetMenu self.mTilesetActionGroup = QActionGroup(self) self.mTilesetMenuMapper = None # needed due to dynamic content self.mEmittingStampCaptured = False self.mSynchronizingSelection = False self.setObjectName("TilesetDock") self.mTabBar.setMovable(True) self.mTabBar.setUsesScrollButtons(True) self.mTabBar.currentChanged.connect(self.updateActions) self.mTabBar.tabMoved.connect(self.moveTileset) w = QWidget(self) horizontal = QHBoxLayout() horizontal.setSpacing(0) horizontal.addWidget(self.mTabBar) horizontal.addWidget(self.mTilesetMenuButton) vertical = QVBoxLayout(w) vertical.setSpacing(0) vertical.setContentsMargins(5, 5, 5, 5) vertical.addLayout(horizontal) vertical.addWidget(self.mViewStack) horizontal = QHBoxLayout() horizontal.setSpacing(0) horizontal.addWidget(self.mToolBar, 1) vertical.addLayout(horizontal) self.mNewTileset.setIcon(QIcon(":images/16x16/document-new.png")) self.mImportTileset.setIcon(QIcon(":images/16x16/document-import.png")) self.mExportTileset.setIcon(QIcon(":images/16x16/document-export.png")) self.mPropertiesTileset.setIcon( QIcon(":images/16x16/document-properties.png")) self.mDeleteTileset.setIcon(QIcon(":images/16x16/edit-delete.png")) self.mEditTerrain.setIcon(QIcon(":images/16x16/terrain.png")) self.mAddTiles.setIcon(QIcon(":images/16x16/add.png")) self.mRemoveTiles.setIcon(QIcon(":images/16x16/remove.png")) Utils.setThemeIcon(self.mNewTileset, "document-new") Utils.setThemeIcon(self.mImportTileset, "document-import") Utils.setThemeIcon(self.mExportTileset, "document-export") Utils.setThemeIcon(self.mPropertiesTileset, "document-properties") Utils.setThemeIcon(self.mDeleteTileset, "edit-delete") Utils.setThemeIcon(self.mAddTiles, "add") Utils.setThemeIcon(self.mRemoveTiles, "remove") self.mNewTileset.triggered.connect(self.newTileset) self.mImportTileset.triggered.connect(self.importTileset) self.mExportTileset.triggered.connect(self.exportTileset) self.mPropertiesTileset.triggered.connect(self.editTilesetProperties) self.mDeleteTileset.triggered.connect(self.removeTileset) self.mEditTerrain.triggered.connect(self.editTerrain) self.mAddTiles.triggered.connect(self.addTiles) self.mRemoveTiles.triggered.connect(self.removeTiles) self.mToolBar.addAction(self.mNewTileset) self.mToolBar.setIconSize(QSize(16, 16)) self.mToolBar.addAction(self.mImportTileset) self.mToolBar.addAction(self.mExportTileset) self.mToolBar.addAction(self.mPropertiesTileset) self.mToolBar.addAction(self.mDeleteTileset) self.mToolBar.addAction(self.mEditTerrain) self.mToolBar.addAction(self.mAddTiles) self.mToolBar.addAction(self.mRemoveTiles) self.mZoomable = Zoomable(self) self.mZoomComboBox = QComboBox() self.mZoomable.connectToComboBox(self.mZoomComboBox) horizontal.addWidget(self.mZoomComboBox) self.mViewStack.currentChanged.connect(self.updateCurrentTiles) TilesetManager.instance().tilesetChanged.connect(self.tilesetChanged) DocumentManager.instance().documentAboutToClose.connect( self.documentAboutToClose) self.mTilesetMenuButton.setMenu(self.mTilesetMenu) self.mTilesetMenu.aboutToShow.connect(self.refreshTilesetMenu) self.setWidget(w) self.retranslateUi() self.setAcceptDrops(True) self.updateActions() def __del__(self): del self.mCurrentTiles ## # Sets the map for which the tilesets should be displayed. ## def setMapDocument(self, mapDocument): if (self.mMapDocument == mapDocument): return # Hide while we update the tab bar, to avoid repeated layouting if sys.platform != 'darwin': self.widget().hide() self.setCurrentTiles(None) self.setCurrentTile(None) if (self.mMapDocument): # Remember the last visible tileset for this map tilesetName = self.mTabBar.tabText(self.mTabBar.currentIndex()) self.mCurrentTilesets.insert(self.mMapDocument, tilesetName) # Clear previous content while (self.mTabBar.count()): self.mTabBar.removeTab(0) while (self.mViewStack.count()): self.mViewStack.removeWidget(self.mViewStack.widget(0)) #self.mTilesets.clear() # Clear all connections to the previous document if (self.mMapDocument): self.mMapDocument.disconnect() self.mMapDocument = mapDocument if (self.mMapDocument): self.mTilesets = self.mMapDocument.map().tilesets() for tileset in self.mTilesets: view = TilesetView() view.setMapDocument(self.mMapDocument) view.setZoomable(self.mZoomable) self.mTabBar.addTab(tileset.name()) self.mViewStack.addWidget(view) self.mMapDocument.tilesetAdded.connect(self.tilesetAdded) self.mMapDocument.tilesetRemoved.connect(self.tilesetRemoved) self.mMapDocument.tilesetMoved.connect(self.tilesetMoved) self.mMapDocument.tilesetNameChanged.connect( self.tilesetNameChanged) self.mMapDocument.tilesetFileNameChanged.connect( self.updateActions) self.mMapDocument.tilesetChanged.connect(self.tilesetChanged) self.mMapDocument.tileAnimationChanged.connect( self.tileAnimationChanged) cacheName = self.mCurrentTilesets.take(self.mMapDocument) for i in range(self.mTabBar.count()): if (self.mTabBar.tabText(i) == cacheName): self.mTabBar.setCurrentIndex(i) break object = self.mMapDocument.currentObject() if object: if object.typeId() == Object.TileType: self.setCurrentTile(object) self.updateActions() if sys.platform != 'darwin': self.widget().show() ## # Synchronizes the selection with the given stamp. Ignored when the stamp is # changing because of a selection change in the TilesetDock. ## def selectTilesInStamp(self, stamp): if self.mEmittingStampCaptured: return processed = QSet() selections = QMap() for variation in stamp.variations(): tileLayer = variation.tileLayer() for cell in tileLayer: tile = cell.tile if tile: if (processed.contains(tile)): continue processed.insert(tile) # avoid spending time on duplicates tileset = tile.tileset() tilesetIndex = self.mTilesets.indexOf( tileset.sharedPointer()) if (tilesetIndex != -1): view = self.tilesetViewAt(tilesetIndex) if (not view.model()): # Lazily set up the model self.setupTilesetModel(view, tileset) model = view.tilesetModel() modelIndex = model.tileIndex(tile) selectionModel = view.selectionModel() _x = QItemSelection() _x.select(modelIndex, modelIndex) selections[selectionModel] = _x if (not selections.isEmpty()): self.mSynchronizingSelection = True # Mark captured tiles as selected for i in selections: selectionModel = i[0] selection = i[1] selectionModel.select(selection, QItemSelectionModel.SelectCurrent) # Show/edit properties of all captured tiles self.mMapDocument.setSelectedTiles(processed.toList()) # Update the current tile (useful for animation and collision editors) first = selections.first() selectionModel = first[0] selection = first[1] currentIndex = QModelIndex(selection.first().topLeft()) if (selectionModel.currentIndex() != currentIndex): selectionModel.setCurrentIndex(currentIndex, QItemSelectionModel.NoUpdate) else: self.currentChanged(currentIndex) self.mSynchronizingSelection = False def currentTilesetChanged(self): view = self.currentTilesetView() if view: s = view.selectionModel() if s: self.setCurrentTile(view.tilesetModel().tileAt( s.currentIndex())) ## # Returns the currently selected tile. ## def currentTile(self): return self.mCurrentTile def changeEvent(self, e): super().changeEvent(e) x = e.type() if x == QEvent.LanguageChange: self.retranslateUi() else: pass def dragEnterEvent(self, e): urls = e.mimeData().urls() if (not urls.isEmpty() and not urls.at(0).toLocalFile().isEmpty()): e.accept() def dropEvent(self, e): paths = QStringList() for url in e.mimeData().urls(): localFile = url.toLocalFile() if (not localFile.isEmpty()): paths.append(localFile) if (not paths.isEmpty()): self.tilesetsDropped.emit(paths) e.accept() def selectionChanged(self): self.updateActions() if not self.mSynchronizingSelection: self.updateCurrentTiles() def currentChanged(self, index): if (not index.isValid()): return model = index.model() self.setCurrentTile(model.tileAt(index)) def updateActions(self): external = False hasImageSource = False hasSelection = False view = None index = self.mTabBar.currentIndex() if (index > -1): view = self.tilesetViewAt(index) if (view): tileset = self.mTilesets.at(index) if (not view.model()): # Lazily set up the model self.setupTilesetModel(view, tileset) self.mViewStack.setCurrentIndex(index) external = tileset.isExternal() hasImageSource = tileset.imageSource() != '' hasSelection = view.selectionModel().hasSelection() tilesetIsDisplayed = view != None mapIsDisplayed = self.mMapDocument != None self.mNewTileset.setEnabled(mapIsDisplayed) self.mImportTileset.setEnabled(tilesetIsDisplayed and external) self.mExportTileset.setEnabled(tilesetIsDisplayed and not external) self.mPropertiesTileset.setEnabled(tilesetIsDisplayed and not external) self.mDeleteTileset.setEnabled(tilesetIsDisplayed) self.mEditTerrain.setEnabled(tilesetIsDisplayed and not external) self.mAddTiles.setEnabled(tilesetIsDisplayed and not hasImageSource and not external) self.mRemoveTiles.setEnabled(tilesetIsDisplayed and not hasImageSource and hasSelection and not external) def updateCurrentTiles(self): view = self.currentTilesetView() if (not view): return s = view.selectionModel() if (not s): return indexes = s.selection().indexes() if len(indexes) == 0: return first = indexes[0] minX = first.column() maxX = first.column() minY = first.row() maxY = first.row() for index in indexes: if minX > index.column(): minX = index.column() if maxX < index.column(): maxX = index.column() if minY > index.row(): minY = index.row() if maxY < index.row(): maxY = index.row() # Create a tile layer from the current selection tileLayer = TileLayer(QString(), 0, 0, maxX - minX + 1, maxY - minY + 1) model = view.tilesetModel() for index in indexes: tileLayer.setCell(index.column() - minX, index.row() - minY, Cell(model.tileAt(index))) self.setCurrentTiles(tileLayer) def indexPressed(self, index): view = self.currentTilesetView() tile = view.tilesetModel().tileAt(index) if tile: self.mMapDocument.setCurrentObject(tile) def tilesetAdded(self, index, tileset): view = TilesetView() view.setMapDocument(self.mMapDocument) view.setZoomable(self.mZoomable) self.mTilesets.insert(index, tileset.sharedPointer()) self.mTabBar.insertTab(index, tileset.name()) self.mViewStack.insertWidget(index, view) self.updateActions() def tilesetChanged(self, tileset): # Update the affected tileset model, if it exists index = indexOf(self.mTilesets, tileset) if (index < 0): return model = self.tilesetViewAt(index).tilesetModel() if model: model.tilesetChanged() def tilesetRemoved(self, tileset): # Delete the related tileset view index = indexOf(self.mTilesets, tileset) self.mTilesets.removeAt(index) self.mTabBar.removeTab(index) self.tilesetViewAt(index).close() # Make sure we don't reference this tileset anymore if (self.mCurrentTiles): # TODO: Don't clean unnecessarily (but first the concept of # "current brush" would need to be introduced) cleaned = self.mCurrentTiles.clone() cleaned.removeReferencesToTileset(tileset) self.setCurrentTiles(cleaned) if (self.mCurrentTile and self.mCurrentTile.tileset() == tileset): self.setCurrentTile(None) self.updateActions() def tilesetMoved(self, _from, to): self.mTilesets.insert(to, self.mTilesets.takeAt(_from)) # Move the related tileset views widget = self.mViewStack.widget(_from) self.mViewStack.removeWidget(widget) self.mViewStack.insertWidget(to, widget) self.mViewStack.setCurrentIndex(self.mTabBar.currentIndex()) # Update the titles of the affected tabs start = min(_from, to) end = max(_from, to) for i in range(start, end + 1): tileset = self.mTilesets.at(i) if (self.mTabBar.tabText(i) != tileset.name()): self.mTabBar.setTabText(i, tileset.name()) def tilesetNameChanged(self, tileset): index = indexOf(self.mTilesets, tileset) self.mTabBar.setTabText(index, tileset.name()) def tileAnimationChanged(self, tile): view = self.currentTilesetView() if view: model = view.tilesetModel() if model: model.tileChanged(tile) ## # Removes the currently selected tileset. ## def removeTileset(self, *args): l = len(args) if l == 0: currentIndex = self.mViewStack.currentIndex() if (currentIndex != -1): self.removeTileset(self.mViewStack.currentIndex()) elif l == 1: ## # Removes the tileset at the given index. Prompting the user when the tileset # is in use by the map. ## index = args[0] tileset = self.mTilesets.at(index).data() inUse = self.mMapDocument.map().isTilesetUsed(tileset) # If the tileset is in use, warn the user and confirm removal if (inUse): warning = QMessageBox( QMessageBox.Warning, self.tr("Remove Tileset"), self.tr("The tileset \"%s\" is still in use by the map!" % tileset.name()), QMessageBox.Yes | QMessageBox.No, self) warning.setDefaultButton(QMessageBox.Yes) warning.setInformativeText( self.tr("Remove this tileset and all references " "to the tiles in this tileset?")) if (warning.exec() != QMessageBox.Yes): return remove = RemoveTileset(self.mMapDocument, index, tileset) undoStack = self.mMapDocument.undoStack() if (inUse): # Remove references to tiles in this tileset from the current map def referencesTileset(cell): tile = cell.tile if tile: return tile.tileset() == tileset return False undoStack.beginMacro(remove.text()) removeTileReferences(self.mMapDocument, referencesTileset) undoStack.push(remove) if (inUse): undoStack.endMacro() def moveTileset(self, _from, to): command = MoveTileset(self.mMapDocument, _from, to) self.mMapDocument.undoStack().push(command) def editTilesetProperties(self): tileset = self.currentTileset() if (not tileset): return self.mMapDocument.setCurrentObject(tileset) self.mMapDocument.emitEditCurrentObject() def importTileset(self): tileset = self.currentTileset() if (not tileset): return command = SetTilesetFileName(self.mMapDocument, tileset, QString()) self.mMapDocument.undoStack().push(command) def exportTileset(self): tileset = self.currentTileset() if (not tileset): return tsxFilter = self.tr("Tiled tileset files (*.tsx)") helper = FormatHelper(FileFormat.ReadWrite, tsxFilter) prefs = preferences.Preferences.instance() suggestedFileName = prefs.lastPath( preferences.Preferences.ExternalTileset) suggestedFileName += '/' suggestedFileName += tileset.name() extension = ".tsx" if (not suggestedFileName.endswith(extension)): suggestedFileName += extension selectedFilter = tsxFilter fileName, _ = QFileDialog.getSaveFileName(self, self.tr("Export Tileset"), suggestedFileName, helper.filter(), selectedFilter) if fileName == '': return prefs.setLastPath(preferences.Preferences.ExternalTileset, QFileInfo(fileName).path()) tsxFormat = TsxTilesetFormat() format = helper.formatByNameFilter(selectedFilter) if not format: format = tsxFormat if format.write(tileset, fileName): command = SetTilesetFileName(self.mMapDocument, tileset, fileName) self.mMapDocument.undoStack().push(command) else: error = format.errorString() QMessageBox.critical(self.window(), self.tr("Export Tileset"), self.tr("Error saving tileset: %s" % error)) def editTerrain(self): tileset = self.currentTileset() if (not tileset): return editTerrainDialog = EditTerrainDialog(self.mMapDocument, tileset, self) editTerrainDialog.exec() def addTiles(self): tileset = self.currentTileset() if (not tileset): return prefs = preferences.Preferences.instance() startLocation = QFileInfo( prefs.lastPath(preferences.Preferences.ImageFile)).absolutePath() filter = Utils.readableImageFormatsFilter() files = QFileDialog.getOpenFileNames(self.window(), self.tr("Add Tiles"), startLocation, filter) tiles = QList() id = tileset.tileCount() for file in files: image = QPixmap(file) if (not image.isNull()): tiles.append(Tile(image, file, id, tileset)) id += 1 else: warning = QMessageBox(QMessageBox.Warning, self.tr("Add Tiles"), self.tr("Could not load \"%s\"!" % file), QMessageBox.Ignore | QMessageBox.Cancel, self.window()) warning.setDefaultButton(QMessageBox.Ignore) if (warning.exec() != QMessageBox.Ignore): tiles.clear() return if (tiles.isEmpty()): return prefs.setLastPath(preferences.Preferences.ImageFile, files.last()) self.mMapDocument.undoStack().push( AddTiles(self.mMapDocument, tileset, tiles)) def removeTiles(self): view = self.currentTilesetView() if (not view): return if (not view.selectionModel().hasSelection()): return indexes = view.selectionModel().selectedIndexes() model = view.tilesetModel() tileIds = RangeSet() tiles = QList() for index in indexes: tile = model.tileAt(index) if tile: tileIds.insert(tile.id()) tiles.append(tile) def matchesAnyTile(cell): tile = cell.tile if tile: return tiles.contains(tile) return False inUse = self.hasTileReferences(self.mMapDocument, matchesAnyTile) # If the tileset is in use, warn the user and confirm removal if (inUse): warning = QMessageBox( QMessageBox.Warning, self.tr("Remove Tiles"), self.tr("One or more of the tiles to be removed are " "still in use by the map!"), QMessageBox.Yes | QMessageBox.No, self) warning.setDefaultButton(QMessageBox.Yes) warning.setInformativeText( self.tr("Remove all references to these tiles?")) if (warning.exec() != QMessageBox.Yes): return undoStack = self.mMapDocument.undoStack() undoStack.beginMacro(self.tr("Remove Tiles")) removeTileReferences(self.mMapDocument, matchesAnyTile) # Iterate backwards over the ranges in order to keep the indexes valid firstRange = tileIds.begin() it = tileIds.end() if (it == firstRange): # no range return tileset = view.tilesetModel().tileset() while (it != firstRange): it -= 1 item = tileIds.item(it) length = item[1] - item[0] + 1 undoStack.push( RemoveTiles(self.mMapDocument, tileset, item[0], length)) undoStack.endMacro() # Clear the current tiles, will be referencing the removed tiles self.setCurrentTiles(None) self.setCurrentTile(None) def documentAboutToClose(self, mapDocument): self.mCurrentTilesets.remove(mapDocument) def refreshTilesetMenu(self): self.mTilesetMenu.clear() if (self.mTilesetMenuMapper): self.mTabBar.disconnect(self.mTilesetMenuMapper) del self.mTilesetMenuMapper self.mTilesetMenuMapper = QSignalMapper(self) self.mTilesetMenuMapper.mapped.connect(self.mTabBar.setCurrentIndex) currentIndex = self.mTabBar.currentIndex() for i in range(self.mTabBar.count()): action = QAction(self.mTabBar.tabText(i), self) action.setCheckable(True) self.mTilesetActionGroup.addAction(action) if (i == currentIndex): action.setChecked(True) self.mTilesetMenu.addAction(action) action.triggered.connect(self.mTilesetMenuMapper.map) self.mTilesetMenuMapper.setMapping(action, i) def setCurrentTile(self, tile): if (self.mCurrentTile == tile): return self.mCurrentTile = tile self.currentTileChanged.emit([tile]) if (tile): self.mMapDocument.setCurrentObject(tile) def setCurrentTiles(self, tiles): if (self.mCurrentTiles == tiles): return del self.mCurrentTiles self.mCurrentTiles = tiles # Set the selected tiles on the map document if (tiles): selectedTiles = QList() for y in range(tiles.height()): for x in range(tiles.width()): cell = tiles.cellAt(x, y) if (not cell.isEmpty()): selectedTiles.append(cell.tile) self.mMapDocument.setSelectedTiles(selectedTiles) # Create a tile stamp with these tiles map = self.mMapDocument.map() stamp = Map(map.orientation(), tiles.width(), tiles.height(), map.tileWidth(), map.tileHeight()) stamp.addLayer(tiles.clone()) stamp.addTilesets(tiles.usedTilesets()) self.mEmittingStampCaptured = True self.stampCaptured.emit(TileStamp(stamp)) self.mEmittingStampCaptured = False def retranslateUi(self): self.setWindowTitle(self.tr("Tilesets")) self.mNewTileset.setText(self.tr("New Tileset")) self.mImportTileset.setText(self.tr("Import Tileset")) self.mExportTileset.setText(self.tr("Export Tileset As...")) self.mPropertiesTileset.setText(self.tr("Tileset Properties")) self.mDeleteTileset.setText(self.tr("Remove Tileset")) self.mEditTerrain.setText(self.tr("Edit Terrain Information")) self.mAddTiles.setText(self.tr("Add Tiles")) self.mRemoveTiles.setText(self.tr("Remove Tiles")) def currentTileset(self): index = self.mTabBar.currentIndex() if (index == -1): return None return self.mTilesets.at(index) def currentTilesetView(self): return self.mViewStack.currentWidget() def tilesetViewAt(self, index): return self.mViewStack.widget(index) def setupTilesetModel(self, view, tileset): view.setModel(TilesetModel(tileset, view)) s = view.selectionModel() s.selectionChanged.connect(self.selectionChanged) s.currentChanged.connect(self.currentChanged) view.pressed.connect(self.indexPressed)
class CommandLineParser(): def __init__(self): self.mCurrentProgramName = QString() self.mOptions = QVector() self.mShowHelp = False self.mLongestArgument = 0 self.mFilesToOpen = QStringList() def tr(self, sourceText, disambiguation='', n=-1): return QCoreApplication.translate('CommandLineParser', sourceText, disambiguation, n) def trUtf8(self, sourceText, disambiguation='', n=-1): return QCoreApplication.translate('CommandLineParser', sourceText, disambiguation, n) ## # Registers an option with the parser. When an option with the given # \a shortName or \a longName is encountered, \a callback is called with # \a data as its only parameter. ## def registerOption(self, *args): l = len(args) if l == 4: ## # Convenience overload that allows registering an option with a callback # as a member function of a class. The class type and the member function # are given as template parameters, while the instance is passed in as # \a handler. # # \overload ## handler, shortName, longName, help = args self.registerOption(MemberFunctionCall, handler, shortName, longName, help) elif l == 5: callback, data, shortName, longName, help = args self.mOptions.append( CommandLineParser.Option(callback, data, shortName, longName, help)) length = longName.length() if (self.mLongestArgument < length): self.mLongestArgument = length ## # Parses the given \a arguments. Returns False when the application is not # expected to run (either there was a parsing error, or the help was # requested). ## def parse(self, arguments): self.mFilesToOpen.clear() self.mShowHelp = False todo = QStringList(arguments) self.mCurrentProgramName = QFileInfo(todo.takeFirst()).fileName() index = 0 noMoreArguments = False while (not todo.isEmpty()): index += 1 arg = todo.takeFirst() if (arg.isEmpty()): continue if (noMoreArguments or arg.at(0) != '-'): self.mFilesToOpen.append(arg) continue if (arg.length() == 1): # Traditionally a single hyphen means read file from stdin, # write file to stdout. This isn't supported right now. qWarning(self.tr("Bad argument %d: lonely hyphen" % index)) self.showHelp() return False # Long options if (arg.at(1) == '-'): # Double hypen "--" means no more options will follow if (arg.length() == 2): noMoreArguments = True continue if (not self.handleLongOption(arg)): qWarning( self.tr("Unknown long argument %d: %s" % (index, arg))) self.mShowHelp = True break continue # Short options for i in range(1, arg.length()): c = arg.at(i) if (not self.handleShortOption(c)): qWarning( self.tr("Unknown short argument %d.%d: %s" % (index, i, c))) self.mShowHelp = True break if (self.mShowHelp): self.showHelp() return False return True ## # Returns the files to open that were found among the arguments. ## def filesToOpen(self): return QList(self.mFilesToOpen) def showHelp(self): qWarning( self.tr("Usage:\n %s [options] [files...]" % self.mCurrentProgramName) + "\n\n" + self.tr("Options:")) qWarning(" -h %-*s : %s", self.mLongestArgument, "--help", self.tr("Display this help")) for option in self.mOptions: if (not option.shortName.isNull()): qWarning(" -%c %-*s : %s", option.shortName.toLatin1(), self.mLongestArgument, option.longName, option.help) else: qWarning(" %-*s : %s", self.mLongestArgument, option.longName, option.help) qWarning() def handleLongOption(self, longName): if (longName == "--help"): self.mShowHelp = True return True for option in self.mOptions: if (longName == option.longName): option.callback(option.data) return True return False def handleShortOption(self, c): if (c == 'h'): self.mShowHelp = True return True for option in self.mOptions: if (c == option.shortName): option.callback(option.data) return True return False ## # Internal definition of a command line option. ## class Option(): def __init__(self, *args): l = len(args) callback = Callback() shortName = QChar() longName = QString() help = QString() if l == 0: self.callback = 0 self.data = 0 elif l == 5: callback = args[0] data = args[1] shortName = args[2] longName = args[3] help = args[4] self.callback = callback self.data = data self.shortName = shortName self.longName = longName self.help = help
def drawGrid(self, painter, exposed, gridColor): rect = exposed.toAlignedRect() if (rect.isNull()): return p = RenderParams(self.map()) # Determine the tile and pixel coordinates to start at startTile = self.screenToTileCoords_(rect.topLeft()).toPoint() startPos = self.tileToScreenCoords_(startTile).toPoint() ## Determine in which half of the tile the top-left corner of the area we # need to draw is. If we're in the upper half, we need to start one row # up due to those tiles being visible as well. How we go up one row # depends on whether we're in the left or right half of the tile. ## inUpperHalf = rect.y() - startPos.y() < p.sideOffsetY inLeftHalf = rect.x() - startPos.x() < p.sideOffsetX if (inUpperHalf): startTile.setY(startTile.y() - 1) if (inLeftHalf): startTile.setX(startTile.x() - 1) startTile.setX(max(0, startTile.x())) startTile.setY(max(0, startTile.y())) startPos = self.tileToScreenCoords_(startTile).toPoint() oct = [ QPoint(0, p.tileHeight - p.sideOffsetY), QPoint(0, p.sideOffsetY), QPoint(p.sideOffsetX, 0), QPoint(p.tileWidth - p.sideOffsetX, 0), QPoint(p.tileWidth, p.sideOffsetY), QPoint(p.tileWidth, p.tileHeight - p.sideOffsetY), QPoint(p.tileWidth - p.sideOffsetX, p.tileHeight), QPoint(p.sideOffsetX, p.tileHeight) ] lines = QVector() #lines.reserve(8) gridColor.setAlpha(128) gridPen = QPen(gridColor) gridPen.setCosmetic(True) _x = QVector() _x.append(2) _x.append(2) gridPen.setDashPattern(_x) painter.setPen(gridPen) if (p.staggerX): # Odd row shifting is applied in the rendering loop, so un-apply it here if (p.doStaggerX(startTile.x())): startPos.setY(startPos.y() - p.rowHeight) while (startPos.x() <= rect.right() and startTile.x() < self.map().width()): rowTile = QPoint(startTile) rowPos = QPoint(startPos) if (p.doStaggerX(startTile.x())): rowPos.setY(rowPos.y() + p.rowHeight) while (rowPos.y() <= rect.bottom() and rowTile.y() < self.map().height()): lines.append(QLineF(rowPos + oct[1], rowPos + oct[2])) lines.append(QLineF(rowPos + oct[2], rowPos + oct[3])) lines.append(QLineF(rowPos + oct[3], rowPos + oct[4])) isStaggered = p.doStaggerX(startTile.x()) lastRow = rowTile.y() == self.map().height() - 1 lastColumn = rowTile.x() == self.map().width() - 1 bottomLeft = rowTile.x() == 0 or (lastRow and isStaggered) bottomRight = lastColumn or (lastRow and isStaggered) if (bottomRight): lines.append(QLineF(rowPos + oct[5], rowPos + oct[6])) if (lastRow): lines.append(QLineF(rowPos + oct[6], rowPos + oct[7])) if (bottomLeft): lines.append(QLineF(rowPos + oct[7], rowPos + oct[0])) painter.drawLines(lines) lines.resize(0) rowPos.setY(rowPos.y() + p.tileHeight + p.sideLengthY) rowTile.setY(rowTile.y() + 1) startPos.setX(startPos.x() + p.columnWidth) startTile.setX(startTile.x() + 1) else: # Odd row shifting is applied in the rendering loop, so un-apply it here if (p.doStaggerY(startTile.y())): startPos.setX(startPos.x() - p.columnWidth) while (startPos.y() <= rect.bottom() and startTile.y() < self.map().height()): rowTile = QPoint(startTile) rowPos = QPoint(startPos) if (p.doStaggerY(startTile.y())): rowPos.setX(rowPos.x() + p.columnWidth) while (rowPos.x() <= rect.right() and rowTile.x() < self.map().width()): lines.append(QLineF(rowPos + oct[0], rowPos + oct[1])) lines.append(QLineF(rowPos + oct[1], rowPos + oct[2])) lines.append(QLineF(rowPos + oct[3], rowPos + oct[4])) isStaggered = p.doStaggerY(startTile.y()) lastRow = rowTile.y() == self.map().height() - 1 lastColumn = rowTile.x() == self.map().width() - 1 bottomLeft = lastRow or (rowTile.x() == 0 and not isStaggered) bottomRight = lastRow or (lastColumn and isStaggered) if (lastColumn): lines.append(QLineF(rowPos + oct[4], rowPos + oct[5])) if (bottomRight): lines.append(QLineF(rowPos + oct[5], rowPos + oct[6])) if (bottomLeft): lines.append(QLineF(rowPos + oct[7], rowPos + oct[0])) painter.drawLines(lines) lines.resize(0) rowPos.setX(rowPos.x() + p.tileWidth + p.sideLengthX) rowTile.setX(rowTile.x() + 1) startPos.setY(startPos.y() + p.rowHeight) startTile.setY(startTile.y() + 1)