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))
class QtCursorDatabase(): def __init__(self): self.m_cursorNames = QList() self.m_cursorIcons = QMap() self.m_valueToCursorShape = QMap() self.m_cursorShapeToValue = QMap() self.appendCursor( Qt.ArrowCursor, QCoreApplication.translate("QtCursorDatabase", "Arrow"), QIcon( ":/qt-project.org/qtpropertybrowser/images/cursor-arrow.png")) self.appendCursor( Qt.UpArrowCursor, QCoreApplication.translate("QtCursorDatabase", "Up Arrow"), QIcon( ":/qt-project.org/qtpropertybrowser/images/cursor-uparrow.png") ) self.appendCursor( Qt.CrossCursor, QCoreApplication.translate("QtCursorDatabase", "Cross"), QIcon( ":/qt-project.org/qtpropertybrowser/images/cursor-cross.png")) self.appendCursor( Qt.WaitCursor, QCoreApplication.translate("QtCursorDatabase", "Wait"), QIcon(":/qt-project.org/qtpropertybrowser/images/cursor-wait.png")) self.appendCursor( Qt.IBeamCursor, QCoreApplication.translate("QtCursorDatabase", "IBeam"), QIcon( ":/qt-project.org/qtpropertybrowser/images/cursor-ibeam.png")) self.appendCursor( Qt.SizeVerCursor, QCoreApplication.translate("QtCursorDatabase", "Size Vertical"), QIcon( ":/qt-project.org/qtpropertybrowser/images/cursor-sizev.png")) self.appendCursor( Qt.SizeHorCursor, QCoreApplication.translate("QtCursorDatabase", "Size Horizontal"), QIcon( ":/qt-project.org/qtpropertybrowser/images/cursor-sizeh.png")) self.appendCursor( Qt.SizeFDiagCursor, QCoreApplication.translate("QtCursorDatabase", "Size Backslash"), QIcon( ":/qt-project.org/qtpropertybrowser/images/cursor-sizef.png")) self.appendCursor( Qt.SizeBDiagCursor, QCoreApplication.translate("QtCursorDatabase", "Size Slash"), QIcon( ":/qt-project.org/qtpropertybrowser/images/cursor-sizeb.png")) self.appendCursor( Qt.SizeAllCursor, QCoreApplication.translate("QtCursorDatabase", "Size All"), QIcon( ":/qt-project.org/qtpropertybrowser/images/cursor-sizeall.png") ) self.appendCursor( Qt.BlankCursor, QCoreApplication.translate("QtCursorDatabase", "Blank"), QIcon()) self.appendCursor( Qt.SplitVCursor, QCoreApplication.translate("QtCursorDatabase", "Split Vertical"), QIcon( ":/qt-project.org/qtpropertybrowser/images/cursor-vsplit.png")) self.appendCursor( Qt.SplitHCursor, QCoreApplication.translate("QtCursorDatabase", "Split Horizontal"), QIcon( ":/qt-project.org/qtpropertybrowser/images/cursor-hsplit.png")) self.appendCursor( Qt.PointingHandCursor, QCoreApplication.translate("QtCursorDatabase", "Pointing Hand"), QIcon(":/qt-project.org/qtpropertybrowser/images/cursor-hand.png")) self.appendCursor( Qt.ForbiddenCursor, QCoreApplication.translate("QtCursorDatabase", "Forbidden"), QIcon( ":/qt-project.org/qtpropertybrowser/images/cursor-forbidden.png" )) self.appendCursor( Qt.OpenHandCursor, QCoreApplication.translate("QtCursorDatabase", "Open Hand"), QIcon( ":/qt-project.org/qtpropertybrowser/images/cursor-openhand.png" )) self.appendCursor( Qt.ClosedHandCursor, QCoreApplication.translate("QtCursorDatabase", "Closed Hand"), QIcon( ":/qt-project.org/qtpropertybrowser/images/cursor-closedhand.png" )) self.appendCursor( Qt.WhatsThisCursor, QCoreApplication.translate("QtCursorDatabase", "What's This"), QIcon( ":/qt-project.org/qtpropertybrowser/images/cursor-whatsthis.png" )) self.appendCursor( Qt.BusyCursor, QCoreApplication.translate("QtCursorDatabase", "Busy"), QIcon(":/qt-project.org/qtpropertybrowser/images/cursor-busy.png")) def clear(self): self.m_cursorNames.clear() self.m_cursorIcons.clear() self.m_valueToCursorShape.clear() self.m_cursorShapeToValue.clear() def appendCursor(self, shape, name, icon): if self.m_cursorShapeToValue.get(shape): return value = len(self.m_cursorNames) self.m_cursorNames.append(name) self.m_cursorIcons[value] = icon self.m_valueToCursorShape[value] = shape self.m_cursorShapeToValue[shape] = value def cursorShapeNames(self): return self.m_cursorNames def cursorShapeIcons(self): return self.m_cursorIcons def cursorToShapeName(self, cursor): val = self.cursorToValue(cursor) if val >= 0: return self.m_cursorNames[val] return '' def cursorToShapeIcon(self, cursor): val = self.cursorToValue(cursor) return self.m_cursorIcons[val] def cursorToValue(self, cursor): shape = cursor.shape() return self.m_cursorShapeToValue.get(shape, -1) def valueToCursor(self, value): if value in self.m_valueToCursorShape: return QCursor(self.m_valueToCursorShape[value]) return QCursor()
class Tileset(Object): ## # Constructor. # # @param name the name of the tileset # @param tileWidth the width of the tiles in the tileset # @param tileHeight the height of the tiles in the tileset # @param tileSpacing the spacing between the tiles in the tileset image # @param margin the margin around the tiles in the tileset image ## def __init__(self, name, tileWidth, tileHeight, tileSpacing = 0, margin = 0): super().__init__(Object.TilesetType) self.mName = name self.mTileWidth = tileWidth self.mTileHeight = tileHeight self.mTileSpacing = tileSpacing self.mMargin = margin self.mImageWidth = 0 self.mImageHeight = 0 self.mColumnCount = 0 self.mTerrainDistancesDirty = False self.mTileOffset = QPoint() self.mFileName = QString() self.mTiles = QList() self.mTransparentColor = QColor() self.mImageSource = QString() self.mTerrainTypes = QList() self.mWeakPointer = None ## # Destructor. ## def __del__(self): self.mTiles.clear() self.mTerrainTypes.clear() def create(name, tileWidth, tileHeight, tileSpacing = 0, margin = 0): tileset = Tileset(name, tileWidth, tileHeight, tileSpacing, margin) tileset.mWeakPointer = tileset return tileset def __iter__(self): return self.mTiles.__iter__() ## # Returns the name of this tileset. ## def name(self): return self.mName ## # Sets the name of this tileset. ## def setName(self, name): self.mName = name ## # Returns the file name of this tileset. When the tileset isn't an # external tileset, the file name is empty. ## def fileName(self): return self.mFileName ## # Sets the filename of this tileset. ## def setFileName(self, fileName): self.mFileName = fileName ## # Returns whether this tileset is external. ## def isExternal(self): return self.mFileName!='' ## # Returns the maximum width of the tiles in this tileset. ## def tileWidth(self): return self.mTileWidth ## # Returns the maximum height of the tiles in this tileset. ## def tileHeight(self): return self.mTileHeight ## # Returns the maximum size of the tiles in this tileset. ## def tileSize(self): return QSize(self.mTileWidth, self.mTileHeight) ## # Returns the spacing between the tiles in the tileset image. ## def tileSpacing(self): return self.mTileSpacing ## # Returns the margin around the tiles in the tileset image. ## def margin(self): return self.mMargin ## # Returns the offset that is applied when drawing the tiles in this # tileset. ## def tileOffset(self): return self.mTileOffset ## # @see tileOffset ## def setTileOffset(self, offset): self.mTileOffset = offset ## # Returns a const reference to the list of tiles in this tileset. ## def tiles(self): return QList(self.mTiles) ## # Returns the tile for the given tile ID. # The tile ID is local to this tileset, which means the IDs are in range # [0, tileCount() - 1]. ## def tileAt(self, id): if id < self.mTiles.size(): return self.mTiles.at(id) return None ## # Returns the number of tiles in this tileset. ## def tileCount(self): return self.mTiles.size() ## # Returns the number of tile columns in the tileset image. ## def columnCount(self): return self.mColumnCount ## # Returns the width of the tileset image. ## def imageWidth(self): return self.mImageWidth ## # Returns the height of the tileset image. ## def imageHeight(self): return self.mImageHeight ## # Returns the transparent color, or an invalid color if no transparent # color is used. ## def transparentColor(self): return QColor(self.mTransparentColor) ## # Sets the transparent color. Pixels with this color will be masked out # when loadFromImage() is called. ## def setTransparentColor(self, c): self.mTransparentColor = c ## # Load this tileset from the given tileset \a image. This will replace # existing tile images in this tileset with new ones. If the new image # contains more tiles than exist in the tileset new tiles will be # appended, if there are fewer tiles the excess images will be blanked. # # The tile width and height of this tileset must be higher than 0. # # @param image the image to load the tiles from # @param fileName the file name of the image, which will be remembered # as the image source of this tileset. # @return <code>true</code> if loading was successful, otherwise # returns <code>false</code> ## def loadFromImage(self, *args): l = len(args) if l==2: image, fileName = args tileSize = self.tileSize() margin = self.margin() spacing = self.tileSpacing() if (image.isNull()): return False stopWidth = image.width() - tileSize.width() stopHeight = image.height() - tileSize.height() oldTilesetSize = self.tileCount() tileNum = 0 for y in range(margin, stopHeight+1, tileSize.height() + spacing): for x in range(margin, stopWidth+1, tileSize.width() + spacing): tileImage = image.copy(x, y, tileSize.width(), tileSize.height()) tilePixmap = QPixmap.fromImage(tileImage) if (self.mTransparentColor.isValid()): mask = tileImage.createMaskFromColor(self.mTransparentColor.rgb()) tilePixmap.setMask(QBitmap.fromImage(mask)) if (tileNum < oldTilesetSize): self.mTiles.at(tileNum).setImage(tilePixmap) else: self.mTiles.append(Tile(tilePixmap, tileNum, self)) tileNum += 1 # Blank out any remaining tiles to avoid confusion while (tileNum < oldTilesetSize): tilePixmap = QPixmap(tileSize) tilePixmap.fill() self.mTiles.at(tileNum).setImage(tilePixmap) tileNum += 1 self.mImageWidth = image.width() self.mImageHeight = image.height() self.mColumnCount = self.columnCountForWidth(self.mImageWidth) self.mImageSource = fileName return True elif l==1: ## # Convenience override that loads the image using the QImage constructor. ## fileName = args[0] return self.loadFromImage(QImage(fileName), fileName) ## # This checks if there is a similar tileset in the given list. # It is needed for replacing this tileset by its similar copy. ## def findSimilarTileset(self, tilesets): for candidate in tilesets: if (candidate.tileCount() != self.tileCount()): continue if (candidate.imageSource() != self.imageSource()): continue if (candidate.tileSize() != self.tileSize()): continue if (candidate.tileSpacing() != self.tileSpacing()): continue if (candidate.margin() != self.margin()): continue if (candidate.tileOffset() != self.tileOffset()): continue # For an image collection tileset, check the image sources if (self.imageSource()==''): if (not sameTileImages(self, candidate)): continue return candidate return None ## # Returns the file name of the external image that contains the tiles in # this tileset. Is an empty string when this tileset doesn't have a # tileset image. ## def imageSource(self): return self.mImageSource ## # Returns the column count that this tileset would have if the tileset # image would have the given \a width. This takes into account the tile # size, margin and spacing. ## def columnCountForWidth(self, width): return int((width - self.mMargin + self.mTileSpacing) / (self.mTileWidth + self.mTileSpacing)) ## # Returns a const reference to the list of terrains in this tileset. ## def terrains(self): return QList(self.mTerrainTypes) ## # Returns the number of terrain types in this tileset. ## def terrainCount(self): return self.mTerrainTypes.size() ## # Returns the terrain type at the given \a index. ## def terrain(self, index): if index >= 0: _x = self.mTerrainTypes[index] else: _x = None return _x ## # Adds a new terrain type. # # @param name the name of the terrain # @param imageTile the id of the tile that represents the terrain visually # @return the created Terrain instance ## def addTerrain(self, name, imageTileId): terrain = Terrain(self.terrainCount(), self, name, imageTileId) self.insertTerrain(self.terrainCount(), terrain) return terrain ## # Adds the \a terrain type at the given \a index. # # The terrain should already have this tileset associated with it. ## def insertTerrain(self, index, terrain): self.mTerrainTypes.insert(index, terrain) # Reassign terrain IDs for terrainId in range(index, self.mTerrainTypes.size()): self.mTerrainTypes.at(terrainId).mId = terrainId # Adjust tile terrain references for tile in self.mTiles: for corner in range(4): terrainId = tile.cornerTerrainId(corner) if (terrainId >= index): tile.setCornerTerrainId(corner, terrainId + 1) self.mTerrainDistancesDirty = True ## # Removes the terrain type at the given \a index and returns it. The # caller becomes responsible for the lifetime of the terrain type. # # This will cause the terrain ids of subsequent terrains to shift up to # fill the space and the terrain information of all tiles in this tileset # will be updated accordingly. ## def takeTerrainAt(self, index): terrain = self.mTerrainTypes.takeAt(index) # Reassign terrain IDs for terrainId in range(index, self.mTerrainTypes.size()): self.mTerrainTypes.at(terrainId).mId = terrainId # Clear and adjust tile terrain references for tile in self.mTiles: for corner in range(4): terrainId = tile.cornerTerrainId(corner) if (terrainId == index): tile.setCornerTerrainId(corner, 0xFF) elif (terrainId > index): tile.setCornerTerrainId(corner, terrainId - 1) self.mTerrainDistancesDirty = True return terrain ## # Returns the transition penalty(/distance) between 2 terrains. -1 if no # transition is possible. ## def terrainTransitionPenalty(self, terrainType0, terrainType1): if (self.mTerrainDistancesDirty): self.recalculateTerrainDistances() self.mTerrainDistancesDirty = False if terrainType0 == 255: terrainType0 = -1 if terrainType1 == 255: terrainType1 = -1 # Do some magic, since we don't have a transition array for no-terrain if (terrainType0 == -1 and terrainType1 == -1): return 0 if (terrainType0 == -1): return self.mTerrainTypes.at(terrainType1).transitionDistance(terrainType0) return self.mTerrainTypes.at(terrainType0).transitionDistance(terrainType1) ## # Adds a new tile to the end of the tileset. ## def addTile(self, image, source=QString()): newTile = Tile(image, source, self.tileCount(), self) self.mTiles.append(newTile) if (self.mTileHeight < image.height()): self.mTileHeight = image.height() if (self.mTileWidth < image.width()): self.mTileWidth = image.width() return newTile def insertTiles(self, index, tiles): count = tiles.count() for i in range(count): self.mTiles.insert(index + i, tiles.at(i)) # Adjust the tile IDs of the remaining tiles for i in range(index + count, self.mTiles.size()): self.mTiles.at(i).mId += count self.updateTileSize() def removeTiles(self, index, count): first = self.mTiles.begin() + index last = first + count last = self.mTiles.erase(first, last) # Adjust the tile IDs of the remaining tiles for last in self.mTiles: last.mId -= count self.updateTileSize() ## # Sets the \a image to be used for the tile with the given \a id. ## def setTileImage(self, id, image, source = QString()): # This operation is not supposed to be used on tilesets that are based # on a single image tile = self.tileAt(id) if (not tile): return previousImageSize = tile.image().size() newImageSize = image.size() tile.setImage(image) tile.setImageSource(source) if (previousImageSize != newImageSize): # Update our max. tile size if (previousImageSize.height() == self.mTileHeight or previousImageSize.width() == self.mTileWidth): # This used to be the max image; we have to recompute self.updateTileSize() else: # Check if we have a new maximum if (self.mTileHeight < newImageSize.height()): self.mTileHeight = newImageSize.height() if (self.mTileWidth < newImageSize.width()): self.mTileWidth = newImageSize.width() ## # Used by the Tile class when its terrain information changes. ## def markTerrainDistancesDirty(self): self.mTerrainDistancesDirty = True ## # Sets tile size to the maximum size. ## def updateTileSize(self): maxWidth = 0 maxHeight = 0 for tile in self.mTiles: size = tile.size() if (maxWidth < size.width()): maxWidth = size.width() if (maxHeight < size.height()): maxHeight = size.height() self.mTileWidth = maxWidth self.mTileHeight = maxHeight ## # Calculates the transition distance matrix for all terrain types. ## 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 sharedPointer(self): return self.mWeakPointer
class QtGroupBoxPropertyBrowserPrivate(): def __init__(self): self.q_ptr = None self.m_indexToItem = QMap() self.m_itemToIndex = QMap() self.m_widgetToItem = QMap() self.m_mainLayout = 0 self.m_children = QList() self.m_recreateQueue = QList() def createEditor(self, property, parent): return self.q_ptr.createEditor(property, parent) def init(self, parent): self.m_mainLayout = QGridLayout() parent.setLayout(self.m_mainLayout) item = QSpacerItem(0, 0, QSizePolicy.Fixed, QSizePolicy.Expanding) self.m_mainLayout.addItem(item, 0, 0) def slotEditorDestroyed(self): editor = self.q_ptr.sender() if (not editor): return if (not editor in self.m_widgetToItem.keys()): return self.m_widgetToItem[editor].widget = 0 self.m_widgetToItem.remove(editor) def slotUpdate(self): for item in self.m_recreateQueue: par = item.parent w = 0 l = 0 oldRow = -1 if (not par): w = self.q_ptr l = self.m_mainLayout oldRow = self.m_children.indexOf(item) else: w = par.groupBox l = par.layout oldRow = par.children.indexOf(item) if (self.hasHeader(par)): oldRow += 2 if (item.widget): item.widget.setParent(w) elif (item.widgetLabel): item.widgetLabel.setParent(w) else: item.widgetLabel = QLabel(w) item.widgetLabel.setSizePolicy(QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed)) item.widgetLabel.setTextFormat(Qt.PlainText) span = 1 if (item.widget): l.addWidget(item.widget, oldRow, 1, 1, 1) elif (item.widgetLabel): l.addWidget(item.widgetLabel, oldRow, 1, 1, 1) else: span = 2 item.label = QLabel(w) item.label.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)) l.addWidget(item.label, oldRow, 0, 1, span) self.updateItem(item) self.m_recreateQueue.clear() def updateLater(self): QTimer.singleShot(0, self.q_ptr, self.slotUpdate()) def propertyInserted(self, index, afterIndex): afterItem = self.m_indexToItem[afterIndex] parentItem = self.m_indexToItem.value(index.parent()) newItem = WidgetItem() newItem.parent = parentItem layout = 0 parentWidget = 0 row = -1 if (not afterItem): row = 0 if (parentItem): parentItem.children.insert(0, newItem) else: self.m_children.insert(0, newItem) else: if (parentItem): row = parentItem.children.indexOf(afterItem) + 1 parentItem.children.insert(row, newItem) else: row = self.m_children.indexOf(afterItem) + 1 self.m_children.insert(row, newItem) if (parentItem and self.hasHeader(parentItem)): row += 2 if (not parentItem): layout = self.m_mainLayout parentWidget = self.q_ptr else: if not parentItem.groupBox: self.m_recreateQueue.removeAll(parentItem) par = parentItem.parent w = 0 l = 0 oldRow = -1 if (not par): w = self.q_ptr l = self.m_mainLayout oldRow = self.m_children.indexOf(parentItem) else: w = par.groupBox l = par.layout oldRow = par.children.indexOf(parentItem) if (self.hasHeader(par)): oldRow += 2 parentItem.groupBox = QGroupBox(w) parentItem.layout = QGridLayout() parentItem.groupBox.setLayout(parentItem.layout) if (parentItem.label): l.removeWidget(parentItem.label) parentItem.label.close() parentItem.label = 0 if (parentItem.widget): l.removeWidget(parentItem.widget) parentItem.widget.setParent(parentItem.groupBox) parentItem.layout.addWidget(parentItem.widget, 0, 0, 1, 2) parentItem.line = QFrame(parentItem.groupBox) elif (parentItem.widgetLabel): l.removeWidget(parentItem.widgetLabel) parentItem.widgetLabel.close() parentItem.widgetLabel = 0 if (parentItem.line): parentItem.line.setFrameShape(QFrame.HLine) parentItem.line.setFrameShadow(QFrame.Sunken) parentItem.layout.addWidget(parentItem.line, 1, 0, 1, 2) l.addWidget(parentItem.groupBox, oldRow, 0, 1, 2) self.updateItem(parentItem) layout = parentItem.layout parentWidget = parentItem.groupBox newItem.label = QLabel(parentWidget) newItem.label.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)) newItem.widget = self.createEditor(index.property(), parentWidget) if (not newItem.widget): newItem.widgetLabel = QLabel(parentWidget) newItem.widgetLabel.setSizePolicy(QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed)) newItem.widgetLabel.setTextFormat(Qt.PlainText) else: newItem.widget.destroyed.connect(self.slotEditorDestroyed) self.m_widgetToItem[newItem.widget] = newItem self.insertRow(layout, row) span = 1 if (newItem.widget): layout.addWidget(newItem.widget, row, 1) elif (newItem.widgetLabel): layout.addWidget(newItem.widgetLabel, row, 1) else: span = 2 layout.addWidget(newItem.label, row, 0, 1, span) self.m_itemToIndex[newItem] = index self.m_indexToItem[index] = newItem self.updateItem(newItem) def propertyRemoved(self, index): item = self.m_indexToItem[index] self.m_indexToItem.remove(index) self.m_itemToIndex.remove(item) parentItem = item.parent row = -1 if (parentItem): row = parentItem.children.indexOf(item) parentItem.children.removeAt(row) if (self.hasHeader(parentItem)): row += 2 else: row = self.m_children.indexOf(item) self.m_children.removeAt(row) if (item.widget): item.widget.close() del item.widget if (item.label): item.label.close() del item.label if (item.widgetLabel): item.widgetLabel.close() del item.widgetLabel if (item.groupBox): item.groupBox.close() del item.groupBox if (not parentItem): self.removeRow(self.m_mainLayout, row) elif len(parentItem.children) > 0: self.removeRow(parentItem.layout, row) else: par = parentItem.parent l = 0 oldRow = -1 if (not par): l = self.m_mainLayout oldRow = self.m_children.indexOf(parentItem) else: l = par.layout oldRow = par.children.indexOf(parentItem) if (self.hasHeader(par)): oldRow += 2 if (parentItem.widget): parentItem.widget.hide() parentItem.widget.setParent(0) elif (parentItem.widgetLabel): parentItem.widgetLabel.hide() parentItem.widgetLabel.setParent(0) else: #parentItem.widgetLabel = QLabel(w) pass l.removeWidget(parentItem.groupBox) parentItem.groupBox.close() parentItem.groupBox = 0 parentItem.line = 0 parentItem.layout = 0 if (not parentItem in self.m_recreateQueue): self.m_recreateQueue.append(parentItem) self.updateLater() self.m_recreateQueue.removeAll(item) del item def insertRow(self, layout, row): itemToPos = QMap() idx = 0 while (idx < layout.count()): r, c, rs, cs = layout.getItemPosition(idx) if (r >= row): itemToPos[layout.takeAt(idx)] = QRect(r + 1, c, rs, cs) else: idx += 1 for it in itemToPos.keys(): r = itemToPos[it] layout.addItem(it, r.x(), r.y(), r.width(), r.height()) def removeRow(self, layout, row): itemToPos = QMap() idx = 0 while (idx < layout.count()): r, c, rs, cs = layout.getItemPosition(idx) if (r > row): itemToPos[layout.takeAt(idx)] = QRect(r - 1, c, rs, cs) else: idx += 1 for it in itemToPos.keys(): r = itemToPos[it] layout.addItem(it, r.x(), r.y(), r.width(), r.height()) def hasHeader(self, item): if (item.widget): return True return False def propertyChanged(self, index): item = self.m_indexToItem[index] self.updateItem(item) def updateItem(self, item): property = self.m_itemToIndex[item].property() if (item.groupBox): font = item.groupBox.font() font.setUnderline(property.isModified()) item.groupBox.setFont(font) item.groupBox.setTitle(property.propertyName()) item.groupBox.setToolTip(property.toolTip()) item.groupBox.setStatusTip(property.statusTip()) item.groupBox.setWhatsThis(property.whatsThis()) item.groupBox.setEnabled(property.isEnabled()) if (item.label): font = item.label.font() font.setUnderline(property.isModified()) item.label.setFont(font) item.label.setText(property.propertyName()) item.label.setToolTip(property.toolTip()) item.label.setStatusTip(property.statusTip()) item.label.setWhatsThis(property.whatsThis()) item.label.setEnabled(property.isEnabled()) if (item.widgetLabel): font = item.widgetLabel.font() font.setUnderline(False) item.widgetLabel.setFont(font) item.widgetLabel.setText(property.valueText()) item.widgetLabel.setToolTip(property.valueText()) item.widgetLabel.setEnabled(property.isEnabled()) if (item.widget): font = item.widget.font() font.setUnderline(False) item.widget.setFont(font) item.widget.setEnabled(property.isEnabled()) item.widget.setToolTip(property.valueText())
class Map(Object): ## # The orientation of the map determines how it should be rendered. An # Orthogonal map is using rectangular tiles that are aligned on a # straight grid. An Isometric map uses diamond shaped tiles that are # aligned on an isometric projected grid. A Hexagonal map uses hexagon # shaped tiles that fit into each other by shifting every other row. ## class Orientation(Enum): Unknown, Orthogonal, Isometric, Staggered, Hexagonal = range(5) ## # The different formats in which the tile layer data can be stored. ## class LayerDataFormat(Enum): XML = 0 Base64 = 1 Base64Gzip = 2 Base64Zlib = 3 CSV = 4 ## # The order in which tiles are rendered on screen. ## class RenderOrder(Enum): RightDown = 0 RightUp = 1 LeftDown = 2 LeftUp = 3 ## # Which axis is staggered. Only used by the isometric staggered and # hexagonal map renderers. ## class StaggerAxis(Enum): StaggerX, StaggerY = range(2) ## # When staggering, specifies whether the odd or the even rows/columns are # shifted half a tile right/down. Only used by the isometric staggered and # hexagonal map renderers. ## class StaggerIndex(Enum): StaggerOdd = 0 StaggerEven = 1 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 ## # Destructor. ## def __del__(self): self.mLayers.clear() ## # Returns the orientation of the map. ## def orientation(self): return self.mOrientation ## # Sets the orientation of the map. ## def setOrientation(self, orientation): self.mOrientation = orientation ## # Returns the render order of the map. ## def renderOrder(self): return self.mRenderOrder ## # Sets the render order of the map. ## def setRenderOrder(self, renderOrder): self.mRenderOrder = renderOrder ## # Returns the width of this map in tiles. ## def width(self): return self.mWidth ## # Sets the width of this map in tiles. ## def setWidth(self, width): self.mWidth = width ## # Returns the height of this map in tiles. ## def height(self): return self.mHeight ## # Sets the height of this map in tiles. ## def setHeight(self, height): self.mHeight = height ## # Returns the size of this map. Provided for convenience. ## def size(self): return QSize(self.mWidth, self.mHeight) ## # Returns the tile width of this map. ## def tileWidth(self): return self.mTileWidth ## # Sets the width of one tile. ## def setTileWidth(self, width): self.mTileWidth = width ## # Returns the tile height used by this map. ## def tileHeight(self): return self.mTileHeight ## # Sets the height of one tile. ## def setTileHeight(self, height): self.mTileHeight = height ## # Returns the size of one tile. Provided for convenience. ## def tileSize(self): return QSize(self.mTileWidth, self.mTileHeight) def hexSideLength(self): return self.mHexSideLength def setHexSideLength(self, hexSideLength): self.mHexSideLength = hexSideLength def staggerAxis(self): return self.mStaggerAxis def setStaggerAxis(self, staggerAxis): self.mStaggerAxis = staggerAxis def staggerIndex(self): return self.mStaggerIndex def setStaggerIndex(self, staggerIndex): self.mStaggerIndex = staggerIndex ## # Adjusts the draw margins to be at least as big as the given margins. # Called from tile layers when their tiles change. ## def adjustDrawMargins(self, margins): # The TileLayer includes the maximum tile size in its draw margins. So # we need to subtract the tile size of the map, since that part does not # contribute to additional margin. self.mDrawMargins = maxMargins(QMargins(margins.left(), margins.top() - self.mTileHeight, margins.right() - self.mTileWidth, margins.bottom()), self.mDrawMargins) ## # Computes the extra margins due to layer offsets. These need to be taken into # account when determining the bounding rect of the map for example. ## def computeLayerOffsetMargins(self): offsetMargins = QMargins() for layer in self.mLayers: offset = layer.offset() offsetMargins = maxMargins(QMargins(math.ceil(-offset.x()), math.ceil(-offset.y()), math.ceil(offset.x()), math.ceil(offset.y())), offsetMargins) return offsetMargins ## # Returns the margins that have to be taken into account when figuring # out which part of the map to repaint after changing some tiles. # # @see TileLayer.drawMargins ## def drawMargins(self): return self.mDrawMargins ## # Recomputes the draw margins for this map and each of its tile layers. Needed # after the tile offset of a tileset has changed for example. # # \sa TileLayer.recomputeDrawMargins ## def recomputeDrawMargins(self): self.mDrawMargins = QMargins() for layer in self.mLayers: tileLayer = layer.asTileLayer() if tileLayer: tileLayer.recomputeDrawMargins() ## # Returns the number of layers of this map. ## def layerCount(self, *args): l = len(args) if l==0: return self.mLayers.size() elif l==1: ## # Convenience function that returns the number of layers of this map that # match the given \a type. ## tp = args[0] count = 0 for layer in self.mLayers: if (layer.layerType() == tp): count += 1 return count def tileLayerCount(self): return self.layerCount(Layer.TileLayerType) def objectGroupCount(self): return self.layerCount(Layer.ObjectGroupType) def imageLayerCount(self): return self.layerCount(Layer.ImageLayerType) ## # Returns the layer at the specified index. ## def layerAt(self, index): return self.mLayers.at(index) ## # Returns the list of layers of this map. This is useful when you want to # use foreach. ## def layers(self, *args): l = len(args) if l==0: return QList(self.mLayers) elif l==1: tp = args[0] layers = QList() for layer in self.mLayers: if (layer.layerType() == tp): layers.append(layer) return layers def objectGroups(self): layers = QList() for layer in self.mLayers: og = layer.asObjectGroup() if og: layers.append(og) return layers def tileLayers(self): layers = QList() for layer in self.mLayers: tl = layer.asTileLayer() if tl: layers.append(tl) return layers ## # Adds a layer to this map. ## def addLayer(self, layer): self.adoptLayer(layer) self.mLayers.append(layer) ## # Returns the index of the layer given by \a layerName, or -1 if no # layer with that name is found. # # The second optional parameter specifies the layer types which are # searched. ## def indexOfLayer(self, layerName, layertypes = Layer.AnyLayerType): for index in range(self.mLayers.size()): if (self.layerAt(index).name() == layerName and (layertypes & self.layerAt(index).layerType())): return index return -1 ## # Adds a layer to this map, inserting it at the given index. ## def insertLayer(self, index, layer): self.adoptLayer(layer) self.mLayers.insert(index, layer) ## # Removes the layer at the given index from this map and returns it. # The caller becomes responsible for the lifetime of this layer. ## def takeLayerAt(self, index): layer = self.mLayers.takeAt(index) layer.setMap(None) return layer ## # Adds a tileset to this map. The map does not take ownership over its # tilesets, this is merely for keeping track of which tilesets are used by # the map, and their saving order. # # @param tileset the tileset to add ## def addTileset(self, tileset): self.mTilesets.append(tileset) ## # Convenience function to be used together with Layer.usedTilesets() ## def addTilesets(self, tilesets): for tileset in tilesets: self.addTileset(tileset) ## # Inserts \a tileset at \a index in the list of tilesets used by this map. ## def insertTileset(self, index, tileset): self.mTilesets.insert(index, tileset) ## # Returns the index of the given \a tileset, or -1 if it is not used in # this map. ## def indexOfTileset(self, tileset): return self.mTilesets.indexOf(tileset) ## # Removes the tileset at \a index from this map. # # \warning Does not make sure that this map no longer refers to tiles from # the removed tileset! # # \sa addTileset ## def removeTilesetAt(self, index): self.mTilesets.removeAt(index) ## # Replaces all tiles from \a oldTileset with tiles from \a newTileset. # Also replaces the old tileset with the new tileset in the list of # tilesets. ## def replaceTileset(self, oldTileset, newTileset): index = self.mTilesets.indexOf(oldTileset) for layer in self.mLayers: layer.replaceReferencesToTileset(oldTileset, newTileset) self.mTilesets[index] = newTileset ## # Returns the number of tilesets of this map. ## def tilesetCount(self): return self.mTilesets.size() ## # Returns the tileset at the given index. ## def tilesetAt(self, index): return self.mTilesets.at(index) ## # Returns the tilesets that the tiles on this map are using. ## def tilesets(self): return QList(self.mTilesets) ## # Returns the background color of this map. ## def backgroundColor(self): return QColor(self.mBackgroundColor) ## # Sets the background color of this map. ## def setBackgroundColor(self, color): self.mBackgroundColor = color ## # Returns whether the given \a tileset is used by any tile layer of this # map. ## def isTilesetUsed(self, tileset): for layer in self.mLayers: if (layer.referencesTileset(tileset)): return True return False ## # Creates a new map that contains the given \a layer. The map size will be # determined by the size of the layer. # # The orientation defaults to Unknown and the tile width and height will # default to 0. In case this map needs to be rendered, these properties # will need to be properly set. ## def fromLayer(layer): result = Map(Map.Orientation.Unknown, layer.width(), layer.height(), 0, 0) result.addLayer(layer) return result def layerDataFormat(self): return self.mLayerDataFormat def setLayerDataFormat(self, format): self.mLayerDataFormat = format ## # Sets the next id to be used for objects on this map. ## def setNextObjectId(self, nextId): self.mNextObjectId = nextId ## # Returns the next object id for this map. ## def nextObjectId(self): return self.mNextObjectId ## # Returns the next object id for this map and allocates a new one. ## def takeNextObjectId(self): return self.mNextObjectId+1 def adoptLayer(self, layer): layer.setMap(self) tileLayer = layer.asTileLayer() if tileLayer: self.adjustDrawMargins(tileLayer.drawMargins()) group = layer.asObjectGroup() if group: for o in group.objects(): if (o.id() == 0): o.setId(self.takeNextObjectId())
class PropertyWidget(QDockWidget, Ui_PropertyWidget): itemUpdated = pyqtSignal(int) nameUpdated = pyqtSignal(int) def __init__(self, parent=None): super(PropertyWidget, self).__init__(parent) self.setupUi(self) self.groupManager = QtGroupPropertyManager(self) self.boolManager = QtBoolPropertyManager(self) self.intManager = QtIntPropertyManager(self) self.doubleManager = QtDoublePropertyManager(self) self.stringManager = QtStringPropertyManager(self) self.colorManager = QtColorPropertyManager(self) self.enumManager = QtEnumPropertyManager(self) self.pointManager = QtPointFPropertyManager(self) self.boolManager.valueChangedSignal.connect(self.valueChanged) self.intManager.valueChangedSignal.connect(self.valueChanged) self.doubleManager.valueChangedSignal.connect(self.valueChanged) self.stringManager.valueChangedSignal.connect(self.valueChanged) self.colorManager.valueChangedSignal.connect(self.valueChanged) self.enumManager.valueChangedSignal.connect(self.valueChanged) self.pointManager.valueChangedSignal.connect(self.valueChanged) checkBoxFactory = QtCheckBoxFactory(self) spinBoxFactory = QtSpinBoxFactory(self) doubleSpinBoxFactory = QtDoubleSpinBoxFactory(self) lineEditFactory = QtLineEditFactory(self) comboBoxFactory = QtEnumEditorFactory(self) charEditfactory = QtCharEditorFactory(self) self.prop_browser.setFactoryForManager(\ self.boolManager, checkBoxFactory) self.prop_browser.setFactoryForManager(\ self.intManager, spinBoxFactory) self.prop_browser.setFactoryForManager(\ self.doubleManager, doubleSpinBoxFactory) self.prop_browser.setFactoryForManager(\ self.stringManager, lineEditFactory) self.prop_browser.setFactoryForManager(\ self.colorManager.subIntPropertyManager(), spinBoxFactory) self.prop_browser.setFactoryForManager(\ self.enumManager, comboBoxFactory) self.prop_browser.setFactoryForManager(\ self.pointManager.subDoublePropertyManager(), doubleSpinBoxFactory) self.selectedNodeID_ = -1 # for CS Nodes #self.CS_ = {} self.EnumToCSID_ = {} self.CSIDtoEnum_ = {} self.CSNames_ = QList() # for Material Nodes self.EnumToMatID_ = {} self.MatIDtoEnum_ = {} self.MatNames_ = QList() # Slot functions ----------------------------------------------------------- @pyqtSlot(QList) def nodesSelected(self, nodeIDs): self.prop_browser.clear() nodeID = nodeIDs.last() if nodeID == None: return node = bzmag.getObject(nodeID) if node == None: self.selectedNode_ = None self.line_path.setText('') return path = node.getAbsolutePath() #self.CS_.clear() self.EnumToCSID_.clear() self.CSIDtoEnum_.clear() self.CSNames_.clear() self.EnumToMatID_.clear() self.MatIDtoEnum_.clear() self.MatNames_.clear() cs_root = bzmag.get('/coordinate') self.build_ObjectItem(cs_root, self.CSIDtoEnum_, self.EnumToCSID_, self.CSNames_, 'Global') mat_root = bzmag.get('/material') self.build_ObjectItem(mat_root, self.MatIDtoEnum_, self.EnumToMatID_, self.MatNames_, 'Vaccum') if path == '' : path = '/' self.line_path.setText(path) self.line_path.setReadOnly(True) #print('Binding node path', path) #node = bzmag.get(path) self.selectedNodeID_ = nodeID for type_name in node.getGenerations(): group = self.groupManager.addProperty(type_name) prop_names = node.getPropertyNames(type_name) if not prop_names: continue for prop_name in prop_names: prop_name, prop_value, prop_type, readonly = \ node.getProperty(prop_name) #print(prop_name, prop_value, prop_type, readonly) self.add_property(group, prop_name, prop_value, prop_type, readonly) #print('Property Browse : Add Property...', group) self.prop_browser.addProperty(group) # ------------------------------------------------------------------------ def valueChanged(self, property, value): if self.selectedNodeID_ == -1: return nodeID = self.selectedNodeID_ node = bzmag.getObject(nodeID) prop_name = property.propertyName() prop_name, prop_value, prop_type, readonly = node.getProperty(prop_name) #print(prop_name, prop_value, prop_type, readonly) # when type of the value is 'QColor', make value as '[r, g, b, a]' #if type(value).__name__ == 'QColor': if prop_type == 'color': value = '{}, {}, {}, {}'.format(value.red(), value.green(), \ value.blue(), value.alpha()) if prop_type == 'node': #print('Node value changed : ', value) #if value in self.EnumToCSID_.keys(): #value = self.EnumToCSID_[value] if prop_name == 'CoordinateSystem': value = self.EnumToCSID_[value] elif prop_name == 'Material': value = self.EnumToMatID_[value] # when the node is not readonly , update the property value if not readonly: #print(node, prop_name, str(value)) node.setProperty(str(prop_name), str(value)) # when the property is 'name' update NOHTree and Node Path if prop_name == 'name': path = node.getAbsolutePath() self.nameUpdated.emit(nodeID) self.line_path.setText(path) self.itemUpdated.emit(nodeID) # private members ----------------------------------------------------------- def add_property(self, group, name, value, type, readonly): # type is boolean if type == 'bool': item = self.boolManager.addProperty(name) self.boolManager.setValue(item, (value == 'true')) #self.boolManager.setReadOnly(item, readonly) group.addSubProperty(item) # type is integer types elif type == 'int' or type == 'uint' or type == 'int16' or type == 'uint16' or\ type == 'int32' or type == 'uint32' or type == 'int64' or type == 'uint64': item = self.intManager.addProperty(name) #self.intManager.setRange(item, 0, 256) self.intManager.setValue(item, int(value)) self.intManager.setReadOnly(item, readonly) group.addSubProperty(item) # type is floating types (float32 or float64) elif type == 'float32' or type == 'float64': item = self.doubleManager.addProperty(self.tr(name)) self.doubleManager.setValue(item, float(value)) #self.doubleManager.setRange(item, 0, 256) self.doubleManager.setReadOnly(item, readonly) group.addSubProperty(item) # type is string types (stirng or uri) elif type == 'string' or type == 'uri': item = self.stringManager.addProperty(name) self.stringManager.setValue(item, value) self.stringManager.setReadOnly(item, readonly) group.addSubProperty(item) if name == 'name': regExp = QRegExp('^[a-z + A-Z + _]+\\w+$') self.stringManager.setRegExp(item, regExp) # type == color elif type == 'color': item = self.colorManager.addProperty(name) rgba = value.split(',') color = QColor(int(rgba[0]), int(rgba[1]), int(rgba[2]), int(rgba[3])) self.colorManager.setValue(item, color) group.addSubProperty(item) # type == vector2 elif type == 'vector2': item = self.pointManager.addProperty(name) xy = value.split(',') pt = QPointF(float(xy[0]), float(xy[1])) self.pointManager.setValue(item, pt) group.addSubProperty(item) # type == node or object elif type == 'object' or type == 'node': NodeID = int(value) if name == 'CoordinateSystem': item = self.enumManager.addProperty(name) self.enumManager.setEnumNames(item, self.CSNames_) self.enumManager.setValue(item, self.CSIDtoEnum_[NodeID]) group.addSubProperty(item) elif name == 'Material': item = self.enumManager.addProperty(name) self.enumManager.setEnumNames(item, self.MatNames_) self.enumManager.setValue(item, self.MatIDtoEnum_[NodeID]) group.addSubProperty(item) # ------------------------------------------------------------------------ def build_ObjectItem(self, parent, IDtoEnum, EnumToID, Name, DefaultName, index = 0): if index == 0: IDtoEnum[-1] = index EnumToID[index] = -1 Name.append(DefaultName) index = index + 1 for node in parent.getChildren(): IDtoEnum[node.getID()] = index EnumToID[index] = node.getID() Name.append(node.getName()) index = index + 1 self.build_ObjectItem(node, IDtoEnum, EnumToID, Name, DefaultName, index)
class AutoMapper(QObject): ## # Constructs an AutoMapper. # All data structures, which only rely on the rules map are setup # here. # # @param workingDocument: the map to work on. # @param rules: The rule map which should be used for automapping # @param rulePath: The filepath to the rule map. ## def __init__(self, workingDocument, rules, rulePath): ## # where to work in ## self.mMapDocument = workingDocument ## # the same as mMapDocument.map() ## self.mMapWork = None if workingDocument: self.mMapWork = workingDocument.map() ## # map containing the rules, usually different than mMapWork ## self.mMapRules = rules ## # This contains all added tilesets as pointers. # if rules use Tilesets which are not in the mMapWork they are added. # keep track of them, because we need to delete them afterwards, # when they still are unused # they will be added while setupTilesets(). ## self.mAddedTilesets = QVector() ## # description see: mAddedTilesets, just described by Strings ## self.mAddedTileLayers = QList() ## # Points to the tilelayer, which defines the inputregions. ## self.mLayerInputRegions = None ## # Points to the tilelayer, which defines the outputregions. ## self.mLayerOutputRegions = None ## # Contains all tilelayer pointers, which names begin with input* # It is sorted by index and name ## self.mInputRules = InputLayers() ## # List of Regions in mMapRules to know where the input rules are ## self.mRulesInput = QList() ## # List of regions in mMapRules to know where the output of a # rule is. # mRulesOutput[i] is the output of that rule, # which has the input at mRulesInput[i], meaning that mRulesInput # and mRulesOutput must match with the indexes. ## self.mRulesOutput = QList() ## # The inner set with layers to indexes is needed for translating # tile layers from mMapRules to mMapWork. # # The key is the pointer to the layer in the rulemap. The # pointer to the layer within the working map is not hardwired, but the # position in the layerlist, where it was found the last time. # This loosely bound pointer ensures we will get the right layer, since we # need to check before anyway, and it is still fast. # # The list is used to hold different translation tables # => one of the tables is chosen by chance, so randomness is available ## self.mLayerList = QList() ## # store the name of the processed rules file, to have detailed # error messages available ## self.mRulePath = rulePath ## # determines if all tiles in all touched layers should be deleted first. ## self.mDeleteTiles = False ## # This variable determines, how many overlapping tiles should be used. # The bigger the more area is remapped at an automapping operation. # This can lead to higher latency, but provides a better behavior on # interactive automapping. # It defaults to zero. ## self.mAutoMappingRadius = 0 ## # Determines if a rule is allowed to overlap it ## self.mNoOverlappingRules = False self.mTouchedObjectGroups = QSet() self.mWarning = QString() self.mTouchedTileLayers = QSet() self.mError = '' if (not self.setupRuleMapProperties()): return if (not self.setupRuleMapTileLayers()): return if (not self.setupRuleList()): return def __del__(self): self.cleanUpRulesMap() ## # Checks if the passed \a ruleLayerName is used in this instance # of Automapper. ## def ruleLayerNameUsed(self, ruleLayerName): return self.mInputRules.names.contains(ruleLayerName) ## # Call prepareLoad first! Returns a set of strings describing the tile # layers, which could be touched considering the given layers of the # rule map. ## def getTouchedTileLayers(self): return self.mTouchedTileLayers ## # This needs to be called directly before the autoMap call. # It sets up some data structures which change rapidly, so it is quite # painful to keep these datastructures up to date all time. (indices of # layers of the working map) ## def prepareAutoMap(self): self.mError = '' self.mWarning = '' if (not self.setupMissingLayers()): return False if (not self.setupCorrectIndexes()): return False if (not self.setupTilesets(self.mMapRules, self.mMapWork)): return False return True ## # Here is done all the automapping. ## def autoMap(self, where): # first resize the active area if (self.mAutoMappingRadius): region = QRegion() for r in where.rects(): region += r.adjusted(-self.mAutoMappingRadius, -self.mAutoMappingRadius, +self.mAutoMappingRadius, +self.mAutoMappingRadius) #where += region # delete all the relevant area, if the property "DeleteTiles" is set if (self.mDeleteTiles): setLayersRegion = self.getSetLayersRegion() for i in range(self.mLayerList.size()): translationTable = self.mLayerList.at(i) for layer in translationTable.keys(): index = self.mLayerList.at(i).value(layer) dstLayer = self.mMapWork.layerAt(index) region = setLayersRegion.intersected(where) dstTileLayer = dstLayer.asTileLayer() if (dstTileLayer): dstTileLayer.erase(region) else: self.eraseRegionObjectGroup(self.mMapDocument, dstLayer.asObjectGroup(), region) # Increase the given region where the next automapper should work. # This needs to be done, so you can rely on the order of the rules at all # locations ret = QRegion() for rect in where.rects(): for i in range(self.mRulesInput.size()): # at the moment the parallel execution does not work yet # TODO: make multithreading available! # either by dividing the rules or the region to multiple threads ret = ret.united(self.applyRule(i, rect)) #where = where.united(ret) ## # This cleans all datastructures, which are setup via prepareAutoMap, # so the auto mapper becomes ready for its next automatic mapping. ## def cleanAll(self): self.cleanTilesets() self.cleanTileLayers() ## # Contains all errors until operation was canceled. # The errorlist is cleared within prepareLoad and prepareAutoMap. ## def errorString(self): return self.mError ## # Contains all warnings which occur at loading a rules map or while # automapping. # The errorlist is cleared within prepareLoad and prepareAutoMap. ## def warningString(self): return self.mWarning ## # Reads the map properties of the rulesmap. # @return returns True when anything is ok, False when errors occured. ## def setupRuleMapProperties(self): properties = self.mMapRules.properties() for key in properties.keys(): value = properties.value(key) raiseWarning = True if (key.toLower() == "deletetiles"): if (value.canConvert(QVariant.Bool)): self.mDeleteTiles = value.toBool() raiseWarning = False elif (key.toLower() == "automappingradius"): if (value.canConvert(QVariant.Int)): self.mAutoMappingRadius = value raiseWarning = False elif (key.toLower() == "nooverlappingrules"): if (value.canConvert(QVariant.Bool)): self.mNoOverlappingRules = value.toBool() raiseWarning = False if (raiseWarning): self.mWarning += self.tr( "'%s': Property '%s' = '%s' does not make sense. \nIgnoring this property." % (self.mRulePath, key, value.toString()) + '\n') return True def cleanUpRulesMap(self): self.cleanTilesets() # mMapRules can be empty, when in prepareLoad the very first stages fail. if (not self.mMapRules): return tilesetManager = TilesetManager.instance() tilesetManager.removeReferences(self.mMapRules.tilesets()) del self.mMapRules self.mMapRules = None self.cleanUpRuleMapLayers() self.mRulesInput.clear() self.mRulesOutput.clear() ## # Searches the rules layer for regions and stores these in \a rules. # @return returns True when anything is ok, False when errors occured. ## def setupRuleList(self): combinedRegions = coherentRegions(self.mLayerInputRegions.region() + self.mLayerOutputRegions.region()) combinedRegions = QList( sorted(combinedRegions, key=lambda x: x.y(), reverse=True)) rulesInput = coherentRegions(self.mLayerInputRegions.region()) rulesOutput = coherentRegions(self.mLayerOutputRegions.region()) for i in range(combinedRegions.size()): self.mRulesInput.append(QRegion()) self.mRulesOutput.append(QRegion()) for reg in rulesInput: for i in range(combinedRegions.size()): if (reg.intersects(combinedRegions[i])): self.mRulesInput[i] += reg break for reg in rulesOutput: for i in range(combinedRegions.size()): if (reg.intersects(combinedRegions[i])): self.mRulesOutput[i] += reg break for i in range(self.mRulesInput.size()): checkCoherent = self.mRulesInput.at(i).united( self.mRulesOutput.at(i)) coherentRegions(checkCoherent).length() == 1 return True ## # Sets up the layers in the rules map, which are used for automapping. # The layers are detected and put in the internal data structures # @return returns True when anything is ok, False when errors occured. ## def setupRuleMapTileLayers(self): error = QString() for layer in self.mMapRules.layers(): layerName = layer.name() if (layerName.lower().startswith("regions")): treatAsBoth = layerName.toLower() == "regions" if (layerName.lower().endswith("input") or treatAsBoth): if (self.mLayerInputRegions): error += self.tr( "'regions_input' layer must not occur more than once.\n" ) if (layer.isTileLayer()): self.mLayerInputRegions = layer.asTileLayer() else: error += self.tr( "'regions_*' layers must be tile layers.\n") if (layerName.lower().endswith("output") or treatAsBoth): if (self.mLayerOutputRegions): error += self.tr( "'regions_output' layer must not occur more than once.\n" ) if (layer.isTileLayer()): self.mLayerOutputRegions = layer.asTileLayer() else: error += self.tr( "'regions_*' layers must be tile layers.\n") continue nameStartPosition = layerName.indexOf('_') + 1 # name is all characters behind the underscore (excluded) name = layerName.right(layerName.size() - nameStartPosition) # group is all before the underscore (included) index = layerName.left(nameStartPosition) if (index.lower().startswith("output")): index.remove(0, 6) elif (index.lower().startswith("inputnot")): index.remove(0, 8) elif (index.lower().startswith("input")): index.remove(0, 5) # both 'rule' and 'output' layers will require and underscore and # rely on the correct position detected of the underscore if (nameStartPosition == 0): error += self.tr( "Did you forget an underscore in layer '%d'?\n" % layerName) continue if (layerName.startsWith("input", Qt.CaseInsensitive)): isNotList = layerName.lower().startswith("inputnot") if (not layer.isTileLayer()): error += self.tr( "'input_*' and 'inputnot_*' layers must be tile layers.\n" ) continue self.mInputRules.names.insert(name) if (not self.mInputRules.indexes.contains(index)): self.mInputRules.indexes.insert(index) self.mInputRules.insert(index, InputIndex()) if (not self.mInputRules[index].names.contains(name)): self.mInputRules[index].names.insert(name) self.mInputRules[index].insert(name, InputIndexName()) if (isNotList): self.mInputRules[index][name].listNo.append( layer.asTileLayer()) else: self.mInputRules[index][name].listYes.append( layer.asTileLayer()) continue if layerName.lower().startswith("output"): if (layer.isTileLayer()): self.mTouchedTileLayers.insert(name) else: self.mTouchedObjectGroups.insert(name) type = layer.layerType() layerIndex = self.mMapWork.indexOfLayer(name, type) found = False for translationTable in self.mLayerList: if (translationTable.index == index): translationTable.insert(layer, layerIndex) found = True break if (not found): self.mLayerList.append(RuleOutput()) self.mLayerList.last().insert(layer, layerIndex) self.mLayerList.last().index = index continue error += self.tr( "Layer '%s' is not recognized as a valid layer for Automapping.\n" % layerName) if (not self.mLayerInputRegions): error += self.tr("No 'regions' or 'regions_input' layer found.\n") if (not self.mLayerOutputRegions): error += self.tr("No 'regions' or 'regions_output' layer found.\n") if (self.mInputRules.isEmpty()): error += self.tr("No input_<name> layer found!\n") # no need to check for mInputNotRules.size() == 0 here. # these layers are not necessary. if error != '': error = self.mRulePath + '\n' + error self.mError += error return False return True ## # Checks if all needed layers in the working map are there. # If not, add them in the correct order. ## def setupMissingLayers(self): # make sure all needed layers are there: for name in self.mTouchedTileLayers: if (self.mMapWork.indexOfLayer(name, Layer.TileLayerType) != -1): continue index = self.mMapWork.layerCount() tilelayer = TileLayer(name, 0, 0, self.mMapWork.width(), self.mMapWork.height()) self.mMapDocument.undoStack().push( AddLayer(self.mMapDocument, index, tilelayer)) self.mAddedTileLayers.append(name) for name in self.mTouchedObjectGroups: if (self.mMapWork.indexOfLayer(name, Layer.ObjectGroupType) != -1): continue index = self.mMapWork.layerCount() objectGroup = ObjectGroup(name, 0, 0, self.mMapWork.width(), self.mMapWork.height()) self.mMapDocument.undoStack().push( AddLayer(self.mMapDocument, index, objectGroup)) self.mAddedTileLayers.append(name) return True ## # Checks if the layers setup as in setupRuleMapLayers are still right. # If it's not right, correct them. # @return returns True if everything went fine. False is returned when # no set layer was found ## def setupCorrectIndexes(self): # make sure all indexes of the layer translationtables are correct. for i in range(self.mLayerList.size()): translationTable = self.mLayerList.at(i) for layerKey in translationTable.keys(): name = layerKey.name() pos = name.indexOf('_') + 1 name = name.right(name.length() - pos) index = translationTable.value(layerKey, -1) if (index >= self.mMapWork.layerCount() or index == -1 or name != self.mMapWork.layerAt(index).name()): newIndex = self.mMapWork.indexOfLayer( name, layerKey.layerType()) translationTable.insert(layerKey, newIndex) return True ## # sets up the tilesets which are used in automapping. # @return returns True when anything is ok, False when errors occured. # (in that case will be a msg box anyway) ## # This cannot just be replaced by MapDocument::unifyTileset(Map), # because here mAddedTileset is modified. def setupTilesets(self, src, dst): existingTilesets = dst.tilesets() tilesetManager = TilesetManager.instance() # Add tilesets that are not yet part of dst map for tileset in src.tilesets(): if (existingTilesets.contains(tileset)): continue undoStack = self.mMapDocument.undoStack() replacement = tileset.findSimilarTileset(existingTilesets) if (not replacement): self.mAddedTilesets.append(tileset) undoStack.push(AddTileset(self.mMapDocument, tileset)) continue # Merge the tile properties sharedTileCount = min(tileset.tileCount(), replacement.tileCount()) for i in range(sharedTileCount): replacementTile = replacement.tileAt(i) properties = replacementTile.properties() properties.merge(tileset.tileAt(i).properties()) undoStack.push( ChangeProperties(self.mMapDocument, self.tr("Tile"), replacementTile, properties)) src.replaceTileset(tileset, replacement) tilesetManager.addReference(replacement) tilesetManager.removeReference(tileset) return True ## # Returns the conjunction of of all regions of all setlayers ## def getSetLayersRegion(self): result = QRegion() for name in self.mInputRules.names: index = self.mMapWork.indexOfLayer(name, Layer.TileLayerType) if (index == -1): continue setLayer = self.mMapWork.layerAt(index).asTileLayer() result |= setLayer.region() return result ## # This copies all Tiles from TileLayer src to TileLayer dst # # In src the Tiles are taken from the rectangle given by # src_x, src_y, width and height. # In dst they get copied to a rectangle given by # dst_x, dst_y, width, height . # if there is no tile in src TileLayer, there will nothing be copied, # so the maybe existing tile in dst will not be overwritten. # ## def copyTileRegion(self, srcLayer, srcX, srcY, width, height, dstLayer, dstX, dstY): startX = max(dstX, 0) startY = max(dstY, 0) endX = min(dstX + width, dstLayer.width()) endY = min(dstY + height, dstLayer.height()) offsetX = srcX - dstX offsetY = srcY - dstY for x in range(startX, endX): for y in range(startY, endY): cell = srcLayer.cellAt(x + offsetX, y + offsetY) if (not cell.isEmpty()): # this is without graphics update, it's done afterwards for all dstLayer.setCell(x, y, cell) ## # This copies all objects from the \a src_lr ObjectGroup to the \a dst_lr # in the given rectangle. # # The rectangle is described by the upper left corner \a src_x \a src_y # and its \a width and \a height. The parameter \a dst_x and \a dst_y # offset the copied objects in the destination object group. ## def copyObjectRegion(self, srcLayer, srcX, srcY, width, height, dstLayer, dstX, dstY): undo = self.mMapDocument.undoStack() rect = QRectF(srcX, srcY, width, height) pixelRect = self.mMapDocument.renderer().tileToPixelCoords_(rect) objects = objectsInRegion(srcLayer, pixelRect.toAlignedRect()) pixelOffset = self.mMapDocument.renderer().tileToPixelCoords( dstX, dstY) pixelOffset -= pixelRect.topLeft() clones = QList() for obj in objects: clone = obj.clone() clones.append(clone) clone.setX(clone.x() + pixelOffset.x()) clone.setY(clone.y() + pixelOffset.y()) undo.push(AddMapObject(self.mMapDocument, dstLayer, clone)) ## # This copies multiple TileLayers from one map to another. # Only the region \a region is considered for copying. # In the destination it will come to the region translated by Offset. # The parameter \a LayerTranslation is a map of which layers of the rulesmap # should get copied into which layers of the working map. ## def copyMapRegion(self, region, offset, layerTranslation): for i in range(layerTranslation.keys().size()): _from = layerTranslation.keys().at(i) to = self.mMapWork.layerAt(layerTranslation.value(_from)) for rect in region.rects(): fromTileLayer = _from.asTileLayer() fromObjectGroup = _from.asObjectGroup() if (fromTileLayer): toTileLayer = to.asTileLayer() self.copyTileRegion(fromTileLayer, rect.x(), rect.y(), rect.width(), rect.height(), toTileLayer, rect.x() + offset.x(), rect.y() + offset.y()) elif (fromObjectGroup): toObjectGroup = to.asObjectGroup() self.copyObjectRegion(fromObjectGroup, rect.x(), rect.y(), rect.width(), rect.height(), toObjectGroup, rect.x() + offset.x(), rect.y() + offset.y()) else: pass ## # This goes through all the positions of the mMapWork and checks if # there fits the rule given by the region in mMapRuleSet. # if there is a match all Layers are copied to mMapWork. # @param ruleIndex: the region which should be compared to all positions # of mMapWork will be looked up in mRulesInput and mRulesOutput # @return where: an rectangle where the rule actually got applied ## def applyRule(self, ruleIndex, where): ret = QRect() if (self.mLayerList.isEmpty()): return ret ruleInput = self.mRulesInput.at(ruleIndex) ruleOutput = self.mRulesOutput.at(ruleIndex) rbr = ruleInput.boundingRect() # Since the rule itself is translated, we need to adjust the borders of the # loops. Decrease the size at all sides by one: There must be at least one # tile overlap to the rule. minX = where.left() - rbr.left() - rbr.width() + 1 minY = where.top() - rbr.top() - rbr.height() + 1 maxX = where.right() - rbr.left() + rbr.width() - 1 maxY = where.bottom() - rbr.top() + rbr.height() - 1 # In this list of regions it is stored which parts or the map have already # been altered by exactly this rule. We store all the altered parts to # make sure there are no overlaps of the same rule applied to # (neighbouring) places appliedRegions = QList() if (self.mNoOverlappingRules): for i in range(self.mMapWork.layerCount()): appliedRegions.append(QRegion()) for y in range(minY, maxY + 1): for x in range(minX, maxX + 1): anymatch = False for index in self.mInputRules.indexes: ii = self.mInputRules[index] allLayerNamesMatch = True for name in ii.names: i = self.mMapWork.indexOfLayer(name, Layer.TileLayerType) if (i == -1): allLayerNamesMatch = False else: setLayer = self.mMapWork.layerAt(i).asTileLayer() allLayerNamesMatch &= compareLayerTo( setLayer, ii[name].listYes, ii[name].listNo, ruleInput, QPoint(x, y)) if (allLayerNamesMatch): anymatch = True break if (anymatch): r = 0 # choose by chance which group of rule_layers should be used: if (self.mLayerList.size() > 1): r = qrand() % self.mLayerList.size() if (not self.mNoOverlappingRules): self.copyMapRegion(ruleOutput, QPoint(x, y), self.mLayerList.at(r)) ret = ret.united(rbr.translated(QPoint(x, y))) continue missmatch = False translationTable = self.mLayerList.at(r) layers = translationTable.keys() # check if there are no overlaps within this rule. ruleRegionInLayer = QVector() for i in range(layers.size()): layer = layers.at(i) appliedPlace = QRegion() tileLayer = layer.asTileLayer() if (tileLayer): appliedPlace = tileLayer.region() else: appliedPlace = tileRegionOfObjectGroup( layer.asObjectGroup()) ruleRegionInLayer.append( appliedPlace.intersected(ruleOutput)) if (appliedRegions.at(i).intersects( ruleRegionInLayer[i].translated(x, y))): missmatch = True break if (missmatch): continue self.copyMapRegion(ruleOutput, QPoint(x, y), self.mLayerList.at(r)) ret = ret.united(rbr.translated(QPoint(x, y))) for i in range(translationTable.size()): appliedRegions[i] += ruleRegionInLayer[i].translated( x, y) return ret ## # Cleans up the data structes filled by setupRuleMapLayers(), # so the next rule can be processed. ## def cleanUpRuleMapLayers(self): self.cleanTileLayers() it = QList.const_iterator() for it in self.mLayerList: del it self.mLayerList.clear() # do not delete mLayerRuleRegions, it is owned by the rulesmap self.mLayerInputRegions = None self.mLayerOutputRegions = None self.mInputRules.clear() ## # Cleans up the data structes filled by setupTilesets(), # so the next rule can be processed. ## def cleanTilesets(self): for tileset in self.mAddedTilesets: if (self.mMapWork.isTilesetUsed(tileset)): continue index = self.mMapWork.indexOfTileset(tileset) if (index == -1): continue undo = self.mMapDocument.undoStack() undo.push(RemoveTileset(self.mMapDocument, index)) self.mAddedTilesets.clear() ## # Cleans up the added tile layers setup by setupMissingLayers(), # so we have a minimal addition of tile layers by the automapping. ## def cleanTileLayers(self): for tilelayerName in self.mAddedTileLayers: layerIndex = self.mMapWork.indexOfLayer(tilelayerName, Layer.TileLayerType) if (layerIndex == -1): continue layer = self.mMapWork.layerAt(layerIndex) if (not layer.isEmpty()): continue undo = self.mMapDocument.undoStack() undo.push(RemoveLayer(self.mMapDocument, layerIndex)) self.mAddedTileLayers.clear()
class AutoMapper(QObject): ## # Constructs an AutoMapper. # All data structures, which only rely on the rules map are setup # here. # # @param workingDocument: the map to work on. # @param rules: The rule map which should be used for automapping # @param rulePath: The filepath to the rule map. ## def __init__(self, workingDocument, rules, rulePath): ## # where to work in ## self.mMapDocument = workingDocument ## # the same as mMapDocument.map() ## self.mMapWork = None if workingDocument: self.mMapWork = workingDocument.map() ## # map containing the rules, usually different than mMapWork ## self.mMapRules = rules ## # This contains all added tilesets as pointers. # if rules use Tilesets which are not in the mMapWork they are added. # keep track of them, because we need to delete them afterwards, # when they still are unused # they will be added while setupTilesets(). ## self.mAddedTilesets = QVector() ## # description see: mAddedTilesets, just described by Strings ## self.mAddedTileLayers = QList() ## # Points to the tilelayer, which defines the inputregions. ## self.mLayerInputRegions = None ## # Points to the tilelayer, which defines the outputregions. ## self.mLayerOutputRegions = None ## # Contains all tilelayer pointers, which names begin with input* # It is sorted by index and name ## self.mInputRules = InputLayers() ## # List of Regions in mMapRules to know where the input rules are ## self.mRulesInput = QList() ## # List of regions in mMapRules to know where the output of a # rule is. # mRulesOutput[i] is the output of that rule, # which has the input at mRulesInput[i], meaning that mRulesInput # and mRulesOutput must match with the indexes. ## self.mRulesOutput = QList() ## # The inner set with layers to indexes is needed for translating # tile layers from mMapRules to mMapWork. # # The key is the pointer to the layer in the rulemap. The # pointer to the layer within the working map is not hardwired, but the # position in the layerlist, where it was found the last time. # This loosely bound pointer ensures we will get the right layer, since we # need to check before anyway, and it is still fast. # # The list is used to hold different translation tables # => one of the tables is chosen by chance, so randomness is available ## self.mLayerList = QList() ## # store the name of the processed rules file, to have detailed # error messages available ## self.mRulePath = rulePath ## # determines if all tiles in all touched layers should be deleted first. ## self.mDeleteTiles = False ## # This variable determines, how many overlapping tiles should be used. # The bigger the more area is remapped at an automapping operation. # This can lead to higher latency, but provides a better behavior on # interactive automapping. # It defaults to zero. ## self.mAutoMappingRadius = 0 ## # Determines if a rule is allowed to overlap it ## self.mNoOverlappingRules = False self.mTouchedObjectGroups = QSet() self.mWarning = QString() self.mTouchedTileLayers = QSet() self.mError = '' if (not self.setupRuleMapProperties()): return if (not self.setupRuleMapTileLayers()): return if (not self.setupRuleList()): return def __del__(self): self.cleanUpRulesMap() ## # Checks if the passed \a ruleLayerName is used in this instance # of Automapper. ## def ruleLayerNameUsed(self, ruleLayerName): return self.mInputRules.names.contains(ruleLayerName) ## # Call prepareLoad first! Returns a set of strings describing the tile # layers, which could be touched considering the given layers of the # rule map. ## def getTouchedTileLayers(self): return self.mTouchedTileLayers ## # This needs to be called directly before the autoMap call. # It sets up some data structures which change rapidly, so it is quite # painful to keep these datastructures up to date all time. (indices of # layers of the working map) ## def prepareAutoMap(self): self.mError = '' self.mWarning = '' if (not self.setupMissingLayers()): return False if (not self.setupCorrectIndexes()): return False if (not self.setupTilesets(self.mMapRules, self.mMapWork)): return False return True ## # Here is done all the automapping. ## def autoMap(self, where): # first resize the active area if (self.mAutoMappingRadius): region = QRegion() for r in where.rects(): region += r.adjusted(- self.mAutoMappingRadius, - self.mAutoMappingRadius, + self.mAutoMappingRadius, + self.mAutoMappingRadius) #where += region # delete all the relevant area, if the property "DeleteTiles" is set if (self.mDeleteTiles): setLayersRegion = self.getSetLayersRegion() for i in range(self.mLayerList.size()): translationTable = self.mLayerList.at(i) for layer in translationTable.keys(): index = self.mLayerList.at(i).value(layer) dstLayer = self.mMapWork.layerAt(index) region = setLayersRegion.intersected(where) dstTileLayer = dstLayer.asTileLayer() if (dstTileLayer): dstTileLayer.erase(region) else: self.eraseRegionObjectGroup(self.mMapDocument, dstLayer.asObjectGroup(), region) # Increase the given region where the next automapper should work. # This needs to be done, so you can rely on the order of the rules at all # locations ret = QRegion() for rect in where.rects(): for i in range(self.mRulesInput.size()): # at the moment the parallel execution does not work yet # TODO: make multithreading available! # either by dividing the rules or the region to multiple threads ret = ret.united(self.applyRule(i, rect)) #where = where.united(ret) ## # This cleans all datastructures, which are setup via prepareAutoMap, # so the auto mapper becomes ready for its next automatic mapping. ## def cleanAll(self): self.cleanTilesets() self.cleanTileLayers() ## # Contains all errors until operation was canceled. # The errorlist is cleared within prepareLoad and prepareAutoMap. ## def errorString(self): return self.mError ## # Contains all warnings which occur at loading a rules map or while # automapping. # The errorlist is cleared within prepareLoad and prepareAutoMap. ## def warningString(self): return self.mWarning ## # Reads the map properties of the rulesmap. # @return returns True when anything is ok, False when errors occured. ## def setupRuleMapProperties(self): properties = self.mMapRules.properties() for key in properties.keys(): value = properties.value(key) raiseWarning = True if (key.toLower() == "deletetiles"): if (value.canConvert(QVariant.Bool)): self.mDeleteTiles = value.toBool() raiseWarning = False elif (key.toLower() == "automappingradius"): if (value.canConvert(QVariant.Int)): self.mAutoMappingRadius = value raiseWarning = False elif (key.toLower() == "nooverlappingrules"): if (value.canConvert(QVariant.Bool)): self.mNoOverlappingRules = value.toBool() raiseWarning = False if (raiseWarning): self.mWarning += self.tr("'%s': Property '%s' = '%s' does not make sense. \nIgnoring this property."%(self.mRulePath, key, value.toString()) + '\n') return True def cleanUpRulesMap(self): self.cleanTilesets() # mMapRules can be empty, when in prepareLoad the very first stages fail. if (not self.mMapRules): return tilesetManager = TilesetManager.instance() tilesetManager.removeReferences(self.mMapRules.tilesets()) del self.mMapRules self.mMapRules = None self.cleanUpRuleMapLayers() self.mRulesInput.clear() self.mRulesOutput.clear() ## # Searches the rules layer for regions and stores these in \a rules. # @return returns True when anything is ok, False when errors occured. ## def setupRuleList(self): combinedRegions = coherentRegions( self.mLayerInputRegions.region() + self.mLayerOutputRegions.region()) combinedRegions = QList(sorted(combinedRegions, key=lambda x:x.y(), reverse=True)) rulesInput = coherentRegions( self.mLayerInputRegions.region()) rulesOutput = coherentRegions( self.mLayerOutputRegions.region()) for i in range(combinedRegions.size()): self.mRulesInput.append(QRegion()) self.mRulesOutput.append(QRegion()) for reg in rulesInput: for i in range(combinedRegions.size()): if (reg.intersects(combinedRegions[i])): self.mRulesInput[i] += reg break for reg in rulesOutput: for i in range(combinedRegions.size()): if (reg.intersects(combinedRegions[i])): self.mRulesOutput[i] += reg break for i in range(self.mRulesInput.size()): checkCoherent = self.mRulesInput.at(i).united(self.mRulesOutput.at(i)) coherentRegions(checkCoherent).length() == 1 return True ## # Sets up the layers in the rules map, which are used for automapping. # The layers are detected and put in the internal data structures # @return returns True when anything is ok, False when errors occured. ## def setupRuleMapTileLayers(self): error = QString() for layer in self.mMapRules.layers(): layerName = layer.name() if (layerName.lower().startswith("regions")): treatAsBoth = layerName.toLower() == "regions" if (layerName.lower().endswith("input") or treatAsBoth): if (self.mLayerInputRegions): error += self.tr("'regions_input' layer must not occur more than once.\n") if (layer.isTileLayer()): self.mLayerInputRegions = layer.asTileLayer() else: error += self.tr("'regions_*' layers must be tile layers.\n") if (layerName.lower().endswith("output") or treatAsBoth): if (self.mLayerOutputRegions): error += self.tr("'regions_output' layer must not occur more than once.\n") if (layer.isTileLayer()): self.mLayerOutputRegions = layer.asTileLayer() else: error += self.tr("'regions_*' layers must be tile layers.\n") continue nameStartPosition = layerName.indexOf('_') + 1 # name is all characters behind the underscore (excluded) name = layerName.right(layerName.size() - nameStartPosition) # group is all before the underscore (included) index = layerName.left(nameStartPosition) if (index.lower().startswith("output")): index.remove(0, 6) elif (index.lower().startswith("inputnot")): index.remove(0, 8) elif (index.lower().startswith("input")): index.remove(0, 5) # both 'rule' and 'output' layers will require and underscore and # rely on the correct position detected of the underscore if (nameStartPosition == 0): error += self.tr("Did you forget an underscore in layer '%d'?\n"%layerName) continue if (layerName.startsWith("input", Qt.CaseInsensitive)): isNotList = layerName.lower().startswith("inputnot") if (not layer.isTileLayer()): error += self.tr("'input_*' and 'inputnot_*' layers must be tile layers.\n") continue self.mInputRules.names.insert(name) if (not self.mInputRules.indexes.contains(index)): self.mInputRules.indexes.insert(index) self.mInputRules.insert(index, InputIndex()) if (not self.mInputRules[index].names.contains(name)): self.mInputRules[index].names.insert(name) self.mInputRules[index].insert(name, InputIndexName()) if (isNotList): self.mInputRules[index][name].listNo.append(layer.asTileLayer()) else: self.mInputRules[index][name].listYes.append(layer.asTileLayer()) continue if layerName.lower().startswith("output"): if (layer.isTileLayer()): self.mTouchedTileLayers.insert(name) else: self.mTouchedObjectGroups.insert(name) type = layer.layerType() layerIndex = self.mMapWork.indexOfLayer(name, type) found = False for translationTable in self.mLayerList: if (translationTable.index == index): translationTable.insert(layer, layerIndex) found = True break if (not found): self.mLayerList.append(RuleOutput()) self.mLayerList.last().insert(layer, layerIndex) self.mLayerList.last().index = index continue error += self.tr("Layer '%s' is not recognized as a valid layer for Automapping.\n"%layerName) if (not self.mLayerInputRegions): error += self.tr("No 'regions' or 'regions_input' layer found.\n") if (not self.mLayerOutputRegions): error += self.tr("No 'regions' or 'regions_output' layer found.\n") if (self.mInputRules.isEmpty()): error += self.tr("No input_<name> layer found!\n") # no need to check for mInputNotRules.size() == 0 here. # these layers are not necessary. if error != '': error = self.mRulePath + '\n' + error self.mError += error return False return True ## # Checks if all needed layers in the working map are there. # If not, add them in the correct order. ## def setupMissingLayers(self): # make sure all needed layers are there: for name in self.mTouchedTileLayers: if (self.mMapWork.indexOfLayer(name, Layer.TileLayerType) != -1): continue index = self.mMapWork.layerCount() tilelayer = TileLayer(name, 0, 0, self.mMapWork.width(), self.mMapWork.height()) self.mMapDocument.undoStack().push(AddLayer(self.mMapDocument, index, tilelayer)) self.mAddedTileLayers.append(name) for name in self.mTouchedObjectGroups: if (self.mMapWork.indexOfLayer(name, Layer.ObjectGroupType) != -1): continue index = self.mMapWork.layerCount() objectGroup = ObjectGroup(name, 0, 0, self.mMapWork.width(), self.mMapWork.height()) self.mMapDocument.undoStack().push(AddLayer(self.mMapDocument, index, objectGroup)) self.mAddedTileLayers.append(name) return True ## # Checks if the layers setup as in setupRuleMapLayers are still right. # If it's not right, correct them. # @return returns True if everything went fine. False is returned when # no set layer was found ## def setupCorrectIndexes(self): # make sure all indexes of the layer translationtables are correct. for i in range(self.mLayerList.size()): translationTable = self.mLayerList.at(i) for layerKey in translationTable.keys(): name = layerKey.name() pos = name.indexOf('_') + 1 name = name.right(name.length() - pos) index = translationTable.value(layerKey, -1) if (index >= self.mMapWork.layerCount() or index == -1 or name != self.mMapWork.layerAt(index).name()): newIndex = self.mMapWork.indexOfLayer(name, layerKey.layerType()) translationTable.insert(layerKey, newIndex) return True ## # sets up the tilesets which are used in automapping. # @return returns True when anything is ok, False when errors occured. # (in that case will be a msg box anyway) ## # This cannot just be replaced by MapDocument::unifyTileset(Map), # because here mAddedTileset is modified. def setupTilesets(self, src, dst): existingTilesets = dst.tilesets() tilesetManager = TilesetManager.instance() # Add tilesets that are not yet part of dst map for tileset in src.tilesets(): if (existingTilesets.contains(tileset)): continue undoStack = self.mMapDocument.undoStack() replacement = tileset.findSimilarTileset(existingTilesets) if (not replacement): self.mAddedTilesets.append(tileset) undoStack.push(AddTileset(self.mMapDocument, tileset)) continue # Merge the tile properties sharedTileCount = min(tileset.tileCount(), replacement.tileCount()) for i in range(sharedTileCount): replacementTile = replacement.tileAt(i) properties = replacementTile.properties() properties.merge(tileset.tileAt(i).properties()) undoStack.push(ChangeProperties(self.mMapDocument, self.tr("Tile"), replacementTile, properties)) src.replaceTileset(tileset, replacement) tilesetManager.addReference(replacement) tilesetManager.removeReference(tileset) return True ## # Returns the conjunction of of all regions of all setlayers ## def getSetLayersRegion(self): result = QRegion() for name in self.mInputRules.names: index = self.mMapWork.indexOfLayer(name, Layer.TileLayerType) if (index == -1): continue setLayer = self.mMapWork.layerAt(index).asTileLayer() result |= setLayer.region() return result ## # This copies all Tiles from TileLayer src to TileLayer dst # # In src the Tiles are taken from the rectangle given by # src_x, src_y, width and height. # In dst they get copied to a rectangle given by # dst_x, dst_y, width, height . # if there is no tile in src TileLayer, there will nothing be copied, # so the maybe existing tile in dst will not be overwritten. # ## def copyTileRegion(self, srcLayer, srcX, srcY, width, height, dstLayer, dstX, dstY): startX = max(dstX, 0) startY = max(dstY, 0) endX = min(dstX + width, dstLayer.width()) endY = min(dstY + height, dstLayer.height()) offsetX = srcX - dstX offsetY = srcY - dstY for x in range(startX, endX): for y in range(startY, endY): cell = srcLayer.cellAt(x + offsetX, y + offsetY) if (not cell.isEmpty()): # this is without graphics update, it's done afterwards for all dstLayer.setCell(x, y, cell) ## # This copies all objects from the \a src_lr ObjectGroup to the \a dst_lr # in the given rectangle. # # The rectangle is described by the upper left corner \a src_x \a src_y # and its \a width and \a height. The parameter \a dst_x and \a dst_y # offset the copied objects in the destination object group. ## def copyObjectRegion(self, srcLayer, srcX, srcY, width, height, dstLayer, dstX, dstY): undo = self.mMapDocument.undoStack() rect = QRectF(srcX, srcY, width, height) pixelRect = self.mMapDocument.renderer().tileToPixelCoords_(rect) objects = objectsInRegion(srcLayer, pixelRect.toAlignedRect()) pixelOffset = self.mMapDocument.renderer().tileToPixelCoords(dstX, dstY) pixelOffset -= pixelRect.topLeft() clones = QList() for obj in objects: clone = obj.clone() clones.append(clone) clone.setX(clone.x() + pixelOffset.x()) clone.setY(clone.y() + pixelOffset.y()) undo.push(AddMapObject(self.mMapDocument, dstLayer, clone)) ## # This copies multiple TileLayers from one map to another. # Only the region \a region is considered for copying. # In the destination it will come to the region translated by Offset. # The parameter \a LayerTranslation is a map of which layers of the rulesmap # should get copied into which layers of the working map. ## def copyMapRegion(self, region, offset, layerTranslation): for i in range(layerTranslation.keys().size()): _from = layerTranslation.keys().at(i) to = self.mMapWork.layerAt(layerTranslation.value(_from)) for rect in region.rects(): fromTileLayer = _from.asTileLayer() fromObjectGroup = _from.asObjectGroup() if (fromTileLayer): toTileLayer = to.asTileLayer() self.copyTileRegion(fromTileLayer, rect.x(), rect.y(), rect.width(), rect.height(), toTileLayer, rect.x() + offset.x(), rect.y() + offset.y()) elif (fromObjectGroup): toObjectGroup = to.asObjectGroup() self.copyObjectRegion(fromObjectGroup, rect.x(), rect.y(), rect.width(), rect.height(), toObjectGroup, rect.x() + offset.x(), rect.y() + offset.y()) else: pass ## # This goes through all the positions of the mMapWork and checks if # there fits the rule given by the region in mMapRuleSet. # if there is a match all Layers are copied to mMapWork. # @param ruleIndex: the region which should be compared to all positions # of mMapWork will be looked up in mRulesInput and mRulesOutput # @return where: an rectangle where the rule actually got applied ## def applyRule(self, ruleIndex, where): ret = QRect() if (self.mLayerList.isEmpty()): return ret ruleInput = self.mRulesInput.at(ruleIndex) ruleOutput = self.mRulesOutput.at(ruleIndex) rbr = ruleInput.boundingRect() # Since the rule itself is translated, we need to adjust the borders of the # loops. Decrease the size at all sides by one: There must be at least one # tile overlap to the rule. minX = where.left() - rbr.left() - rbr.width() + 1 minY = where.top() - rbr.top() - rbr.height() + 1 maxX = where.right() - rbr.left() + rbr.width() - 1 maxY = where.bottom() - rbr.top() + rbr.height() - 1 # In this list of regions it is stored which parts or the map have already # been altered by exactly this rule. We store all the altered parts to # make sure there are no overlaps of the same rule applied to # (neighbouring) places appliedRegions = QList() if (self.mNoOverlappingRules): for i in range(self.mMapWork.layerCount()): appliedRegions.append(QRegion()) for y in range(minY, maxY+1): for x in range(minX, maxX+1): anymatch = False for index in self.mInputRules.indexes: ii = self.mInputRules[index] allLayerNamesMatch = True for name in ii.names: i = self.mMapWork.indexOfLayer(name, Layer.TileLayerType) if (i == -1): allLayerNamesMatch = False else: setLayer = self.mMapWork.layerAt(i).asTileLayer() allLayerNamesMatch &= compareLayerTo(setLayer, ii[name].listYes, ii[name].listNo, ruleInput, QPoint(x, y)) if (allLayerNamesMatch): anymatch = True break if (anymatch): r = 0 # choose by chance which group of rule_layers should be used: if (self.mLayerList.size() > 1): r = qrand() % self.mLayerList.size() if (not self.mNoOverlappingRules): self.copyMapRegion(ruleOutput, QPoint(x, y), self.mLayerList.at(r)) ret = ret.united(rbr.translated(QPoint(x, y))) continue missmatch = False translationTable = self.mLayerList.at(r) layers = translationTable.keys() # check if there are no overlaps within this rule. ruleRegionInLayer = QVector() for i in range(layers.size()): layer = layers.at(i) appliedPlace = QRegion() tileLayer = layer.asTileLayer() if (tileLayer): appliedPlace = tileLayer.region() else: appliedPlace = tileRegionOfObjectGroup(layer.asObjectGroup()) ruleRegionInLayer.append(appliedPlace.intersected(ruleOutput)) if (appliedRegions.at(i).intersects( ruleRegionInLayer[i].translated(x, y))): missmatch = True break if (missmatch): continue self.copyMapRegion(ruleOutput, QPoint(x, y), self.mLayerList.at(r)) ret = ret.united(rbr.translated(QPoint(x, y))) for i in range(translationTable.size()): appliedRegions[i] += ruleRegionInLayer[i].translated(x, y) return ret ## # Cleans up the data structes filled by setupRuleMapLayers(), # so the next rule can be processed. ## def cleanUpRuleMapLayers(self): self.cleanTileLayers() it = QList.const_iterator() for it in self.mLayerList: del it self.mLayerList.clear() # do not delete mLayerRuleRegions, it is owned by the rulesmap self.mLayerInputRegions = None self.mLayerOutputRegions = None self.mInputRules.clear() ## # Cleans up the data structes filled by setupTilesets(), # so the next rule can be processed. ## def cleanTilesets(self): for tileset in self.mAddedTilesets: if (self.mMapWork.isTilesetUsed(tileset)): continue index = self.mMapWork.indexOfTileset(tileset) if (index == -1): continue undo = self.mMapDocument.undoStack() undo.push(RemoveTileset(self.mMapDocument, index)) self.mAddedTilesets.clear() ## # Cleans up the added tile layers setup by setupMissingLayers(), # so we have a minimal addition of tile layers by the automapping. ## def cleanTileLayers(self): for tilelayerName in self.mAddedTileLayers: layerIndex = self.mMapWork.indexOfLayer(tilelayerName, Layer.TileLayerType) if (layerIndex == -1): continue layer = self.mMapWork.layerAt(layerIndex) if (not layer.isEmpty()): continue undo = self.mMapDocument.undoStack() undo.push(RemoveLayer(self.mMapDocument, layerIndex)) self.mAddedTileLayers.clear()
class ObjectGroup(Layer): ## # Objects within an object group can either be drawn top down (sorted # by their y-coordinate) or by index (manual stacking order). # # The default is top down. ## class DrawOrder(): UnknownOrder = -1 TopDownOrder = 1 IndexOrder = 2 ## # Default constructor. ## def __init__(self, *args): self.mObjects = QList() self.mColor = QColor() l = len(args) if l==0: super().__init__(Layer.ObjectGroupType, QString(), 0, 0, 0, 0) elif l==5: ## # Constructor with some parameters. ## name, x, y, width, height = args super().__init__(Layer.ObjectGroupType, name, x, y, width, height) else: pass self.mDrawOrder = ObjectGroup.DrawOrder.IndexOrder ## # Destructor. ## def __del__(self): self.mObjects.clear() ## # Returns a pointer to the list of objects in this object group. ## def objects(self): return QList(self.mObjects) ## # Returns the number of objects in this object group. ## def objectCount(self): return self.mObjects.size() ## # Returns the object at the specified index. ## def objectAt(self, index): return self.mObjects.at(index) ## # Adds an object to this object group. ## def addObject(self, object): self.mObjects.append(object) object.setObjectGroup(self) if (self.mMap and object.id() == 0): object.setId(self.mMap.takeNextObjectId()) ## # Inserts an object at the specified index. This is only used for undoing # the removal of an object at the moment, to make sure not to change the # saved order of the objects. ## def insertObject(self, index, object): self.mObjects.insert(index, object) object.setObjectGroup(self) if (self.mMap and object.id() == 0): object.setId(self.mMap.takeNextObjectId()) ## # Removes an object from this object group. Ownership of the object is # transferred to the caller. # # @return the index at which the specified object was removed ## def removeObject(self, object): index = self.mObjects.indexOf(object) self.mObjects.removeAt(index) object.setObjectGroup(None) return index ## # Removes the object at the given index. Ownership of the object is # transferred to the caller. # # This is faster than removeObject when you've already got the index. # # @param index the index at which to remove an object ## def removeObjectAt(self, index): object = self.mObjects.takeAt(index) object.setObjectGroup(None) ## # Moves \a count objects starting at \a from to the index given by \a to. # # The \a to index may not lie within the range of objects that is # being moved. ## def moveObjects(self, _from, to, count): # It's an error when 'to' lies within the moving range of objects # Nothing to be done when 'to' is the start or the end of the range, or # when the number of objects to be moved is 0. if (to == _from or to == _from + count or count == 0): return movingObjects = self.mObjects[_from:_from+count] self.mObjects.erase(_from, _from + count) if (to > _from): to -= count for i in range(count): self.mObjects.insert(to + i, movingObjects[i]) ## # Returns the bounding rect around all objects in this object group. ## def objectsBoundingRect(self): boundingRect = QRectF() for object in self.mObjects: boundingRect = boundingRect.united(object.bounds()) return boundingRect ## # Returns whether this object group contains any objects. ## def isEmpty(self): return self.mObjects.isEmpty() ## # Computes and returns the set of tilesets used by this object group. ## def usedTilesets(self): tilesets = QSet() for object in self.mObjects: tile = object.cell().tile if tile: tilesets.insert(tile.sharedTileset()) return tilesets ## # Returns whether any tile objects in this object group reference tiles # in the given tileset. ## def referencesTileset(self, tileset): for object in self.mObjects: tile = object.cell().tile if (tile and tile.tileset() == tileset): return True return False ## # Replaces all references to tiles from \a oldTileset with tiles from # \a newTileset. ## def replaceReferencesToTileset(self, oldTileset, newTileset): for object in self.mObjects: tile = object.cell().tile if (tile and tile.tileset() == oldTileset): cell = object.cell() cell.tile = Tileset.tileAt(tile.id()) object.setCell(cell) ## # Offsets all objects within the group by the \a offset given in pixel # coordinates, and optionally wraps them. The object's center must be # within \a bounds, and wrapping occurs if the displaced center is out of # the bounds. # # \sa TileLayer.offset() ## def offsetObjects(self, offset, bounds, wrapX, wrapY): for object in self.mObjects: objectCenter = object.bounds().center() if (not bounds.contains(objectCenter)): continue newCenter = QPointF(objectCenter + offset) if (wrapX and bounds.width() > 0): nx = math.fmod(newCenter.x() - bounds.left(), bounds.width()) if nx < 0: x = bounds.width() + nx else: x = nx newCenter.setX(bounds.left() + x) if (wrapY and bounds.height() > 0): ny = math.fmod(newCenter.y() - bounds.top(), bounds.height()) if ny < 0: x = bounds.height() + ny else: x = ny newCenter.setY(bounds.top() + x) object.setPosition(object.position() + (newCenter - objectCenter)) def canMergeWith(self, other): return other.isObjectGroup() def mergedWith(self, other): og = other merged = self.clone() for mapObject in og.objects(): merged.addObject(mapObject.clone()) return merged ## # Returns the color of the object group, or an invalid color if no color # is set. ## def color(self): return self.mColor ## # Sets the display color of the object group. ## def setColor(self, color): if type(color) != QColor: color = QColor(color) self.mColor = color ## # Returns the draw order for the objects in this group. # # \sa ObjectGroup.DrawOrder ## def drawOrder(self): return self.mDrawOrder ## # Sets the draw order for the objects in this group. # # \sa ObjectGroup.DrawOrder ## def setDrawOrder(self, drawOrder): self.mDrawOrder = drawOrder ## # Returns a duplicate of this ObjectGroup. # # \sa Layer.clone() ## def clone(self): return self.initializeClone(ObjectGroup(self.mName, self.mX, self.mY, self.mWidth, self.mHeight)) def initializeClone(self, clone): super().initializeClone(clone) for object in self.mObjects: clone.addObject(object.clone()) clone.setColor(self.mColor) clone.setDrawOrder(self.mDrawOrder) return clone
class QtCursorDatabase(): def __init__(self): self.m_cursorNames = QList() self.m_cursorIcons = QMap() self.m_valueToCursorShape = QMap() self.m_cursorShapeToValue = QMap() self.appendCursor(Qt.ArrowCursor, QCoreApplication.translate("QtCursorDatabase", "Arrow"), QIcon(":/qt-project.org/qtpropertybrowser/images/cursor-arrow.png")) self.appendCursor(Qt.UpArrowCursor, QCoreApplication.translate("QtCursorDatabase", "Up Arrow"), QIcon(":/qt-project.org/qtpropertybrowser/images/cursor-uparrow.png")) self.appendCursor(Qt.CrossCursor, QCoreApplication.translate("QtCursorDatabase", "Cross"), QIcon(":/qt-project.org/qtpropertybrowser/images/cursor-cross.png")) self.appendCursor(Qt.WaitCursor, QCoreApplication.translate("QtCursorDatabase", "Wait"), QIcon(":/qt-project.org/qtpropertybrowser/images/cursor-wait.png")) self.appendCursor(Qt.IBeamCursor, QCoreApplication.translate("QtCursorDatabase", "IBeam"), QIcon(":/qt-project.org/qtpropertybrowser/images/cursor-ibeam.png")) self.appendCursor(Qt.SizeVerCursor, QCoreApplication.translate("QtCursorDatabase", "Size Vertical"), QIcon(":/qt-project.org/qtpropertybrowser/images/cursor-sizev.png")) self.appendCursor(Qt.SizeHorCursor, QCoreApplication.translate("QtCursorDatabase", "Size Horizontal"), QIcon(":/qt-project.org/qtpropertybrowser/images/cursor-sizeh.png")) self.appendCursor(Qt.SizeFDiagCursor, QCoreApplication.translate("QtCursorDatabase", "Size Backslash"), QIcon(":/qt-project.org/qtpropertybrowser/images/cursor-sizef.png")) self.appendCursor(Qt.SizeBDiagCursor, QCoreApplication.translate("QtCursorDatabase", "Size Slash"), QIcon(":/qt-project.org/qtpropertybrowser/images/cursor-sizeb.png")) self.appendCursor(Qt.SizeAllCursor, QCoreApplication.translate("QtCursorDatabase", "Size All"), QIcon(":/qt-project.org/qtpropertybrowser/images/cursor-sizeall.png")) self.appendCursor(Qt.BlankCursor, QCoreApplication.translate("QtCursorDatabase", "Blank"), QIcon()) self.appendCursor(Qt.SplitVCursor, QCoreApplication.translate("QtCursorDatabase", "Split Vertical"), QIcon(":/qt-project.org/qtpropertybrowser/images/cursor-vsplit.png")) self.appendCursor(Qt.SplitHCursor, QCoreApplication.translate("QtCursorDatabase", "Split Horizontal"), QIcon(":/qt-project.org/qtpropertybrowser/images/cursor-hsplit.png")) self.appendCursor(Qt.PointingHandCursor, QCoreApplication.translate("QtCursorDatabase", "Pointing Hand"), QIcon(":/qt-project.org/qtpropertybrowser/images/cursor-hand.png")) self.appendCursor(Qt.ForbiddenCursor, QCoreApplication.translate("QtCursorDatabase", "Forbidden"), QIcon(":/qt-project.org/qtpropertybrowser/images/cursor-forbidden.png")) self.appendCursor(Qt.OpenHandCursor, QCoreApplication.translate("QtCursorDatabase", "Open Hand"), QIcon(":/qt-project.org/qtpropertybrowser/images/cursor-openhand.png")) self.appendCursor(Qt.ClosedHandCursor, QCoreApplication.translate("QtCursorDatabase", "Closed Hand"), QIcon(":/qt-project.org/qtpropertybrowser/images/cursor-closedhand.png")) self.appendCursor(Qt.WhatsThisCursor, QCoreApplication.translate("QtCursorDatabase", "What's This"), QIcon(":/qt-project.org/qtpropertybrowser/images/cursor-whatsthis.png")) self.appendCursor(Qt.BusyCursor, QCoreApplication.translate("QtCursorDatabase", "Busy"), QIcon(":/qt-project.org/qtpropertybrowser/images/cursor-busy.png")) def clear(self): self.m_cursorNames.clear() self.m_cursorIcons.clear() self.m_valueToCursorShape.clear() self.m_cursorShapeToValue.clear() def appendCursor(self,shape, name, icon): if self.m_cursorShapeToValue.get(shape): return value = len(self.m_cursorNames) self.m_cursorNames.append(name) self.m_cursorIcons[value] = icon self.m_valueToCursorShape[value] = shape self.m_cursorShapeToValue[shape] = value def cursorShapeNames(self): return self.m_cursorNames def cursorShapeIcons(self): return self.m_cursorIcons def cursorToShapeName(self,cursor): val = self.cursorToValue(cursor) if val >= 0: return self.m_cursorNames[val] return '' def cursorToShapeIcon(self,cursor): val = self.cursorToValue(cursor) return self.m_cursorIcons[val] def cursorToValue(self,cursor): shape = cursor.shape() return self.m_cursorShapeToValue.get(shape, -1) def valueToCursor(self,value): if value in self.m_valueToCursorShape: return QCursor(self.m_valueToCursorShape[value]) return QCursor()
class Map(Object): ## # The orientation of the map determines how it should be rendered. An # Orthogonal map is using rectangular tiles that are aligned on a # straight grid. An Isometric map uses diamond shaped tiles that are # aligned on an isometric projected grid. A Hexagonal map uses hexagon # shaped tiles that fit into each other by shifting every other row. ## class Orientation(Enum): Unknown, Orthogonal, Isometric, Staggered, Hexagonal = range(5) ## # The different formats in which the tile layer data can be stored. ## class LayerDataFormat(Enum): XML = 0 Base64 = 1 Base64Gzip = 2 Base64Zlib = 3 CSV = 4 ## # The order in which tiles are rendered on screen. ## class RenderOrder(Enum): RightDown = 0 RightUp = 1 LeftDown = 2 LeftUp = 3 ## # Which axis is staggered. Only used by the isometric staggered and # hexagonal map renderers. ## class StaggerAxis(Enum): StaggerX, StaggerY = range(2) ## # When staggering, specifies whether the odd or the even rows/columns are # shifted half a tile right/down. Only used by the isometric staggered and # hexagonal map renderers. ## class StaggerIndex(Enum): StaggerOdd = 0 StaggerEven = 1 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 ## # Destructor. ## def __del__(self): self.mLayers.clear() ## # Returns the orientation of the map. ## def orientation(self): return self.mOrientation ## # Sets the orientation of the map. ## def setOrientation(self, orientation): self.mOrientation = orientation ## # Returns the render order of the map. ## def renderOrder(self): return self.mRenderOrder ## # Sets the render order of the map. ## def setRenderOrder(self, renderOrder): self.mRenderOrder = renderOrder ## # Returns the width of this map in tiles. ## def width(self): return self.mWidth ## # Sets the width of this map in tiles. ## def setWidth(self, width): self.mWidth = width ## # Returns the height of this map in tiles. ## def height(self): return self.mHeight ## # Sets the height of this map in tiles. ## def setHeight(self, height): self.mHeight = height ## # Returns the size of this map. Provided for convenience. ## def size(self): return QSize(self.mWidth, self.mHeight) ## # Returns the tile width of this map. ## def tileWidth(self): return self.mTileWidth ## # Sets the width of one tile. ## def setTileWidth(self, width): self.mTileWidth = width ## # Returns the tile height used by this map. ## def tileHeight(self): return self.mTileHeight ## # Sets the height of one tile. ## def setTileHeight(self, height): self.mTileHeight = height ## # Returns the size of one tile. Provided for convenience. ## def tileSize(self): return QSize(self.mTileWidth, self.mTileHeight) def hexSideLength(self): return self.mHexSideLength def setHexSideLength(self, hexSideLength): self.mHexSideLength = hexSideLength def staggerAxis(self): return self.mStaggerAxis def setStaggerAxis(self, staggerAxis): self.mStaggerAxis = staggerAxis def staggerIndex(self): return self.mStaggerIndex def setStaggerIndex(self, staggerIndex): self.mStaggerIndex = staggerIndex ## # Adjusts the draw margins to be at least as big as the given margins. # Called from tile layers when their tiles change. ## def adjustDrawMargins(self, margins): # The TileLayer includes the maximum tile size in its draw margins. So # we need to subtract the tile size of the map, since that part does not # contribute to additional margin. self.mDrawMargins = maxMargins( QMargins(margins.left(), margins.top() - self.mTileHeight, margins.right() - self.mTileWidth, margins.bottom()), self.mDrawMargins) ## # Computes the extra margins due to layer offsets. These need to be taken into # account when determining the bounding rect of the map for example. ## def computeLayerOffsetMargins(self): offsetMargins = QMargins() for layer in self.mLayers: offset = layer.offset() offsetMargins = maxMargins( QMargins(math.ceil(-offset.x()), math.ceil(-offset.y()), math.ceil(offset.x()), math.ceil(offset.y())), offsetMargins) return offsetMargins ## # Returns the margins that have to be taken into account when figuring # out which part of the map to repaint after changing some tiles. # # @see TileLayer.drawMargins ## def drawMargins(self): return self.mDrawMargins ## # Recomputes the draw margins for this map and each of its tile layers. Needed # after the tile offset of a tileset has changed for example. # # \sa TileLayer.recomputeDrawMargins ## def recomputeDrawMargins(self): self.mDrawMargins = QMargins() for layer in self.mLayers: tileLayer = layer.asTileLayer() if tileLayer: tileLayer.recomputeDrawMargins() ## # Returns the number of layers of this map. ## def layerCount(self, *args): l = len(args) if l == 0: return self.mLayers.size() elif l == 1: ## # Convenience function that returns the number of layers of this map that # match the given \a type. ## tp = args[0] count = 0 for layer in self.mLayers: if (layer.layerType() == tp): count += 1 return count def tileLayerCount(self): return self.layerCount(Layer.TileLayerType) def objectGroupCount(self): return self.layerCount(Layer.ObjectGroupType) def imageLayerCount(self): return self.layerCount(Layer.ImageLayerType) ## # Returns the layer at the specified index. ## def layerAt(self, index): return self.mLayers.at(index) ## # Returns the list of layers of this map. This is useful when you want to # use foreach. ## def layers(self, *args): l = len(args) if l == 0: return QList(self.mLayers) elif l == 1: tp = args[0] layers = QList() for layer in self.mLayers: if (layer.layerType() == tp): layers.append(layer) return layers def objectGroups(self): layers = QList() for layer in self.mLayers: og = layer.asObjectGroup() if og: layers.append(og) return layers def tileLayers(self): layers = QList() for layer in self.mLayers: tl = layer.asTileLayer() if tl: layers.append(tl) return layers ## # Adds a layer to this map. ## def addLayer(self, layer): self.adoptLayer(layer) self.mLayers.append(layer) ## # Returns the index of the layer given by \a layerName, or -1 if no # layer with that name is found. # # The second optional parameter specifies the layer types which are # searched. ## def indexOfLayer(self, layerName, layertypes=Layer.AnyLayerType): for index in range(self.mLayers.size()): if (self.layerAt(index).name() == layerName and (layertypes & self.layerAt(index).layerType())): return index return -1 ## # Adds a layer to this map, inserting it at the given index. ## def insertLayer(self, index, layer): self.adoptLayer(layer) self.mLayers.insert(index, layer) ## # Removes the layer at the given index from this map and returns it. # The caller becomes responsible for the lifetime of this layer. ## def takeLayerAt(self, index): layer = self.mLayers.takeAt(index) layer.setMap(None) return layer ## # Adds a tileset to this map. The map does not take ownership over its # tilesets, this is merely for keeping track of which tilesets are used by # the map, and their saving order. # # @param tileset the tileset to add ## def addTileset(self, tileset): self.mTilesets.append(tileset) ## # Convenience function to be used together with Layer.usedTilesets() ## def addTilesets(self, tilesets): for tileset in tilesets: self.addTileset(tileset) ## # Inserts \a tileset at \a index in the list of tilesets used by this map. ## def insertTileset(self, index, tileset): self.mTilesets.insert(index, tileset) ## # Returns the index of the given \a tileset, or -1 if it is not used in # this map. ## def indexOfTileset(self, tileset): return self.mTilesets.indexOf(tileset) ## # Removes the tileset at \a index from this map. # # \warning Does not make sure that this map no longer refers to tiles from # the removed tileset! # # \sa addTileset ## def removeTilesetAt(self, index): self.mTilesets.removeAt(index) ## # Replaces all tiles from \a oldTileset with tiles from \a newTileset. # Also replaces the old tileset with the new tileset in the list of # tilesets. ## def replaceTileset(self, oldTileset, newTileset): index = self.mTilesets.indexOf(oldTileset) for layer in self.mLayers: layer.replaceReferencesToTileset(oldTileset, newTileset) self.mTilesets[index] = newTileset ## # Returns the number of tilesets of this map. ## def tilesetCount(self): return self.mTilesets.size() ## # Returns the tileset at the given index. ## def tilesetAt(self, index): return self.mTilesets.at(index) ## # Returns the tilesets that the tiles on this map are using. ## def tilesets(self): return QList(self.mTilesets) ## # Returns the background color of this map. ## def backgroundColor(self): return QColor(self.mBackgroundColor) ## # Sets the background color of this map. ## def setBackgroundColor(self, color): self.mBackgroundColor = color ## # Returns whether the given \a tileset is used by any tile layer of this # map. ## def isTilesetUsed(self, tileset): for layer in self.mLayers: if (layer.referencesTileset(tileset)): return True return False ## # Creates a new map that contains the given \a layer. The map size will be # determined by the size of the layer. # # The orientation defaults to Unknown and the tile width and height will # default to 0. In case this map needs to be rendered, these properties # will need to be properly set. ## def fromLayer(layer): result = Map(Map.Orientation.Unknown, layer.width(), layer.height(), 0, 0) result.addLayer(layer) return result def layerDataFormat(self): return self.mLayerDataFormat def setLayerDataFormat(self, format): self.mLayerDataFormat = format ## # Sets the next id to be used for objects on this map. ## def setNextObjectId(self, nextId): self.mNextObjectId = nextId ## # Returns the next object id for this map. ## def nextObjectId(self): return self.mNextObjectId ## # Returns the next object id for this map and allocates a new one. ## def takeNextObjectId(self): return self.mNextObjectId + 1 def adoptLayer(self, layer): layer.setMap(self) tileLayer = layer.asTileLayer() if tileLayer: self.adjustDrawMargins(tileLayer.drawMargins()) group = layer.asObjectGroup() if group: for o in group.objects(): if (o.id() == 0): o.setId(self.takeNextObjectId())
class MapObjectModel(QAbstractItemModel): objectsAdded = pyqtSignal(QList) objectsChanged = pyqtSignal(QList) objectsRemoved = pyqtSignal(QList) def __init__(self, parent): super().__init__(parent) self.mObjectGroups = QList() self.mObjects = QMap() self.mGroups = QMap() self.mMapDocument = None self.mMap = None self.mObject = None self.mObjectGroupIcon = ":/images/16x16/layer-object.png" def index(self, *args): l = len(args) if l>0: tp = type(args[0]) if tp==int: if l==2: args = (args[0], args[1], QModelIndex()) row, column, parent = args if (not parent.isValid()): if (row < self.mObjectGroups.count()): return self.createIndex(row, column, self.mGroups[self.mObjectGroups.at(row)]) return QModelIndex() og = self.toObjectGroup(parent) # happens when deleting the last item in a parent if (row >= og.objectCount()): return QModelIndex() # Paranoia: sometimes "fake" objects are in use (see createobjecttool) if (not self.mObjects.contains(og.objects().at(row))): return QModelIndex() return self.createIndex(row, column, self.mObjects[og.objects()[row]]) elif tp==ObjectGroup: og = args[0] row = self.mObjectGroups.indexOf(og) return self.createIndex(row, 0, self.mGroups[og]) elif tp==MapObject: if l==1: args = (args[0],0) o, column = args row = o.objectGroup().objects().indexOf(o) return self.createIndex(row, column, self.mObjects[o]) def parent(self, index): mapObject = self.toMapObject(index) if mapObject: return self.index(mapObject.objectGroup()) return QModelIndex() def rowCount(self, parent = QModelIndex()): if (not self.mMapDocument): return 0 if (not parent.isValid()): return self.mObjectGroups.size() og = self.toObjectGroup(parent) if og: return og.objectCount() return 0 def columnCount(self, parent = QModelIndex()): return 2 # MapObject name|type def headerData(self, section, orientation, role = Qt.DisplayRole): if (role == Qt.DisplayRole and orientation == Qt.Horizontal): x = section if x==0: return self.tr("Name") elif x==1: return self.tr("Type") return QVariant() def setData(self, index, value, role): mapObject = self.toMapObject(index) if mapObject: x = role if x==Qt.CheckStateRole: c = value visible = (c == Qt.Checked) if (visible != mapObject.isVisible()): command = SetMapObjectVisible(self.mMapDocument, mapObject, visible) self.mMapDocument.undoStack().push(command) return True elif x==Qt.EditRole: s = value if (index.column() == 0 and s != mapObject.name()): undo = self.mMapDocument.undoStack() undo.beginMacro(self.tr("Change Object Name")) undo.push(ChangeMapObject(self.mMapDocument, mapObject, s, mapObject.type())) undo.endMacro() if (index.column() == 1 and s != mapObject.type()): undo = self.mMapDocument.undoStack() undo.beginMacro(self.tr("Change Object Type")) undo.push(ChangeMapObject(self.mMapDocument, mapObject, mapObject.name(), s)) undo.endMacro() return True return False objectGroup = self.toObjectGroup(index) if objectGroup: x = role if x==Qt.CheckStateRole: layerModel = self.mMapDocument.layerModel() layerIndex = self.mMap.layers().indexOf(objectGroup) row = layerModel.layerIndexToRow(layerIndex) layerModel.setData(layerModel.index(row), value, role) return True elif x==Qt.EditRole: newName = value if (objectGroup.name() != newName): layerIndex = self.mMap.layers().indexOf(objectGroup) rename = RenameLayer(self.mMapDocument, layerIndex, newName) self.mMapDocument.undoStack().push(rename) return True return False return False def data(self, index, role = Qt.DisplayRole): mapObject = self.toMapObject(index) if mapObject: x = role if x==Qt.DisplayRole or x==Qt.EditRole: if index.column(): _x = mapObject.type() else: _x = mapObject.name() return _x elif x==Qt.DecorationRole: return QVariant() # no icon . maybe the color? elif x==Qt.CheckStateRole: if (index.column() > 0): return QVariant() if mapObject.isVisible(): _x = Qt.Checked else: _x = Qt.Unchecked return _x elif x==LayerModel.UserRoles.OpacityRole: return 1.0 else: return QVariant() objectGroup = self.toObjectGroup(index) if objectGroup: x = role if x==Qt.DisplayRole or x==Qt.EditRole: if index.column(): _x = QVariant() else: _x = objectGroup.name() return _x elif x==Qt.DecorationRole: if index.column(): _x = QVariant() else: _x = self.mObjectGroupIcon return _x elif x==Qt.CheckStateRole: if (index.column() > 0): return QVariant() if objectGroup.isVisible(): _x = Qt.Checked else: _x = Qt.Unchecked return _x elif x==LayerModel.UserRoles.OpacityRole: return objectGroup.opacity() else: return QVariant() return QVariant() def flags(self, index): rc = super().flags(index) if (index.column() == 0): rc |= Qt.ItemIsUserCheckable | Qt.ItemIsEditable elif (index.parent().isValid()): rc |= Qt.ItemIsEditable # MapObject type return rc def toObjectGroup(self, index): if (not index.isValid()): return None oog = index.internalPointer() if oog: return oog.mGroup def toMapObject(self, index): if (not index.isValid()): return None oog = index.internalPointer() if oog: return oog.mObject def toLayer(self, index): if (not index.isValid()): return None oog = index.internalPointer() if oog: if oog.mGroup: _x = oog.mGroup else: _x = oog.mObject.objectGroup() return _x def setMapDocument(self, mapDocument): if (self.mMapDocument == mapDocument): return if (self.mMapDocument): self.mMapDocument.disconnect() self.beginResetModel() self.mMapDocument = mapDocument self.mMap = None self.mObjectGroups.clear() self.mGroups.clear() self.mGroups.clear() self.mObjects.clear() self.mObjects.clear() if (self.mMapDocument): self.mMap = self.mMapDocument.map() self.mMapDocument.layerAdded.connect(self.layerAdded) self.mMapDocument.layerChanged.connect(self.layerChanged) self.mMapDocument.layerAboutToBeRemoved.connect(self.layerAboutToBeRemoved) for og in self.mMap.objectGroups(): if GROUPS_IN_DISPLAY_ORDER: self.mObjectGroups.prepend(og) else: self.mObjectGroups.append(og) self.mGroups.insert(og, ObjectOrGroup(og)) for o in og.objects(): self.mObjects.insert(o, ObjectOrGroup(o)) self.endResetModel() def insertObject(self, og, index, o): if (index >= 0): _x = index else: _x = og.objectCount() row = _x self.beginInsertRows(self.index(og), row, row) og.insertObject(row, o) self.mObjects.insert(o, ObjectOrGroup(o)) self.endInsertRows() self.objectsAdded.emit(QList([o])) def removeObject(self, og, o): objects = QList() objects.append(o) row = og.objects().indexOf(o) self.beginRemoveRows(self.index(og), row, row) og.removeObjectAt(row) self.mObjects.remove(o) self.endRemoveRows() self.objectsRemoved.emit(objects) return row def moveObjects(self, og, _from, to, count): parent = self.index(og) if (not self.beginMoveRows(parent, _from, _from + count - 1, parent, to)): return og.moveObjects(_from, to, count) self.endMoveRows() # ObjectGroup color changed # FIXME: layerChanged should let the scene know that objects need redrawing def emitObjectsChanged(self, objects): if objects.isEmpty(): return self.objectsChanged.emit(objects) def setObjectName(self, o, name): if o.name() == name: return o.setName(name) index = self.index(o) self.dataChanged.emit(index, index) self.objectsChanged.emit(QList([o])) def setObjectType(self, o, type): if o.type() == type: return o.setType(type) index = self.index(o, 1) self.dataChanged.emit(index, index) self.objectsChanged.emit(QList([o])) def setObjectPolygon(self, o, polygon): if o.polygon() == polygon: return o.setPolygon(polygon) self.objectsChanged.emit(QList([o])) def setObjectPosition(self, o, pos): if o.position() == pos: return o.setPosition(pos) self.objectsChanged.emit(QList([o])) def setObjectSize(self, o, size): if o.size() == size: return o.setSize(size) self.objectsChanged.emit(QList([o])) def setObjectRotation(self, o, rotation): if o.rotation() == rotation: return o.setRotation(rotation) self.objectsChanged.emit(QList([o])) def setObjectVisible(self, o, visible): if o.isVisible() == visible: return o.setVisible(visible) index = self.index(o) self.dataChanged.emit(index, index) self.objectsChanged.emit(QList([o])) def layerAdded(self, index): layer = self.mMap.layerAt(index) og = layer.asObjectGroup() if og: if (not self.mGroups.contains(og)): prev = None for index in range(index - 1, -1, -1): prev = self.mMap.layerAt(index).asObjectGroup() if prev: break if GROUPS_IN_DISPLAY_ORDER: if prev: _x = self.mObjectGroups.indexOf(prev) else: _x = self.mObjectGroups.count() index = _x else: if prev: index = self.mObjectGroups.indexOf(prev) + 1 else: index = 0 self.mObjectGroups.insert(index, og) row = self.mObjectGroups.indexOf(og) self.beginInsertRows(QModelIndex(), row, row) self.mGroups.insert(og, ObjectOrGroup(og)) for o in og.objects(): if (not self.mObjects.contains(o)): self.mObjects.insert(o, ObjectOrGroup(o)) self.endInsertRows() def layerChanged(self, index): layer = self.mMap.layerAt(index) og = layer.asObjectGroup() if og: index = self.index(og) self.dataChanged.emit(index, index) def layerAboutToBeRemoved(self, index): layer = self.mMap.layerAt(index) og = layer.asObjectGroup() if og: row = self.mObjectGroups.indexOf(og) self.beginRemoveRows(QModelIndex(), row, row) self.mObjectGroups.removeAt(row) self.mGroups.remove(og) for o in og.objects(): self.mObjects.remove(og) self.endRemoveRows()
class RaiseLowerHelper(): def __init__(self, mapScene): self.mSelectionRanges = RangeSet() self.mMapDocument = mapScene.mapDocument() self.mMapScene = mapScene # Context self.mObjectGroup = None self.mRelatedObjects = QList() def raise_(self): if (not self.initContext()): return # Iterate backwards over the ranges in order to keep the indexes valid size = len(self.mSelectionRanges) if size <= 0: # no range return firstRange = self.mSelectionRanges.begin() it = self.mSelectionRanges.end() if (it == firstRange): # no range return # For each range of objects, only the first will move commands = QList() lastIndex = len(self.mRelatedObjects) - 1 for i in range(size - 1, -1, -1): it = self.mSelectionRanges.item(i) value = it[1] # The last range may be already at the top of the related items if value == lastIndex: continue movingItem = self.mRelatedObjects.at(value) targetItem = self.mRelatedObjects.at(value + 1) _from = int(movingItem.zValue()) to = int(targetItem.zValue() + 1) commands.append( ChangeMapObjectsOrder(self.mMapDocument, self.mObjectGroup, _from, to, 1)) self.push(commands, QCoreApplication.translate("Undo Commands", "Raise Object")) def lower(self): if (not self.initContext()): return # For each range of objects, only the first will move commands = QList() for it in self.mSelectionRanges: value = it[0] # The first range may be already at the bottom of the related items if (value == 0): continue movingItem = self.mRelatedObjects.at(value) targetItem = self.mRelatedObjects.at(value - 1) _from = int(movingItem.zValue()) to = int(targetItem.zValue()) commands.append( ChangeMapObjectsOrder(self.mMapDocument, self.mObjectGroup, _from, to, 1)) self.push(commands, QCoreApplication.translate("Undo Commands", "Lower Object")) def raiseToTop(self): selectedItems = self.mMapScene.selectedObjectItems() objectGroup = RaiseLowerHelper.sameObjectGroup(selectedItems) if (not objectGroup): return if (objectGroup.drawOrder() != ObjectGroup.DrawOrder.IndexOrder): return ranges = RangeSet() for item in selectedItems: ranges.insert(int(item.zValue())) # Iterate backwards over the ranges in order to keep the indexes valid size = len(ranges) if size <= 0: # no range return commands = QList() to = objectGroup.objectCount() for i in range(size - 1, -1, -1): it = ranges.item(i) first = it[0] last = it[1] count = last - first + 1 if (last + 1 == to): to -= count continue _from = first commands.append( ChangeMapObjectsOrder(self.mMapDocument, objectGroup, _from, to, count)) to -= count self.push( commands, QCoreApplication.translate("Undo Commands", "Raise Object To Top")) def lowerToBottom(self): selectedItems = self.mMapScene.selectedObjectItems() objectGroup = RaiseLowerHelper.sameObjectGroup(selectedItems) if (not objectGroup): return if (objectGroup.drawOrder() != ObjectGroup.DrawOrder.IndexOrder): return ranges = RangeSet() for item in selectedItems: ranges.insert(int(item.zValue())) commands = QList() to = 0 for it in ranges: first = it[0] _from = first count = it[1] - first + 1 if (_from == to): to += count continue commands.append( ChangeMapObjectsOrder(self.mMapDocument, objectGroup, _from, to, count)) to += count self.push( commands, QCoreApplication.translate("Undo Commands", "Lower Object To Bottom")) def sameObjectGroup(items): if (items.isEmpty()): return None # All selected objects need to be in the same group group = items.begin().mapObject().objectGroup() for item in items: if (item.mapObject().objectGroup() != group): return None return group ## # Initializes the context in which objects are being raised or lowered. Only # used for single-step raising and lowering, since the context is not relevant # when raising to the top or lowering to the bottom. # # Returns whether the operation can be performed. ## def initContext(self): self.mObjectGroup = None self.mRelatedObjects.clear() self.mSelectionRanges.clear() selectedItems = self.mMapScene.selectedObjectItems() if (selectedItems.isEmpty()): return False # All selected objects need to be in the same group self.mObjectGroup = selectedItems.begin().mapObject().objectGroup() if (self.mObjectGroup.drawOrder() != ObjectGroup.DrawOrder.IndexOrder): return False shape = QPainterPath() for item in selectedItems: if (item.mapObject().objectGroup() != self.mObjectGroup): return False shape |= item.mapToScene(item.shape()) # The list of related items are all items from the same object group # that share space with the selected items. items = self.mMapScene.items(shape, Qt.IntersectsItemShape, Qt.AscendingOrder) for item in items: if type(item) == MapObjectItem: if (item.mapObject().objectGroup() == self.mObjectGroup): self.mRelatedObjects.append(item) for item in selectedItems: index = self.mRelatedObjects.indexOf(item) self.mSelectionRanges.insert(index) return True def push(self, commands, text): if (commands.isEmpty()): return undoStack = self.mMapDocument.undoStack() undoStack.beginMacro(text) for command in commands: undoStack.push(command) undoStack.endMacro()
class TileStampModel(QAbstractItemModel): stampAdded = pyqtSignal(TileStamp) stampRenamed = pyqtSignal(TileStamp) stampChanged = pyqtSignal(TileStamp) stampRemoved = pyqtSignal(TileStamp) def __init__(self, parent = None): super().__init__(parent) self.mStamps = QList() def index(self, *args): l = len(args) if l==1: stamp = args[0] i = self.mStamps.indexOf(stamp) if i == -1: return QModelIndex() else: return TileStampModel.index(i, 0) elif l==2 or l==3: if l==2: row, column = args elif l==3: row, column, parent = args if (not self.hasIndex(row, column, parent)): return QModelIndex() if (not parent.isValid()): return self.createIndex(row, column) elif (self.isStamp(parent)): return self.createIndex(row, column, parent.row() + 1) return QModelIndex() def parent(self, index): id = index.internalId() if id: return self.createIndex(id - 1, 0) return QModelIndex() def rowCount(self, parent = QModelIndex()): if (not parent.isValid()): return self.mStamps.size() elif (self.isStamp(parent)): stamp = self.mStamps.at(parent.row()) count = stamp.variations().size() # it does not make much sense to expand single variations if count==1: return 0 else: return count return 0 def columnCount(self, parent = QModelIndex()): return 2 # stamp | probability def headerData(self, section, orientation, role = Qt.DisplayRole): if (role == Qt.DisplayRole and orientation == Qt.Horizontal): x = section if x==0: return self.tr("Stamp") elif x==1: return self.tr("Probability") return QVariant() def setData(self, index, value, role = Qt.EditRole): if self.isStamp(index): stamp = self.mStamps[index.row()] if (index.column() == 0): # stamp name x = role if x==Qt.EditRole: stamp.setName(value.toString()) self.dataChanged.emit(index, index) self.stampRenamed.emit(stamp) self.stampChanged.emit(stamp) return True else: pass elif (index.column() == 1): # variation probability parent = index.parent() if self.isStamp(parent): stamp = self.mStamps[parent.row()] stamp.setProbability(index.row(), value.toReal()) self.dataChanged.emit(index, index) probabilitySumIndex = TileStampModel.index(parent.row(), 1) self.dataChanged.emit(probabilitySumIndex, probabilitySumIndex) self.stampChanged.emit(stamp) return True return False def data(self, index, role = Qt.DisplayRole): if (self.isStamp(index)): stamp = self.mStamps.at(index.row()) if (index.column() == 0): # preview and name x = role if x==Qt.DisplayRole or x==Qt.EditRole: return stamp.name() elif x==Qt.DecorationRole: map = stamp.variations().first().map thumbnail = self.mThumbnailCache.value(map) if (thumbnail.isNull()): renderer = ThumbnailRenderer(map) thumbnail = renderThumbnail(renderer) self.mThumbnailCache.insert(map, thumbnail) return thumbnail elif (index.column() == 1): # sum of probabilities x = role if x==Qt.DisplayRole: if (stamp.variations().size() > 1): sum = 0 for variation in stamp.variations(): sum += variation.probability return sum else: variation = self.variationAt(index) if variation: if (index.column() == 0): x = role if x==Qt.DecorationRole: map = variation.map thumbnail = self.mThumbnailCache.value(map) if (thumbnail.isNull()): renderer = ThumbnailRenderer(map) thumbnail = renderThumbnail(renderer) self.mThumbnailCache.insert(map, thumbnail) return thumbnail elif (index.column() == 1): x = role if x==Qt.DisplayRole or x==Qt.EditRole: return variation.probability return QVariant() def flags(self, index): rc = QAbstractItemModel.flags(index) validParent = index.parent().isValid() if ((not validParent and index.column() == 0) or # can edit stamp names (validParent and index.column() == 1)): # and variation probability rc |= Qt.ItemIsEditable return rc def removeRows(self, row, count, parent): if (parent.isValid()): # removing variations stamp = self.mStamps[parent.row()] # if only one variation is left, we make all variation rows disappear if (stamp.variations().size() - count == 1): self.beginRemoveRows(parent, 0, count) else: self.beginRemoveRows(parent, row, row + count - 1) for x in range(count, 0, -1): self.mThumbnailCache.remove(stamp.variations().at(row).map) stamp.deleteVariation(row) self.endRemoveRows() if (stamp.variations().isEmpty()): # remove stamp since all its variations were removed self.beginRemoveRows(QModelIndex(), parent.row(), parent.row()) self.stampRemoved.emit(stamp) self.mStamps.removeAt(parent.row()) self.endRemoveRows() else : if (row == 0): # preview on stamp and probability sum need update # (while technically I think this is correct, it triggers a # repainting issue in QTreeView) #emit dataChanged(index(parent.row(), 0), # self.index(parent.row(), 1)) pass self.stampChanged.emit(stamp) else : # removing stamps self.beginRemoveRows(parent, row, row + count - 1) for x in range(count, 0, -1): for variation in self.mStamps.at(row).variations(): self.mThumbnailCache.remove(variation.map) self.stampRemoved.emit(self.mStamps.at(row)) self.mStamps.removeAt(row) self.endRemoveRows() return True ## # Returns the stamp at the given \a index. ## def stampAt(self, index): return self.mStamps.at(index.row()) def isStamp(self, index): return index.isValid() \ and not index.parent().isValid() \ and index.row() < self.mStamps.size() def variationAt(self, index): if (not index.isValid()): return None parent = index.parent() if (self.isStamp(parent)): stamp = self.mStamps.at(parent.row()) return stamp.variations().at(index.row()) return None def stamps(self): return self.mStamps def addStamp(self, stamp): if (self.mStamps.contains(stamp)): return self.beginInsertRows(QModelIndex(), self.mStamps.size(), self.mStamps.size()) self.mStamps.append(stamp) self.stampAdded.emit(stamp) self.endInsertRows() def removeStamp(self, stamp): index = self.mStamps.indexOf(stamp) if (index == -1): return self.beginRemoveRows(QModelIndex(), index, index) self.mStamps.removeAt(index) self.endRemoveRows() for variation in stamp.variations(): self.mThumbnailCache.remove(variation.map) self.stampRemoved.emit(stamp) def addVariation(self, stamp, variation): index = self.mStamps.indexOf(stamp) if (index == -1): return variationCount = stamp.variations().size() if (variationCount == 1): self.beginInsertRows(TileStampModel.index(index, 0), 0, 1) else: self.beginInsertRows(TileStampModel.index(index, 0), variationCount, variationCount) self.mStamps[index].addVariation(variation) self.endInsertRows() probabilitySumIndex = TileStampModel.index(index, 1) self.dataChanged.emit(probabilitySumIndex, probabilitySumIndex) self.stampChanged.emit(stamp) def clear(self): self.beginResetModel() self.mStamps.clear() self.mThumbnailCache.clear() self.endResetModel()
class MapObjectModel(QAbstractItemModel): objectsAdded = pyqtSignal(QList) objectsChanged = pyqtSignal(QList) objectsRemoved = pyqtSignal(QList) def __init__(self, parent): super().__init__(parent) self.mObjectGroups = QList() self.mObjects = QMap() self.mGroups = QMap() self.mMapDocument = None self.mMap = None self.mObject = None self.mObjectGroupIcon = ":/images/16x16/layer-object.png" def index(self, *args): l = len(args) if l > 0: tp = type(args[0]) if tp == int: if l == 2: args = (args[0], args[1], QModelIndex()) row, column, parent = args if (not parent.isValid()): if (row < self.mObjectGroups.count()): return self.createIndex( row, column, self.mGroups[self.mObjectGroups.at(row)]) return QModelIndex() og = self.toObjectGroup(parent) # happens when deleting the last item in a parent if (row >= og.objectCount()): return QModelIndex() # Paranoia: sometimes "fake" objects are in use (see createobjecttool) if (not self.mObjects.contains(og.objects().at(row))): return QModelIndex() return self.createIndex(row, column, self.mObjects[og.objects()[row]]) elif tp == ObjectGroup: og = args[0] row = self.mObjectGroups.indexOf(og) return self.createIndex(row, 0, self.mGroups[og]) elif tp == MapObject: if l == 1: args = (args[0], 0) o, column = args row = o.objectGroup().objects().indexOf(o) return self.createIndex(row, column, self.mObjects[o]) def parent(self, index): mapObject = self.toMapObject(index) if mapObject: return self.index(mapObject.objectGroup()) return QModelIndex() def rowCount(self, parent=QModelIndex()): if (not self.mMapDocument): return 0 if (not parent.isValid()): return self.mObjectGroups.size() og = self.toObjectGroup(parent) if og: return og.objectCount() return 0 def columnCount(self, parent=QModelIndex()): return 2 # MapObject name|type def headerData(self, section, orientation, role=Qt.DisplayRole): if (role == Qt.DisplayRole and orientation == Qt.Horizontal): x = section if x == 0: return self.tr("Name") elif x == 1: return self.tr("Type") return QVariant() def setData(self, index, value, role): mapObject = self.toMapObject(index) if mapObject: x = role if x == Qt.CheckStateRole: c = value visible = (c == Qt.Checked) if (visible != mapObject.isVisible()): command = SetMapObjectVisible(self.mMapDocument, mapObject, visible) self.mMapDocument.undoStack().push(command) return True elif x == Qt.EditRole: s = value if (index.column() == 0 and s != mapObject.name()): undo = self.mMapDocument.undoStack() undo.beginMacro(self.tr("Change Object Name")) undo.push( ChangeMapObject(self.mMapDocument, mapObject, s, mapObject.type())) undo.endMacro() if (index.column() == 1 and s != mapObject.type()): undo = self.mMapDocument.undoStack() undo.beginMacro(self.tr("Change Object Type")) undo.push( ChangeMapObject(self.mMapDocument, mapObject, mapObject.name(), s)) undo.endMacro() return True return False objectGroup = self.toObjectGroup(index) if objectGroup: x = role if x == Qt.CheckStateRole: layerModel = self.mMapDocument.layerModel() layerIndex = self.mMap.layers().indexOf(objectGroup) row = layerModel.layerIndexToRow(layerIndex) layerModel.setData(layerModel.index(row), value, role) return True elif x == Qt.EditRole: newName = value if (objectGroup.name() != newName): layerIndex = self.mMap.layers().indexOf(objectGroup) rename = RenameLayer(self.mMapDocument, layerIndex, newName) self.mMapDocument.undoStack().push(rename) return True return False return False def data(self, index, role=Qt.DisplayRole): mapObject = self.toMapObject(index) if mapObject: x = role if x == Qt.DisplayRole or x == Qt.EditRole: if index.column(): _x = mapObject.type() else: _x = mapObject.name() return _x elif x == Qt.DecorationRole: return QVariant() # no icon . maybe the color? elif x == Qt.CheckStateRole: if (index.column() > 0): return QVariant() if mapObject.isVisible(): _x = Qt.Checked else: _x = Qt.Unchecked return _x elif x == LayerModel.UserRoles.OpacityRole: return 1.0 else: return QVariant() objectGroup = self.toObjectGroup(index) if objectGroup: x = role if x == Qt.DisplayRole or x == Qt.EditRole: if index.column(): _x = QVariant() else: _x = objectGroup.name() return _x elif x == Qt.DecorationRole: if index.column(): _x = QVariant() else: _x = self.mObjectGroupIcon return _x elif x == Qt.CheckStateRole: if (index.column() > 0): return QVariant() if objectGroup.isVisible(): _x = Qt.Checked else: _x = Qt.Unchecked return _x elif x == LayerModel.UserRoles.OpacityRole: return objectGroup.opacity() else: return QVariant() return QVariant() def flags(self, index): rc = super().flags(index) if (index.column() == 0): rc |= Qt.ItemIsUserCheckable | Qt.ItemIsEditable elif (index.parent().isValid()): rc |= Qt.ItemIsEditable # MapObject type return rc def toObjectGroup(self, index): if (not index.isValid()): return None oog = index.internalPointer() if oog: return oog.mGroup def toMapObject(self, index): if (not index.isValid()): return None oog = index.internalPointer() if oog: return oog.mObject def toLayer(self, index): if (not index.isValid()): return None oog = index.internalPointer() if oog: if oog.mGroup: _x = oog.mGroup else: _x = oog.mObject.objectGroup() return _x def setMapDocument(self, mapDocument): if (self.mMapDocument == mapDocument): return if (self.mMapDocument): self.mMapDocument.disconnect() self.beginResetModel() self.mMapDocument = mapDocument self.mMap = None self.mObjectGroups.clear() self.mGroups.clear() self.mGroups.clear() self.mObjects.clear() self.mObjects.clear() if (self.mMapDocument): self.mMap = self.mMapDocument.map() self.mMapDocument.layerAdded.connect(self.layerAdded) self.mMapDocument.layerChanged.connect(self.layerChanged) self.mMapDocument.layerAboutToBeRemoved.connect( self.layerAboutToBeRemoved) for og in self.mMap.objectGroups(): if GROUPS_IN_DISPLAY_ORDER: self.mObjectGroups.prepend(og) else: self.mObjectGroups.append(og) self.mGroups.insert(og, ObjectOrGroup(og)) for o in og.objects(): self.mObjects.insert(o, ObjectOrGroup(o)) self.endResetModel() def insertObject(self, og, index, o): if (index >= 0): _x = index else: _x = og.objectCount() row = _x self.beginInsertRows(self.index(og), row, row) og.insertObject(row, o) self.mObjects.insert(o, ObjectOrGroup(o)) self.endInsertRows() self.objectsAdded.emit(QList([o])) def removeObject(self, og, o): objects = QList() objects.append(o) row = og.objects().indexOf(o) self.beginRemoveRows(self.index(og), row, row) og.removeObjectAt(row) self.mObjects.remove(o) self.endRemoveRows() self.objectsRemoved.emit(objects) return row def moveObjects(self, og, _from, to, count): parent = self.index(og) if (not self.beginMoveRows(parent, _from, _from + count - 1, parent, to)): return og.moveObjects(_from, to, count) self.endMoveRows() # ObjectGroup color changed # FIXME: layerChanged should let the scene know that objects need redrawing def emitObjectsChanged(self, objects): if objects.isEmpty(): return self.objectsChanged.emit(objects) def setObjectName(self, o, name): if o.name() == name: return o.setName(name) index = self.index(o) self.dataChanged.emit(index, index) self.objectsChanged.emit(QList([o])) def setObjectType(self, o, type): if o.type() == type: return o.setType(type) index = self.index(o, 1) self.dataChanged.emit(index, index) self.objectsChanged.emit(QList([o])) def setObjectPolygon(self, o, polygon): if o.polygon() == polygon: return o.setPolygon(polygon) self.objectsChanged.emit(QList([o])) def setObjectPosition(self, o, pos): if o.position() == pos: return o.setPosition(pos) self.objectsChanged.emit(QList([o])) def setObjectSize(self, o, size): if o.size() == size: return o.setSize(size) self.objectsChanged.emit(QList([o])) def setObjectRotation(self, o, rotation): if o.rotation() == rotation: return o.setRotation(rotation) self.objectsChanged.emit(QList([o])) def setObjectVisible(self, o, visible): if o.isVisible() == visible: return o.setVisible(visible) index = self.index(o) self.dataChanged.emit(index, index) self.objectsChanged.emit(QList([o])) def layerAdded(self, index): layer = self.mMap.layerAt(index) og = layer.asObjectGroup() if og: if (not self.mGroups.contains(og)): prev = None for index in range(index - 1, -1, -1): prev = self.mMap.layerAt(index).asObjectGroup() if prev: break if GROUPS_IN_DISPLAY_ORDER: if prev: _x = self.mObjectGroups.indexOf(prev) else: _x = self.mObjectGroups.count() index = _x else: if prev: index = self.mObjectGroups.indexOf(prev) + 1 else: index = 0 self.mObjectGroups.insert(index, og) row = self.mObjectGroups.indexOf(og) self.beginInsertRows(QModelIndex(), row, row) self.mGroups.insert(og, ObjectOrGroup(og)) for o in og.objects(): if (not self.mObjects.contains(o)): self.mObjects.insert(o, ObjectOrGroup(o)) self.endInsertRows() def layerChanged(self, index): layer = self.mMap.layerAt(index) og = layer.asObjectGroup() if og: index = self.index(og) self.dataChanged.emit(index, index) def layerAboutToBeRemoved(self, index): layer = self.mMap.layerAt(index) og = layer.asObjectGroup() if og: row = self.mObjectGroups.indexOf(og) self.beginRemoveRows(QModelIndex(), row, row) self.mObjectGroups.removeAt(row) self.mGroups.remove(og) for o in og.objects(): self.mObjects.remove(og) self.endRemoveRows()
class TileStampModel(QAbstractItemModel): stampAdded = pyqtSignal(TileStamp) stampRenamed = pyqtSignal(TileStamp) stampChanged = pyqtSignal(TileStamp) stampRemoved = pyqtSignal(TileStamp) def __init__(self, parent=None): super().__init__(parent) self.mStamps = QList() def index(self, *args): l = len(args) if l == 1: stamp = args[0] i = self.mStamps.indexOf(stamp) if i == -1: return QModelIndex() else: return TileStampModel.index(i, 0) elif l == 2 or l == 3: if l == 2: row, column = args elif l == 3: row, column, parent = args if (not self.hasIndex(row, column, parent)): return QModelIndex() if (not parent.isValid()): return self.createIndex(row, column) elif (self.isStamp(parent)): return self.createIndex(row, column, parent.row() + 1) return QModelIndex() def parent(self, index): id = index.internalId() if id: return self.createIndex(id - 1, 0) return QModelIndex() def rowCount(self, parent=QModelIndex()): if (not parent.isValid()): return self.mStamps.size() elif (self.isStamp(parent)): stamp = self.mStamps.at(parent.row()) count = stamp.variations().size() # it does not make much sense to expand single variations if count == 1: return 0 else: return count return 0 def columnCount(self, parent=QModelIndex()): return 2 # stamp | probability def headerData(self, section, orientation, role=Qt.DisplayRole): if (role == Qt.DisplayRole and orientation == Qt.Horizontal): x = section if x == 0: return self.tr("Stamp") elif x == 1: return self.tr("Probability") return QVariant() def setData(self, index, value, role=Qt.EditRole): if self.isStamp(index): stamp = self.mStamps[index.row()] if (index.column() == 0): # stamp name x = role if x == Qt.EditRole: stamp.setName(value.toString()) self.dataChanged.emit(index, index) self.stampRenamed.emit(stamp) self.stampChanged.emit(stamp) return True else: pass elif (index.column() == 1): # variation probability parent = index.parent() if self.isStamp(parent): stamp = self.mStamps[parent.row()] stamp.setProbability(index.row(), value.toReal()) self.dataChanged.emit(index, index) probabilitySumIndex = TileStampModel.index(parent.row(), 1) self.dataChanged.emit(probabilitySumIndex, probabilitySumIndex) self.stampChanged.emit(stamp) return True return False def data(self, index, role=Qt.DisplayRole): if (self.isStamp(index)): stamp = self.mStamps.at(index.row()) if (index.column() == 0): # preview and name x = role if x == Qt.DisplayRole or x == Qt.EditRole: return stamp.name() elif x == Qt.DecorationRole: map = stamp.variations().first().map thumbnail = self.mThumbnailCache.value(map) if (thumbnail.isNull()): renderer = ThumbnailRenderer(map) thumbnail = renderThumbnail(renderer) self.mThumbnailCache.insert(map, thumbnail) return thumbnail elif (index.column() == 1): # sum of probabilities x = role if x == Qt.DisplayRole: if (stamp.variations().size() > 1): sum = 0 for variation in stamp.variations(): sum += variation.probability return sum else: variation = self.variationAt(index) if variation: if (index.column() == 0): x = role if x == Qt.DecorationRole: map = variation.map thumbnail = self.mThumbnailCache.value(map) if (thumbnail.isNull()): renderer = ThumbnailRenderer(map) thumbnail = renderThumbnail(renderer) self.mThumbnailCache.insert(map, thumbnail) return thumbnail elif (index.column() == 1): x = role if x == Qt.DisplayRole or x == Qt.EditRole: return variation.probability return QVariant() def flags(self, index): rc = QAbstractItemModel.flags(index) validParent = index.parent().isValid() if ((not validParent and index.column() == 0) or # can edit stamp names (validParent and index.column() == 1)): # and variation probability rc |= Qt.ItemIsEditable return rc def removeRows(self, row, count, parent): if (parent.isValid()): # removing variations stamp = self.mStamps[parent.row()] # if only one variation is left, we make all variation rows disappear if (stamp.variations().size() - count == 1): self.beginRemoveRows(parent, 0, count) else: self.beginRemoveRows(parent, row, row + count - 1) for x in range(count, 0, -1): self.mThumbnailCache.remove(stamp.variations().at(row).map) stamp.deleteVariation(row) self.endRemoveRows() if (stamp.variations().isEmpty()): # remove stamp since all its variations were removed self.beginRemoveRows(QModelIndex(), parent.row(), parent.row()) self.stampRemoved.emit(stamp) self.mStamps.removeAt(parent.row()) self.endRemoveRows() else: if (row == 0): # preview on stamp and probability sum need update # (while technically I think this is correct, it triggers a # repainting issue in QTreeView) #emit dataChanged(index(parent.row(), 0), # self.index(parent.row(), 1)) pass self.stampChanged.emit(stamp) else: # removing stamps self.beginRemoveRows(parent, row, row + count - 1) for x in range(count, 0, -1): for variation in self.mStamps.at(row).variations(): self.mThumbnailCache.remove(variation.map) self.stampRemoved.emit(self.mStamps.at(row)) self.mStamps.removeAt(row) self.endRemoveRows() return True ## # Returns the stamp at the given \a index. ## def stampAt(self, index): return self.mStamps.at(index.row()) def isStamp(self, index): return index.isValid() \ and not index.parent().isValid() \ and index.row() < self.mStamps.size() def variationAt(self, index): if (not index.isValid()): return None parent = index.parent() if (self.isStamp(parent)): stamp = self.mStamps.at(parent.row()) return stamp.variations().at(index.row()) return None def stamps(self): return self.mStamps def addStamp(self, stamp): if (self.mStamps.contains(stamp)): return self.beginInsertRows(QModelIndex(), self.mStamps.size(), self.mStamps.size()) self.mStamps.append(stamp) self.stampAdded.emit(stamp) self.endInsertRows() def removeStamp(self, stamp): index = self.mStamps.indexOf(stamp) if (index == -1): return self.beginRemoveRows(QModelIndex(), index, index) self.mStamps.removeAt(index) self.endRemoveRows() for variation in stamp.variations(): self.mThumbnailCache.remove(variation.map) self.stampRemoved.emit(stamp) def addVariation(self, stamp, variation): index = self.mStamps.indexOf(stamp) if (index == -1): return variationCount = stamp.variations().size() if (variationCount == 1): self.beginInsertRows(TileStampModel.index(index, 0), 0, 1) else: self.beginInsertRows(TileStampModel.index(index, 0), variationCount, variationCount) self.mStamps[index].addVariation(variation) self.endInsertRows() probabilitySumIndex = TileStampModel.index(index, 1) self.dataChanged.emit(probabilitySumIndex, probabilitySumIndex) self.stampChanged.emit(stamp) def clear(self): self.beginResetModel() self.mStamps.clear() self.mThumbnailCache.clear() self.endResetModel()
class QtButtonPropertyBrowserPrivate(): def __init__(self): self.q_ptr = None self.WidgetItem = WidgetItem() self.m_indexToItem = QMap() self.m_itemToIndex = QMap() self.m_widgetToItem = QMap() self.m_buttonToItem = QMap() self.m_mainLayout = None self.m_children = QList() self.m_recreateQueue = QList() def createEditor(self, property, parent): return self.q_ptr.createEditor(property, parent) def createButton(self, parent=None): button = QToolButton(parent) button.setCheckable(True) button.setSizePolicy(QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)) button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) button.setArrowType(Qt.DownArrow) button.setIconSize(QSize(3, 16)) ### #QIcon icon #icon.addPixmap(self.style().standardPixmap(QStyle.SP_ArrowDown), QIcon.Normal, QIcon.Off) #icon.addPixmap(self.style().standardPixmap(QStyle.SP_ArrowUp), QIcon.Normal, QIcon.On) #button.setIcon(icon) ### return button def gridRow(self, item): siblings = QList() if (item.parent): siblings = item.parent.children else: siblings = self.m_children row = 0 for sibling in siblings: if (sibling == item): return row row += self.gridSpan(sibling) return -1 def gridSpan(self, item): if (item.container and item.expanded): return 2 return 1 def init(self, parent): self.m_mainLayout = QGridLayout() parent.setLayout(self.m_mainLayout) item = QSpacerItem(0, 0, QSizePolicy.Fixed, QSizePolicy.Expanding) self.m_mainLayout.addItem(item, 0, 0) def slotEditorDestroyed(self): editor = self.q_ptr.sender() if (not editor): return if not self.m_widgetToItem.get(editor): return self.m_widgetToItem[editor].widget = 0 self.m_widgetToItem.remove(editor) def slotUpdate(self): for item in self.m_recreateQueue: parent = item.parent w = 0 l = 0 oldRow = self.gridRow(item) if (parent): w = parent.container l = parent.layout else: w = self.q_ptr l = self.m_mainLayout span = 1 if (not item.widget and not item.widgetLabel): span = 2 item.label = QLabel(w) item.label.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)) l.addWidget(item.label, oldRow, 0, 1, span) self.updateItem(item) self.m_recreateQueue.clear() def setExpanded(self, item, expanded): if (item.expanded == expanded): return if (not item.container): return item.expanded = expanded row = self.gridRow(item) parent = item.parent l = 0 if (parent): l = parent.layout else: l = self.m_mainLayout if (expanded): self.insertRow(l, row + 1) l.addWidget(item.container, row + 1, 0, 1, 2) item.container.show() else: l.removeWidget(item.container) item.container.hide() self.removeRow(l, row + 1) item.button.setChecked(expanded) if expanded: item.button.setArrowType(Qt.UpArrow) else: item.button.setArrowType(Qt.DownArrow) def slotToggled(self, checked): item = self.m_buttonToItem[self.q_ptr.sender()] if (not item): return self.setExpanded(item, checked) if (checked): self.q_ptr.expandedSignal.emit(self.m_itemToIndex[item]) else: self.q_ptr.collapsedSignal.emit(self.m_itemToIndex[item]) def updateLater(self): QTimer.singleShot(0, self.slotUpdate) def propertyInserted(self, index, afterIndex): afterItem = self.m_indexToItem[afterIndex] parentItem = self.m_indexToItem.value(index.parent()) newItem = WidgetItem() newItem.parent = parentItem layout = 0 parentWidget = 0 row = -1 if (not afterItem): row = 0 if (parentItem): parentItem.children.insert(0, newItem) else: self.m_children.insert(0, newItem) else: row = self.gridRow(afterItem) + self.gridSpan(afterItem) if (parentItem): parentItem.children.insert(parentItem.children.indexOf(afterItem) + 1, newItem) else: self.m_children.insert(self.m_children.indexOf(afterItem) + 1, newItem) if (not parentItem): layout = self.m_mainLayout parentWidget = self.q_ptr else: if (not parentItem.container): self.m_recreateQueue.removeAll(parentItem) grandParent = parentItem.parent l = 0 oldRow = self.gridRow(parentItem) if (grandParent): l = grandParent.layout else: l = self.m_mainLayout container = QFrame() container.setFrameShape(QFrame.Panel) container.setFrameShadow(QFrame.Raised) parentItem.container = container parentItem.button = self.createButton() self.m_buttonToItem[parentItem.button] = parentItem parentItem.button.toggled.connect(self.slotToggled) parentItem.layout = QGridLayout() container.setLayout(parentItem.layout) if (parentItem.label): l.removeWidget(parentItem.label) parentItem.label.close() parentItem.label = 0 span = 1 if (not parentItem.widget and not parentItem.widgetLabel): span = 2 l.addWidget(parentItem.button, oldRow, 0, 1, span) self.updateItem(parentItem) layout = parentItem.layout parentWidget = parentItem.container newItem.label = QLabel(parentWidget) newItem.label.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)) newItem.widget = self.createEditor(index.property(), parentWidget) if (newItem.widget): newItem.widget.destroyed.connect(self.slotEditorDestroyed) self.m_widgetToItem[newItem.widget] = newItem elif (index.property().hasValue()): newItem.widgetLabel = QLabel(parentWidget) newItem.widgetLabel.setSizePolicy(QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed)) self.insertRow(layout, row) span = 1 if (newItem.widget): layout.addWidget(newItem.widget, row, 1) elif (newItem.widgetLabel): layout.addWidget(newItem.widgetLabel, row, 1) else: span = 2 layout.addWidget(newItem.label, row, 0, span, 1) self.m_itemToIndex[newItem] = index self.m_indexToItem[index] = newItem self.updateItem(newItem) def propertyRemoved(self, index): item = self.m_indexToItem[index] self.m_indexToItem.remove(index) self.m_itemToIndex.remove(item) parentItem = item.parent row = self.gridRow(item) if (parentItem): parentItem.children.removeAt(parentItem.children.indexOf(item)) else: self.m_children.removeAt(self.m_children.indexOf(item)) colSpan = self.gridSpan(item) self.m_buttonToItem.remove(item.button) if (item.widget): item.widget.close() del item.widget if (item.label): item.label.close() del item.label if (item.widgetLabel): item.widgetLabel.close() del item.widgetLabel if (item.button): item.button.close() del item.button if (item.container): item.container.close() del item.container if (not parentItem): self.removeRow(self.m_mainLayout, row) if (colSpan > 1): self.removeRow(self.m_mainLayout, row) elif (len(parentItem.children) != 0): self.removeRow(parentItem.layout, row) if (colSpan > 1): self.removeRow(parentItem.layout, row) else: grandParent = parentItem.parent l = 0 if (grandParent): l = grandParent.layout else: l = self.m_mainLayout parentRow = self.gridRow(parentItem) parentSpan = self.gridSpan(parentItem) l.removeWidget(parentItem.button) l.removeWidget(parentItem.container) parentItem.button.close() del parentItem.button parentItem.container.close() del parentItem.container parentItem.button = 0 parentItem.container = 0 parentItem.layout = 0 if (not parentItem in self.m_recreateQueue): self.m_recreateQueue.append(parentItem) if (parentSpan > 1): self.removeRow(l, parentRow + 1) self.updateLater() self.m_recreateQueue.removeAll(item) del item def insertRow(self, layout, row): itemToPos = QMap() idx = 0 while (idx < len(layout)): r, c, rs, cs = layout.getItemPosition(idx) if (r >= row): itemToPos[layout.takeAt(idx)] = QRect(r + 1, c, rs, cs) else: idx += 1 for k in itemToPos.keys(): r = itemToPos[k] layout.addItem(k, r.x(), r.y(), r.width(), r.height()) def removeRow(self, layout, row): itemToPos = QMap() idx = 0 while (idx < len(layout)): r, c, rs, cs = layout.getItemPosition(idx) if (r > row): itemToPos[layout.takeAt(idx)] = QRect(r - 1, c, rs, cs) else: idx += 1 for k in itemToPos.keys(): r = itemToPos[k] layout.addItem(k, r.x(), r.y(), r.width(), r.height()) def propertyChanged(self, index): item = self.m_indexToItem[index] self.updateItem(item) def updateItem(self, item): property = self.m_itemToIndex[item].property() if (item.button): font = item.button.font() font.setUnderline(property.isModified()) item.button.setFont(font) item.button.setText(property.propertyName()) item.button.setToolTip(property.toolTip()) item.button.setStatusTip(property.statusTip()) item.button.setWhatsThis(property.whatsThis()) item.button.setEnabled(property.isEnabled()) if (item.label): font = item.label.font() font.setUnderline(property.isModified()) item.label.setFont(font) item.label.setText(property.propertyName()) item.label.setToolTip(property.toolTip()) item.label.setStatusTip(property.statusTip()) item.label.setWhatsThis(property.whatsThis()) item.label.setEnabled(property.isEnabled()) if (item.widgetLabel): font = item.widgetLabel.font() font.setUnderline(False) item.widgetLabel.setFont(font) item.widgetLabel.setText(property.valueText()) item.widgetLabel.setToolTip(property.valueText()) item.widgetLabel.setEnabled(property.isEnabled()) if (item.widget): font = item.widget.font() font.setUnderline(False) item.widget.setFont(font) item.widget.setEnabled(property.isEnabled()) item.widget.setToolTip(property.valueText())
class ObjectGroup(Layer): ## # Objects within an object group can either be drawn top down (sorted # by their y-coordinate) or by index (manual stacking order). # # The default is top down. ## class DrawOrder(): UnknownOrder = -1 TopDownOrder = 1 IndexOrder = 2 ## # Default constructor. ## def __init__(self, *args): self.mObjects = QList() self.mColor = QColor() l = len(args) if l == 0: super().__init__(Layer.ObjectGroupType, QString(), 0, 0, 0, 0) elif l == 5: ## # Constructor with some parameters. ## name, x, y, width, height = args super().__init__(Layer.ObjectGroupType, name, x, y, width, height) else: pass self.mDrawOrder = ObjectGroup.DrawOrder.IndexOrder ## # Destructor. ## def __del__(self): self.mObjects.clear() ## # Returns a pointer to the list of objects in this object group. ## def objects(self): return QList(self.mObjects) ## # Returns the number of objects in this object group. ## def objectCount(self): return self.mObjects.size() ## # Returns the object at the specified index. ## def objectAt(self, index): return self.mObjects.at(index) ## # Adds an object to this object group. ## def addObject(self, object): self.mObjects.append(object) object.setObjectGroup(self) if (self.mMap and object.id() == 0): object.setId(self.mMap.takeNextObjectId()) ## # Inserts an object at the specified index. This is only used for undoing # the removal of an object at the moment, to make sure not to change the # saved order of the objects. ## def insertObject(self, index, object): self.mObjects.insert(index, object) object.setObjectGroup(self) if (self.mMap and object.id() == 0): object.setId(self.mMap.takeNextObjectId()) ## # Removes an object from this object group. Ownership of the object is # transferred to the caller. # # @return the index at which the specified object was removed ## def removeObject(self, object): index = self.mObjects.indexOf(object) self.mObjects.removeAt(index) object.setObjectGroup(None) return index ## # Removes the object at the given index. Ownership of the object is # transferred to the caller. # # This is faster than removeObject when you've already got the index. # # @param index the index at which to remove an object ## def removeObjectAt(self, index): object = self.mObjects.takeAt(index) object.setObjectGroup(None) ## # Moves \a count objects starting at \a from to the index given by \a to. # # The \a to index may not lie within the range of objects that is # being moved. ## def moveObjects(self, _from, to, count): # It's an error when 'to' lies within the moving range of objects # Nothing to be done when 'to' is the start or the end of the range, or # when the number of objects to be moved is 0. if (to == _from or to == _from + count or count == 0): return movingObjects = self.mObjects[_from:_from + count] self.mObjects.erase(_from, _from + count) if (to > _from): to -= count for i in range(count): self.mObjects.insert(to + i, movingObjects[i]) ## # Returns the bounding rect around all objects in this object group. ## def objectsBoundingRect(self): boundingRect = QRectF() for object in self.mObjects: boundingRect = boundingRect.united(object.bounds()) return boundingRect ## # Returns whether this object group contains any objects. ## def isEmpty(self): return self.mObjects.isEmpty() ## # Computes and returns the set of tilesets used by this object group. ## def usedTilesets(self): tilesets = QSet() for object in self.mObjects: tile = object.cell().tile if tile: tilesets.insert(tile.sharedTileset()) return tilesets ## # Returns whether any tile objects in this object group reference tiles # in the given tileset. ## def referencesTileset(self, tileset): for object in self.mObjects: tile = object.cell().tile if (tile and tile.tileset() == tileset): return True return False ## # Replaces all references to tiles from \a oldTileset with tiles from # \a newTileset. ## def replaceReferencesToTileset(self, oldTileset, newTileset): for object in self.mObjects: tile = object.cell().tile if (tile and tile.tileset() == oldTileset): cell = object.cell() cell.tile = Tileset.tileAt(tile.id()) object.setCell(cell) ## # Offsets all objects within the group by the \a offset given in pixel # coordinates, and optionally wraps them. The object's center must be # within \a bounds, and wrapping occurs if the displaced center is out of # the bounds. # # \sa TileLayer.offset() ## def offsetObjects(self, offset, bounds, wrapX, wrapY): for object in self.mObjects: objectCenter = object.bounds().center() if (not bounds.contains(objectCenter)): continue newCenter = QPointF(objectCenter + offset) if (wrapX and bounds.width() > 0): nx = math.fmod(newCenter.x() - bounds.left(), bounds.width()) if nx < 0: x = bounds.width() + nx else: x = nx newCenter.setX(bounds.left() + x) if (wrapY and bounds.height() > 0): ny = math.fmod(newCenter.y() - bounds.top(), bounds.height()) if ny < 0: x = bounds.height() + ny else: x = ny newCenter.setY(bounds.top() + x) object.setPosition(object.position() + (newCenter - objectCenter)) def canMergeWith(self, other): return other.isObjectGroup() def mergedWith(self, other): og = other merged = self.clone() for mapObject in og.objects(): merged.addObject(mapObject.clone()) return merged ## # Returns the color of the object group, or an invalid color if no color # is set. ## def color(self): return self.mColor ## # Sets the display color of the object group. ## def setColor(self, color): if type(color) != QColor: color = QColor(color) self.mColor = color ## # Returns the draw order for the objects in this group. # # \sa ObjectGroup.DrawOrder ## def drawOrder(self): return self.mDrawOrder ## # Sets the draw order for the objects in this group. # # \sa ObjectGroup.DrawOrder ## def setDrawOrder(self, drawOrder): self.mDrawOrder = drawOrder ## # Returns a duplicate of this ObjectGroup. # # \sa Layer.clone() ## def clone(self): return self.initializeClone( ObjectGroup(self.mName, self.mX, self.mY, self.mWidth, self.mHeight)) def initializeClone(self, clone): super().initializeClone(clone) for object in self.mObjects: clone.addObject(object.clone()) clone.setColor(self.mColor) clone.setDrawOrder(self.mDrawOrder) return clone