def intToEnum(self, metaEnum, intValue): valueMap = QMap() # dont show multiple enum values which have the same values values = QList() for i in range(metaEnum.keyCount()): value = metaEnum.value(i) if (not valueMap.contains(value)): valueMap[value] = True values.append(value) if (intValue >= values.count()): return -1 return values.at(intValue)
def intToEnum(self, metaEnum, intValue): valueMap = QMap( ) # dont show multiple enum values which have the same values values = QList() for i in range(metaEnum.keyCount()): value = metaEnum.value(i) if (not valueMap.contains(value)): valueMap[value] = True values.append(value) if (intValue >= values.count()): return -1 return values.at(intValue)
def intToFlag(self, metaEnum, intValue): valueMap = QMap() # dont show multiple enum values which have the same values values = QList() for i in range(metaEnum.keyCount()): value = metaEnum.value(i) if (not valueMap.contains(value) and self.isPowerOf2(value)): valueMap[value] = True values.append(value) flagValue = 0 temp = intValue i = 0 while (temp): if (i >= values.count()): return -1 if (temp & 1): flagValue |= values.at(i) i += 1 temp = temp >> 1 return flagValue
def intToFlag(self, metaEnum, intValue): valueMap = QMap( ) # dont show multiple enum values which have the same values values = QList() for i in range(metaEnum.keyCount()): value = metaEnum.value(i) if (not valueMap.contains(value) and self.isPowerOf2(value)): valueMap[value] = True values.append(value) flagValue = 0 temp = intValue i = 0 while (temp): if (i >= values.count()): return -1 if (temp & 1): flagValue |= values.at(i) i += 1 temp = temp >> 1 return flagValue
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 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 CommandDataModel(QAbstractTableModel): NameColumn, CommandColumn, EnabledColumn = range(3) ## # Constructs the object and parses the users settings to allow easy # programmatic access to the command list. ## def __init__(self, parent): super().__init__(parent) self.mSettings = QSettings() self.mSaveBeforeExecute = False self.mCommands = QList() # Load saveBeforeExecute option s = self.mSettings.value("saveBeforeExecute", True) self.mSaveBeforeExecute = bool(s) # Load command list variant = self.mSettings.value("commandList") commands = variant if commands is None: commands = [] for commandVariant in commands: self.mCommands.append(Command.fromQVariant(commandVariant)) # Add default commands the first time the app has booted up. # This is useful on it's own and helps demonstrate how to use the commands. addPrefStr = "addedDefaultCommands" addedCommands = self.mSettings.value(addPrefStr, False) if (not addedCommands): # Disable default commands by default so user gets an informative # warning when clicking the command button for the first time command = Command(False) if sys.platform == 'linux': command.command = "gedit %mapfile" elif sys.platform == 'darwin': command.command = "open -t %mapfile" if (not command.command.isEmpty()): command.name = self.tr("Open in text editor") self.mCommands.push_back(command) self.commit() self.mSettings.setValue(addPrefStr, True) ## # Saves the data to the users preferences. ## def commit(self): # Save saveBeforeExecute option self.mSettings.setValue("saveBeforeExecute", self.mSaveBeforeExecute) # Save command list commands = QList() for command in self.mCommands: commands.append(command.toQVariant()) self.mSettings.setValue("commandList", commands) ## # Returns whether saving before executing commands is enabled. ## def saveBeforeExecute(self): return self.mSaveBeforeExecute ## # Enables or disables saving before executing commands. ## def setSaveBeforeExecute(self, enabled): self.mSaveBeforeExecute = enabled ## # Returns the first enabled command in the list, or an empty # disabled command if there are no enabled commands. ## def firstEnabledCommand(self): for command in self.mCommands: if (command.isEnabled): return command return Command(False) ## # Returns a list of all the commands. ## def allCommands(self): return QList(self.mCommands) ## # Remove the given row or rows from the model. ## def removeRows(self, *args): l = len(args) if l > 1 and l < 4: row = args[0] count = args[1] if l == 2: parent = QModelIndex() elif l == 3: parent = args[2] if (row < 0 or row + count > self.mCommands.size()): return False self.beginRemoveRows(parent, row, row + count) self.mCommands.erase(self.mCommands.begin() + row, self.mCommands.begin() + row + count) self.endRemoveRows() return True elif l == 1: indices = args[0] ## # Deletes the commands associated with the given row <i>indices</i>. ## while (not indices.empty()): row = indices.takeFirst().row() if (row >= self.mCommands.size()): continue self.beginRemoveRows(QModelIndex(), row, row) self.mCommands.removeAt(row) # Decrement later indices since we removed a row for i in indices: if (i.row() > row): i = i.sibling(i.row() - 1, i.column()) self.endRemoveRows() ## # Returns the number of rows (this includes the <New Command> row). ## def rowCount(self, parent): if parent.isValid(): return 0 else: return self.mCommands.size() + 1 ## # Returns the number of columns. ## def columnCount(self, parent): if parent.isValid(): return 0 else: return 3 ## # Returns the data at <i>index</i> for the given <i>role</i>. ## def data(self, index, role=Qt.DisplayRole): isNormalRow = index.row() < self.mCommands.size() command = Command() if (isNormalRow): command = self.mCommands[index.row()] x = role if x == Qt.DisplayRole or x == Qt.EditRole: if (isNormalRow): if (index.column() == CommandDataModel.NameColumn): return command.name if (index.column() == CommandDataModel.CommandColumn): return command.command else: if (index.column() == CommandDataModel.NameColumn): if (role == Qt.EditRole): return QString() else: return self.tr("<new command>") elif x == Qt.ToolTipRole: if (isNormalRow): if (index.column() == CommandDataModel.NameColumn): return self.tr("Set a name for this command") if (index.column() == CommandDataModel.CommandColumn): return self.tr("Set the shell command to execute") if (index.column() == CommandDataModel.EnabledColumn): return self.tr( "Show or hide this command in the command list") else: if (index.column() == CommandDataModel.NameColumn): return self.tr("Add a new command") elif x == Qt.CheckStateRole: if (isNormalRow and index.column() == CommandDataModel.EnabledColumn): if command.isEnabled: _x = 2 else: _x = 0 return _x return QVariant() ## # Sets the data at <i>index</i> to the given <i>value</i>. # for the given <i>role</i> ## def setData(self, index, value, role): isNormalRow = index.row() < self.mCommands.size() isModified = False shouldAppend = False command = Command() if (isNormalRow): # Get the command as it exists already command = self.mCommands[index.row()] # Modify the command based on the passed date x = role if x == Qt.EditRole: text = value if text != '': if (index.column() == CommandDataModel.NameColumn): command.name = value isModified = True elif (index.column() == CommandDataModel.CommandColumn): command.command = value isModified = True elif x == Qt.CheckStateRole: if (index.column() == CommandDataModel.EnabledColumn): command.isEnabled = value > 0 isModified = True else: # If final row was edited, insert the new command if (role == Qt.EditRole and index.column() == CommandDataModel.NameColumn): command.name = value if (command.name != '' and command.name != self.tr("<new command>")): isModified = True shouldAppend = True if (isModified): # Write the modified command to our cache if (shouldAppend): self.mCommands.append(command) else: self.mCommands[index.row()] = command # Reset if there could be new rows or reordering, else emit dataChanged if (shouldAppend or index.column() == CommandDataModel.NameColumn): self.beginResetModel() self.endResetModel() else: self.dataChanged.emit(index, index) return isModified ## # Returns flags for the item at <i>index</i>. ## def flags(self, index): isNormalRow = index.row() < self.mCommands.size() f = super().flags(index) if (isNormalRow): f |= Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled if (index.column() == CommandDataModel.EnabledColumn): f |= Qt.ItemIsUserCheckable else: f |= Qt.ItemIsEditable else: f |= Qt.ItemIsDropEnabled if (index.column() == CommandDataModel.NameColumn): f |= Qt.ItemIsEditable return f ## # Returns the header data for the given <i>section</i> and <i>role</i>. # <i>orientation</i> should be Qt.Horizontal. ## def headerData(self, section, orientation, role=Qt.EditRole): if (role != Qt.DisplayRole or orientation != Qt.Horizontal): return QVariant() sectionLabels = ["Name", "Command", "Enable"] return self.tr(sectionLabels[section]) ## # Returns a menu containing a list of appropriate actions for the item at # <i>index</i>, or 0 if there are no actions for the index. ## def contextMenu(self, parent, index): menu = None row = index.row() if (row >= 0 and row < self.mCommands.size()): menu = QMenu(parent) if (row > 0): action = menu.addAction(self.tr("Move Up")) mapper = QSignalMapper(action) mapper.setMapping(action, row) action.triggered.connect(mapper.map) mapper.mapped.connect(self.moveUp) if (row + 1 < self.mCommands.size()): action = menu.addAction(self.tr("Move Down")) mapper = QSignalMapper(action) mapper.setMapping(action, row + 1) action.triggered.connect(mapper.map) mapper.mapped.connect(self.moveUp) menu.addSeparator() action = menu.addAction(self.tr("Execute")) mapper = QSignalMapper(action) mapper.setMapping(action, row) action.triggered.connect(mapper.map) mapper.mapped.connect(self.execute) if sys.platform in ['linux', 'darwin']: action = menu.addAction(self.tr("Execute in Terminal")) mapper = QSignalMapper(action) mapper.setMapping(action, row) action.triggered.connect(mapper.map) mapper.mapped.connect(self.executeInTerminal) menu.addSeparator() action = menu.addAction(self.tr("Delete")) mapper = QSignalMapper(action) mapper.setMapping(action, row) action.triggered.connect(mapper.map) mapper.mapped.connect(self.remove) return menu ## # Returns mime data for the first index in <i>indexes</i>. ## def mimeData(self, indices): row = -1 for index in indices: # Only generate mime data on command rows if (index.row() < 0 or index.row() >= self.mCommands.size()): return None # Currently only one row at a time is supported for drags # Note: we can get multiple indexes in the same row (different columns) if (row != -1 and index.row() != row): return None row = index.row() command = self.mCommands[row] mimeData = QMimeData() # Text data is used if command is dragged to a text editor or terminal mimeData.setText(command.finalCommand()) # Ptr is used if command is dragged onto another command # We could store the index instead, the only difference would be that if # the item is moved or deleted shomehow during the drag, the ptr approach # will result in a no-op instead of moving the wrong thing. addr = command mimeData.setData(commandMimeType, QByteArray(addr, 4)) return mimeData ## # Returns a list of mime types that can represent a command. ## def mimeTypes(self): result = QStringList("text/plain") result.append(commandMimeType) return result ## # Returns the drop actions that can be performed. ## def supportedDropActions(self): return Qt.CopyAction | Qt.MoveAction ## # Handles dropping of mime data onto <i>parent</i>. ## def dropMimeData(self, data, action, row, column, parent): if (not parent.isValid()): return False dstRow = parent.row() if (data.hasFormat(commandMimeType)): # Get the ptr to the command that was being dragged byteData = data.data(commandMimeType) addr = byteData.data() # Find the command in the command list so we can move/copy it for srcRow in range(self.mCommands.size()): if (addr == self.mCommands[srcRow]): # If a command is dropped on another command, # move the src command into the positon of the dst command. if (dstRow < self.mCommands.size()): return self.move(srcRow, dstRow) # If a command is dropped elsewhere, create a copy of it if (dstRow == self.mCommands.size()): self.append( Command(addr.isEnabled, self.tr("%s (copy)" % addr.name), addr.command)) return True if (data.hasText()): # If text is dropped on a valid command, just replace the data if (dstRow < self.mCommands.size()): return self.setData(parent, data.text(), Qt.EditRole) # If text is dropped elsewhere, create a new command # Assume the dropped text is the command, not the name if (dstRow == self.mCommands.size()): self.append(Command(True, self.tr("New command"), data.text())) return True return False ## # Moves the command at <i>commandIndex</i> to <i>newIndex></i>. ## def move(self, commandIndex, newIndex): commandIndex = self.mCommands.size() newIndex = self.mCommands.size() if (commandIndex or newIndex or newIndex == commandIndex): return False tmp = newIndex if newIndex > commandIndex: tmp += 1 if (not self.beginMoveRows(QModelIndex(), commandIndex, commandIndex, QModelIndex(), tmp)): return False if (commandIndex - newIndex == 1 or newIndex - commandIndex == 1): # Swapping is probably more efficient than removing/inserting self.mCommands.swap(commandIndex, newIndex) else: command = self.mCommands.at(commandIndex) self.mCommands.removeAt(commandIndex) self.mCommands.insert(newIndex, command) self.endMoveRows() return True ## # Appends <i>command</i> to the command list. ## def append(self, command): self.beginInsertRows(QModelIndex(), self.mCommands.size(), self.mCommands.size()) self.mCommands.append(command) self.endInsertRows() ## # Moves the command at <i>commandIndex</i> up one index, if possible. ## def moveUp(self, commandIndex): self.move(commandIndex, commandIndex - 1) ## # Executes the command at<i>commandIndex</i>. ## def execute(self, commandIndex): self.mCommands.at(commandIndex).execute() ## # Executes the command at <i>commandIndex</i> within the systems native # terminal if available. ## def executeInTerminal(self, commandIndex): self.mCommands.at(commandIndex).execute(True) ## # Deletes the command at <i>commandIndex</i>. ## def remove(self, commandIndex): self.removeRow(commandIndex)
class MyController(QDialog): def __init__(self, parent=None): super(MyController, self).__init__(parent) self.theClassNames = QList() self.theClassCombo = QComboBox(self) self.theControlledObject = None button = QToolButton(self) self.theController = ObjectController(self) buttonBox = QDialogButtonBox(self) button.clicked.connect(self.createAndControl) buttonBox.rejected.connect(self.reject) button.setText(self.tr("Create And Control")) buttonBox.setStandardButtons(QDialogButtonBox.Close) layout = QVBoxLayout(self) internalLayout = QHBoxLayout() internalLayout.addWidget(self.theClassCombo) internalLayout.addWidget(button) layout.addLayout(internalLayout) layout.addWidget(self.theController) layout.addWidget(buttonBox) self.theClassNames.append("QWidget") self.theClassNames.append("QPushButton") self.theClassNames.append("QDialogButtonBox") self.theClassNames.append("QTreeWidget") self.theClassNames.append("QCalendarWidget") self.theClassNames.append("QAction") self.theClassNames.append("QTimeLine") self.theClassNames.append("QTextDocument") self.theClassCombo.addItems(self.theClassNames) def __del__(self): if (self.theControlledObject): del self.theControlledObject def createAndControl(self): newObject = 0 className = self.theClassNames.at(self.theClassCombo.currentIndex()) if (className == "QWidget"): newObject = QWidget() elif (className == "QPushButton"): newObject = QPushButton() elif (className == "QDialogButtonBox"): newObject = QDialogButtonBox() elif (className == "QTreeWidget"): newObject = QTreeWidget() elif (className == "QCalendarWidget"): newObject = QCalendarWidget() elif (className == "QAction"): newObject = QAction(None) elif (className == "QTimeLine"): newObject = QTimeLine() elif (className == "QTextDocument"): newObject = QTextDocument() if (not newObject): return newWidget = newObject if hasattr(newWidget, 'geometry'): r = newWidget.geometry() r.setSize(newWidget.sizeHint()) r.setWidth(max(r.width(), 150)) r.setHeight(max(r.height(), 50)) r.moveCenter(QApplication.desktop().geometry().center()) newWidget.setGeometry(r) newWidget.setWindowTitle( self.tr("Controlled Object: %s" % className)) newWidget.show() if (self.theControlledObject): del self.theControlledObject self.theControlledObject = newObject self.theController.setObject(self.theControlledObject)
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 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 CommandDataModel(QAbstractTableModel): NameColumn, CommandColumn, EnabledColumn = range(3) ## # Constructs the object and parses the users settings to allow easy # programmatic access to the command list. ## def __init__(self, parent): super().__init__(parent) self.mSettings = QSettings() self.mSaveBeforeExecute = False self.mCommands = QList() # Load saveBeforeExecute option s = self.mSettings.value("saveBeforeExecute", True) self.mSaveBeforeExecute = bool(s) # Load command list variant = self.mSettings.value("commandList") commands = variant if commands is None: commands = [] for commandVariant in commands: self.mCommands.append(Command.fromQVariant(commandVariant)) # Add default commands the first time the app has booted up. # This is useful on it's own and helps demonstrate how to use the commands. addPrefStr = "addedDefaultCommands" addedCommands = self.mSettings.value(addPrefStr, False) if (not addedCommands): # Disable default commands by default so user gets an informative # warning when clicking the command button for the first time command = Command(False) if sys.platform == 'linux': command.command = "gedit %mapfile" elif sys.platform == 'darwin': command.command = "open -t %mapfile" if (not command.command.isEmpty()): command.name = self.tr("Open in text editor") self.mCommands.push_back(command) self.commit() self.mSettings.setValue(addPrefStr, True) ## # Saves the data to the users preferences. ## def commit(self): # Save saveBeforeExecute option self.mSettings.setValue("saveBeforeExecute", self.mSaveBeforeExecute) # Save command list commands = QList() for command in self.mCommands: commands.append(command.toQVariant()) self.mSettings.setValue("commandList", commands) ## # Returns whether saving before executing commands is enabled. ## def saveBeforeExecute(self): return self.mSaveBeforeExecute ## # Enables or disables saving before executing commands. ## def setSaveBeforeExecute(self, enabled): self.mSaveBeforeExecute = enabled ## # Returns the first enabled command in the list, or an empty # disabled command if there are no enabled commands. ## def firstEnabledCommand(self): for command in self.mCommands: if (command.isEnabled): return command return Command(False) ## # Returns a list of all the commands. ## def allCommands(self): return QList(self.mCommands) ## # Remove the given row or rows from the model. ## def removeRows(self, *args): l = len(args) if l>1 and l<4: row = args[0] count = args[1] if l==2: parent = QModelIndex() elif l==3: parent = args[2] if (row < 0 or row + count > self.mCommands.size()): return False self.beginRemoveRows(parent, row, row + count) self.mCommands.erase(self.mCommands.begin() + row, self.mCommands.begin() + row + count) self.endRemoveRows() return True elif l==1: indices = args[0] ## # Deletes the commands associated with the given row <i>indices</i>. ## while (not indices.empty()): row = indices.takeFirst().row() if (row >= self.mCommands.size()): continue self.beginRemoveRows(QModelIndex(), row, row) self.mCommands.removeAt(row) # Decrement later indices since we removed a row for i in indices: if (i.row() > row): i = i.sibling(i.row() - 1, i.column()) self.endRemoveRows() ## # Returns the number of rows (this includes the <New Command> row). ## def rowCount(self, parent): if parent.isValid(): return 0 else: return self.mCommands.size() + 1 ## # Returns the number of columns. ## def columnCount(self, parent): if parent.isValid(): return 0 else: return 3 ## # Returns the data at <i>index</i> for the given <i>role</i>. ## def data(self, index, role = Qt.DisplayRole): isNormalRow = index.row() < self.mCommands.size() command = Command() if (isNormalRow): command = self.mCommands[index.row()] x = role if x==Qt.DisplayRole or x==Qt.EditRole: if (isNormalRow): if (index.column() == CommandDataModel.NameColumn): return command.name if (index.column() == CommandDataModel.CommandColumn): return command.command else: if (index.column() == CommandDataModel.NameColumn): if (role == Qt.EditRole): return QString() else: return self.tr("<new command>") elif x==Qt.ToolTipRole: if (isNormalRow): if (index.column() == CommandDataModel.NameColumn): return self.tr("Set a name for this command") if (index.column() == CommandDataModel.CommandColumn): return self.tr("Set the shell command to execute") if (index.column() == CommandDataModel.EnabledColumn): return self.tr("Show or hide this command in the command list") else: if (index.column() == CommandDataModel.NameColumn): return self.tr("Add a new command") elif x==Qt.CheckStateRole: if (isNormalRow and index.column() == CommandDataModel.EnabledColumn): if command.isEnabled: _x = 2 else: _x = 0 return _x return QVariant() ## # Sets the data at <i>index</i> to the given <i>value</i>. # for the given <i>role</i> ## def setData(self, index, value, role): isNormalRow = index.row() < self.mCommands.size() isModified = False shouldAppend = False command = Command() if (isNormalRow): # Get the command as it exists already command = self.mCommands[index.row()] # Modify the command based on the passed date x = role if x==Qt.EditRole: text = value if text != '': if (index.column() == CommandDataModel.NameColumn): command.name = value isModified = True elif (index.column() == CommandDataModel.CommandColumn): command.command = value isModified = True elif x==Qt.CheckStateRole: if (index.column() == CommandDataModel.EnabledColumn): command.isEnabled = value > 0 isModified = True else: # If final row was edited, insert the new command if (role == Qt.EditRole and index.column() == CommandDataModel.NameColumn): command.name = value if (command.name!='' and command.name!=self.tr("<new command>")): isModified = True shouldAppend = True if (isModified): # Write the modified command to our cache if (shouldAppend): self.mCommands.append(command) else: self.mCommands[index.row()] = command # Reset if there could be new rows or reordering, else emit dataChanged if (shouldAppend or index.column() == CommandDataModel.NameColumn): self.beginResetModel() self.endResetModel() else: self.dataChanged.emit(index, index) return isModified ## # Returns flags for the item at <i>index</i>. ## def flags(self, index): isNormalRow = index.row() < self.mCommands.size() f = super().flags(index) if (isNormalRow): f |= Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled if (index.column() == CommandDataModel.EnabledColumn): f |= Qt.ItemIsUserCheckable else: f |= Qt.ItemIsEditable else: f |= Qt.ItemIsDropEnabled if (index.column() == CommandDataModel.NameColumn): f |= Qt.ItemIsEditable return f ## # Returns the header data for the given <i>section</i> and <i>role</i>. # <i>orientation</i> should be Qt.Horizontal. ## def headerData(self, section, orientation, role = Qt.EditRole): if (role != Qt.DisplayRole or orientation != Qt.Horizontal): return QVariant() sectionLabels = ["Name", "Command", "Enable"] return self.tr(sectionLabels[section]) ## # Returns a menu containing a list of appropriate actions for the item at # <i>index</i>, or 0 if there are no actions for the index. ## def contextMenu(self, parent, index): menu = None row = index.row() if (row >= 0 and row < self.mCommands.size()): menu = QMenu(parent) if (row > 0): action = menu.addAction(self.tr("Move Up")) mapper = QSignalMapper(action) mapper.setMapping(action, row) action.triggered.connect(mapper.map) mapper.mapped.connect(self.moveUp) if (row+1 < self.mCommands.size()): action = menu.addAction(self.tr("Move Down")) mapper = QSignalMapper(action) mapper.setMapping(action, row + 1) action.triggered.connect(mapper.map) mapper.mapped.connect(self.moveUp) menu.addSeparator() action = menu.addAction(self.tr("Execute")) mapper = QSignalMapper(action) mapper.setMapping(action, row) action.triggered.connect(mapper.map) mapper.mapped.connect(self.execute) if sys.platform in ['linux', 'darwin']: action = menu.addAction(self.tr("Execute in Terminal")) mapper = QSignalMapper(action) mapper.setMapping(action, row) action.triggered.connect(mapper.map) mapper.mapped.connect(self.executeInTerminal) menu.addSeparator() action = menu.addAction(self.tr("Delete")) mapper = QSignalMapper(action) mapper.setMapping(action, row) action.triggered.connect(mapper.map) mapper.mapped.connect(self.remove) return menu ## # Returns mime data for the first index in <i>indexes</i>. ## def mimeData(self, indices): row = -1 for index in indices: # Only generate mime data on command rows if (index.row() < 0 or index.row() >= self.mCommands.size()): return None # Currently only one row at a time is supported for drags # Note: we can get multiple indexes in the same row (different columns) if (row != -1 and index.row() != row): return None row = index.row() command = self.mCommands[row] mimeData = QMimeData() # Text data is used if command is dragged to a text editor or terminal mimeData.setText(command.finalCommand()) # Ptr is used if command is dragged onto another command # We could store the index instead, the only difference would be that if # the item is moved or deleted shomehow during the drag, the ptr approach # will result in a no-op instead of moving the wrong thing. addr = command mimeData.setData(commandMimeType, QByteArray(addr, 4)) return mimeData ## # Returns a list of mime types that can represent a command. ## def mimeTypes(self): result = QStringList("text/plain") result.append(commandMimeType) return result ## # Returns the drop actions that can be performed. ## def supportedDropActions(self): return Qt.CopyAction | Qt.MoveAction ## # Handles dropping of mime data onto <i>parent</i>. ## def dropMimeData(self, data, action, row, column, parent): if (not parent.isValid()): return False dstRow = parent.row() if (data.hasFormat(commandMimeType)): # Get the ptr to the command that was being dragged byteData = data.data(commandMimeType) addr = byteData.data() # Find the command in the command list so we can move/copy it for srcRow in range(self.mCommands.size()): if (addr == self.mCommands[srcRow]): # If a command is dropped on another command, # move the src command into the positon of the dst command. if (dstRow < self.mCommands.size()): return self.move(srcRow, dstRow) # If a command is dropped elsewhere, create a copy of it if (dstRow == self.mCommands.size()): self.append(Command(addr.isEnabled, self.tr("%s (copy)"%addr.name), addr.command)) return True if (data.hasText()): # If text is dropped on a valid command, just replace the data if (dstRow < self.mCommands.size()): return self.setData(parent, data.text(), Qt.EditRole) # If text is dropped elsewhere, create a new command # Assume the dropped text is the command, not the name if (dstRow == self.mCommands.size()): self.append(Command(True, self.tr("New command"), data.text())) return True return False ## # Moves the command at <i>commandIndex</i> to <i>newIndex></i>. ## def move(self, commandIndex, newIndex): commandIndex = self.mCommands.size() newIndex = self.mCommands.size() if (commandIndex or newIndex or newIndex == commandIndex): return False tmp = newIndex if newIndex > commandIndex: tmp += 1 if (not self.beginMoveRows(QModelIndex(), commandIndex, commandIndex, QModelIndex(), tmp)): return False if (commandIndex - newIndex == 1 or newIndex - commandIndex == 1): # Swapping is probably more efficient than removing/inserting self.mCommands.swap(commandIndex, newIndex) else: command = self.mCommands.at(commandIndex) self.mCommands.removeAt(commandIndex) self.mCommands.insert(newIndex, command) self.endMoveRows() return True ## # Appends <i>command</i> to the command list. ## def append(self, command): self.beginInsertRows(QModelIndex(), self.mCommands.size(), self.mCommands.size()) self.mCommands.append(command) self.endInsertRows() ## # Moves the command at <i>commandIndex</i> up one index, if possible. ## def moveUp(self, commandIndex): self.move(commandIndex, commandIndex - 1) ## # Executes the command at<i>commandIndex</i>. ## def execute(self, commandIndex): self.mCommands.at(commandIndex).execute() ## # Executes the command at <i>commandIndex</i> within the systems native # terminal if available. ## def executeInTerminal(self, commandIndex): self.mCommands.at(commandIndex).execute(True) ## # Deletes the command at <i>commandIndex</i>. ## def remove(self, commandIndex): self.removeRow(commandIndex)
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 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 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 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()
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
class DocumentManager(QObject): ## # Emitted when the current displayed map document changed. ## currentDocumentChanged = pyqtSignal(list) ## # Emitted when the user requested the document at \a index to be closed. ## documentCloseRequested = pyqtSignal(int) ## # Emitted when a document is about to be closed. ## documentAboutToClose = pyqtSignal(MapDocument) ## # Emitted when an error occurred while reloading the map. ## reloadError = pyqtSignal(str) mInstance = None def __init__(self, parent=None): super().__init__(parent) self.mDocuments = QList() self.mTabWidget = MovableTabWidget() self.mUndoGroup = QUndoGroup(self) self.mSelectedTool = None self.mViewWithTool = None self.mFileSystemWatcher = FileSystemWatcher(self) self.mTabWidget.setDocumentMode(True) self.mTabWidget.setTabsClosable(True) self.mTabWidget.currentChanged.connect(self.currentIndexChanged) self.mTabWidget.tabCloseRequested.connect(self.documentCloseRequested) self.mTabWidget.tabMoved.connect(self.documentTabMoved) self.mFileSystemWatcher.fileChanged.connect(self.fileChanged) def __del__(self): # All documents should be closed gracefully beforehand del self.mTabWidget def instance(): if not DocumentManager.mInstance: DocumentManager.mInstance = DocumentManager() return DocumentManager.mInstance def deleteInstance(): del DocumentManager.mInstance DocumentManager.mInstance = None ## # Returns the document manager widget. It contains the different map views # and a tab bar to switch between them. ## def widget(self): return self.mTabWidget ## # Returns the undo group that combines the undo stacks of all opened map # documents. # # @see MapDocument.undoStack() ## def undoGroup(self): return self.mUndoGroup ## # Returns the current map document, or 0 when there is none. ## def currentDocument(self): index = self.mTabWidget.currentIndex() if (index == -1): return None return self.mDocuments.at(index) ## # Returns the map view of the current document, or 0 when there is none. ## def currentMapView(self): widget = self.mTabWidget.currentWidget() if widget: return widget.mapView() return None ## # Returns the map scene of the current document, or 0 when there is none. ## def currentMapScene(self): mapView = self.currentMapView() if mapView: return mapView.mapScene() return None ## # Returns the map view that displays the given document, or 0 when there # is none. ## def viewForDocument(self, mapDocument): index = self.mDocuments.indexOf(mapDocument) if (index == -1): return None return self.mTabWidget.widget(index).mapView() ## # Returns the number of map documents. ## def documentCount(self): return self.mDocuments.size() ## # Searches for a document with the given \a fileName and returns its # index. Returns -1 when the document isn't open. ## def findDocument(self, fileName): canonicalFilePath = QFileInfo(fileName).canonicalFilePath() if (canonicalFilePath == ''): # file doesn't exist return -1 for i in range(self.mDocuments.size()): fileInfo = QFileInfo(self.mDocuments.at(i).fileName()) if (fileInfo.canonicalFilePath() == canonicalFilePath): return i return -1 ## # Switches to the map document at the given \a index. ## def switchToDocument(self, arg): tp = type(arg) if tp == int: index = arg self.mTabWidget.setCurrentIndex(index) elif tp == MapDocument: mapDocument = arg index = self.mDocuments.indexOf(mapDocument) if (index != -1): self.switchToDocument(index) ## # Adds the new or opened \a mapDocument to the document manager. ## def addDocument(self, mapDocument): self.mDocuments.append(mapDocument) self.mUndoGroup.addStack(mapDocument.undoStack()) if (mapDocument.fileName() != ''): self.mFileSystemWatcher.addPath(mapDocument.fileName()) view = MapView() scene = MapScene(view) # scene is owned by the view container = MapViewContainer(view, self.mTabWidget) scene.setMapDocument(mapDocument) view.setScene(scene) documentIndex = self.mDocuments.size() - 1 self.mTabWidget.addTab(container, mapDocument.displayName()) self.mTabWidget.setTabToolTip(documentIndex, mapDocument.fileName()) mapDocument.fileNameChanged.connect(self.fileNameChanged) mapDocument.modifiedChanged.connect(self.updateDocumentTab) mapDocument.saved.connect(self.documentSaved) container.reload.connect(self.reloadRequested) self.switchToDocument(documentIndex) self.centerViewOn(0, 0) ## # Closes the current map document. Will not ask the user whether to save # any changes! ## def closeCurrentDocument(self): index = self.mTabWidget.currentIndex() if (index == -1): return self.closeDocumentAt(index) ## # Closes the document at the given \a index. Will not ask the user whether # to save any changes! ## def closeDocumentAt(self, index): mapDocument = self.mDocuments.at(index) self.documentAboutToClose.emit(mapDocument) self.mDocuments.removeAt(index) self.mTabWidget.removeTab(index) if (mapDocument.fileName() != ''): self.mFileSystemWatcher.removePath(mapDocument.fileName()) self.mUndoGroup.removeStack(mapDocument.undoStack()) ## # Reloads the current document. Will not ask the user whether to save any # changes! # # \sa reloadDocumentAt() ## def reloadCurrentDocument(self): index = self.mTabWidget.currentIndex() if (index == -1): return False return self.reloadDocumentAt(index) ## # Reloads the document at the given \a index. It will lose any undo # history and current selections. Will not ask the user whether to save # any changes! # # Returns whether the map loaded successfully. ## def reloadDocumentAt(self, index): oldDocument = self.mDocuments.at(index) newDocument, error = MapDocument.load(oldDocument.fileName(), oldDocument.readerFormat()) if (not newDocument): self.reloadError.emit( self.tr("%s:\n\n%s" % (oldDocument.fileName(), error))) return False # Remember current view state mapView = self.viewForDocument(oldDocument) layerIndex = oldDocument.currentLayerIndex() scale = mapView.zoomable().scale() horizontalPosition = mapView.horizontalScrollBar().sliderPosition() verticalPosition = mapView.verticalScrollBar().sliderPosition() # Replace old tab self.addDocument(newDocument) self.closeDocumentAt(index) self.mTabWidget.moveTab(self.mDocuments.size() - 1, index) # Restore previous view state mapView = self.currentMapView() mapView.zoomable().setScale(scale) mapView.horizontalScrollBar().setSliderPosition(horizontalPosition) mapView.verticalScrollBar().setSliderPosition(verticalPosition) if (layerIndex > 0 and layerIndex < newDocument.map().layerCount()): newDocument.setCurrentLayerIndex(layerIndex) return True ## # Close all documents. Will not ask the user whether to save any changes! ## def closeAllDocuments(self): while (not self.mDocuments.isEmpty()): self.closeCurrentDocument() ## # Returns all open map documents. ## def documents(self): return self.mDocuments ## # Centers the current map on the tile coordinates \a x, \a y. ## def centerViewOn(self, *args): l = len(args) if l == 2: x, y = args view = self.currentMapView() if (not view): return view.centerOn( self.currentDocument().renderer().pixelToScreenCoords(x, y)) elif l == 1: pos = args[0] self.centerViewOn(pos.x(), pos.y()) def switchToLeftDocument(self): tabCount = self.mTabWidget.count() if (tabCount < 2): return currentIndex = self.mTabWidget.currentIndex() if currentIndex > 0: x = currentIndex else: x = tabCount self.switchToDocument(x - 1) def switchToRightDocument(self): tabCount = self.mTabWidget.count() if (tabCount < 2): return currentIndex = self.mTabWidget.currentIndex() self.switchToDocument((currentIndex + 1) % tabCount) def setSelectedTool(self, tool): if type(tool) == list: tool = tool[0] if (self.mSelectedTool == tool): return if self.mSelectedTool: self.mSelectedTool.cursorChanged.disconnect(self.cursorChanged) self.mSelectedTool = tool if self.mViewWithTool: mapScene = self.mViewWithTool.mapScene() mapScene.disableSelectedTool() if tool: mapScene.setSelectedTool(tool) mapScene.enableSelectedTool() if tool: self.mViewWithTool.viewport().setCursor(tool.cursor) else: self.mViewWithTool.viewport().unsetCursor() if tool: tool.cursorChanged.connect(self.cursorChanged) def currentIndexChanged(self): if self.mViewWithTool: mapScene = self.mViewWithTool.mapScene() mapScene.disableSelectedTool() self.mViewWithTool = None mapDocument = self.currentDocument() if (mapDocument): self.mUndoGroup.setActiveStack(mapDocument.undoStack()) self.currentDocumentChanged.emit([mapDocument]) mapView = self.currentMapView() if mapView: mapScene = mapView.mapScene() mapScene.setSelectedTool(self.mSelectedTool) mapScene.enableSelectedTool() if self.mSelectedTool: mapView.viewport().setCursor(self.mSelectedTool.cursor) else: mapView.viewport().unsetCursor() self.mViewWithTool = mapView def fileNameChanged(self, fileName, oldFileName): if fileName != '': self.mFileSystemWatcher.addPath(fileName) if oldFileName != '': self.mFileSystemWatcher.removePath(oldFileName) self.updateDocumentTab() def updateDocumentTab(self): mapDocument = self.sender() index = self.mDocuments.indexOf(mapDocument) tabText = mapDocument.displayName() if (mapDocument.isModified()): tabText = '*' + tabText self.mTabWidget.setTabText(index, tabText) self.mTabWidget.setTabToolTip(index, mapDocument.fileName()) def documentSaved(self): document = self.sender() index = self.mDocuments.indexOf(document) widget = self.mTabWidget.widget(index) container = widget container.setFileChangedWarningVisible(False) def documentTabMoved(self, _from, to): self.mDocuments.move(_from, to) def fileChanged(self, fileName): index = self.findDocument(fileName) # Most likely the file was removed if (index == -1): return document = self.mDocuments.at(index) # Ignore change event when it seems to be our own save if (QFileInfo(fileName).lastModified() == document.lastSaved()): return # Automatically reload when there are no unsaved changes if (not document.isModified()): self.reloadDocumentAt(index) return widget = self.mTabWidget.widget(index) container = widget container.setFileChangedWarningVisible(True) def cursorChanged(self, cursor): if self.mViewWithTool: self.mViewWithTool.viewport().setCursor(cursor) def reloadRequested(self): index = self.mTabWidget.indexOf(self.sender()) self.reloadDocumentAt(index)
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
class MyController(QDialog): def __init__(self, parent=None): super(MyController, self).__init__(parent) self.theClassNames = QList() self.theClassCombo = QComboBox(self) self.theControlledObject = None button = QToolButton(self) self.theController = ObjectController(self) buttonBox = QDialogButtonBox(self) button.clicked.connect(self.createAndControl) buttonBox.rejected.connect(self.reject) button.setText(self.tr("Create And Control")) buttonBox.setStandardButtons(QDialogButtonBox.Close) layout = QVBoxLayout(self) internalLayout = QHBoxLayout() internalLayout.addWidget(self.theClassCombo) internalLayout.addWidget(button) layout.addLayout(internalLayout) layout.addWidget(self.theController) layout.addWidget(buttonBox) self.theClassNames.append("QWidget") self.theClassNames.append("QPushButton") self.theClassNames.append("QDialogButtonBox") self.theClassNames.append("QTreeWidget") self.theClassNames.append("QCalendarWidget") self.theClassNames.append("QAction") self.theClassNames.append("QTimeLine") self.theClassNames.append("QTextDocument") self.theClassCombo.addItems(self.theClassNames) def __del__(self): if (self.theControlledObject): del self.theControlledObject def createAndControl(self): newObject = 0 className = self.theClassNames.at(self.theClassCombo.currentIndex()) if (className == "QWidget"): newObject = QWidget() elif (className == "QPushButton"): newObject = QPushButton() elif (className == "QDialogButtonBox"): newObject = QDialogButtonBox() elif (className == "QTreeWidget"): newObject = QTreeWidget() elif (className == "QCalendarWidget"): newObject = QCalendarWidget() elif (className == "QAction"): newObject = QAction(None) elif (className == "QTimeLine"): newObject = QTimeLine() elif (className == "QTextDocument"): newObject = QTextDocument() if (not newObject): return newWidget = newObject if hasattr(newWidget, 'geometry'): r = newWidget.geometry() r.setSize(newWidget.sizeHint()) r.setWidth(max(r.width(), 150)) r.setHeight(max(r.height(), 50)) r.moveCenter(QApplication.desktop().geometry().center()) newWidget.setGeometry(r) newWidget.setWindowTitle(self.tr("Controlled Object: %s"%className)) newWidget.show() if (self.theControlledObject): del self.theControlledObject self.theControlledObject = newObject self.theController.setObject(self.theControlledObject)
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 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 DocumentManager(QObject): ## # Emitted when the current displayed map document changed. ## currentDocumentChanged = pyqtSignal(list) ## # Emitted when the user requested the document at \a index to be closed. ## documentCloseRequested = pyqtSignal(int) ## # Emitted when a document is about to be closed. ## documentAboutToClose = pyqtSignal(MapDocument) ## # Emitted when an error occurred while reloading the map. ## reloadError = pyqtSignal(str) mInstance = None def __init__(self, parent = None): super().__init__(parent) self.mDocuments = QList() self.mTabWidget = MovableTabWidget() self.mUndoGroup = QUndoGroup(self) self.mSelectedTool = None self.mViewWithTool = None self.mFileSystemWatcher = FileSystemWatcher(self) self.mTabWidget.setDocumentMode(True) self.mTabWidget.setTabsClosable(True) self.mTabWidget.currentChanged.connect(self.currentIndexChanged) self.mTabWidget.tabCloseRequested.connect(self.documentCloseRequested) self.mTabWidget.tabMoved.connect(self.documentTabMoved) self.mFileSystemWatcher.fileChanged.connect(self.fileChanged) def __del__(self): # All documents should be closed gracefully beforehand del self.mTabWidget def instance(): if not DocumentManager.mInstance: DocumentManager.mInstance = DocumentManager() return DocumentManager.mInstance def deleteInstance(): del DocumentManager.mInstance DocumentManager.mInstance = None ## # Returns the document manager widget. It contains the different map views # and a tab bar to switch between them. ## def widget(self): return self.mTabWidget ## # Returns the undo group that combines the undo stacks of all opened map # documents. # # @see MapDocument.undoStack() ## def undoGroup(self): return self.mUndoGroup ## # Returns the current map document, or 0 when there is none. ## def currentDocument(self): index = self.mTabWidget.currentIndex() if (index == -1): return None return self.mDocuments.at(index) ## # Returns the map view of the current document, or 0 when there is none. ## def currentMapView(self): widget = self.mTabWidget.currentWidget() if widget: return widget.mapView() return None ## # Returns the map scene of the current document, or 0 when there is none. ## def currentMapScene(self): mapView = self.currentMapView() if mapView: return mapView.mapScene() return None ## # Returns the map view that displays the given document, or 0 when there # is none. ## def viewForDocument(self, mapDocument): index = self.mDocuments.indexOf(mapDocument) if (index == -1): return None return self.mTabWidget.widget(index).mapView() ## # Returns the number of map documents. ## def documentCount(self): return self.mDocuments.size() ## # Searches for a document with the given \a fileName and returns its # index. Returns -1 when the document isn't open. ## def findDocument(self, fileName): canonicalFilePath = QFileInfo(fileName).canonicalFilePath() if (canonicalFilePath==''): # file doesn't exist return -1 for i in range(self.mDocuments.size()): fileInfo = QFileInfo(self.mDocuments.at(i).fileName()) if (fileInfo.canonicalFilePath() == canonicalFilePath): return i return -1 ## # Switches to the map document at the given \a index. ## def switchToDocument(self, arg): tp = type(arg) if tp==int: index = arg self.mTabWidget.setCurrentIndex(index) elif tp==MapDocument: mapDocument = arg index = self.mDocuments.indexOf(mapDocument) if (index != -1): self.switchToDocument(index) ## # Adds the new or opened \a mapDocument to the document manager. ## def addDocument(self, mapDocument): self.mDocuments.append(mapDocument) self.mUndoGroup.addStack(mapDocument.undoStack()) if (mapDocument.fileName()!=''): self.mFileSystemWatcher.addPath(mapDocument.fileName()) view = MapView() scene = MapScene(view) # scene is owned by the view container = MapViewContainer(view, self.mTabWidget) scene.setMapDocument(mapDocument) view.setScene(scene) documentIndex = self.mDocuments.size() - 1 self.mTabWidget.addTab(container, mapDocument.displayName()) self.mTabWidget.setTabToolTip(documentIndex, mapDocument.fileName()) mapDocument.fileNameChanged.connect(self.fileNameChanged) mapDocument.modifiedChanged.connect(self.updateDocumentTab) mapDocument.saved.connect(self.documentSaved) container.reload.connect(self.reloadRequested) self.switchToDocument(documentIndex) self.centerViewOn(0, 0) ## # Closes the current map document. Will not ask the user whether to save # any changes! ## def closeCurrentDocument(self): index = self.mTabWidget.currentIndex() if (index == -1): return self.closeDocumentAt(index) ## # Closes the document at the given \a index. Will not ask the user whether # to save any changes! ## def closeDocumentAt(self, index): mapDocument = self.mDocuments.at(index) self.documentAboutToClose.emit(mapDocument) self.mDocuments.removeAt(index) self.mTabWidget.removeTab(index) if (mapDocument.fileName() != ''): self.mFileSystemWatcher.removePath(mapDocument.fileName()) self.mUndoGroup.removeStack(mapDocument.undoStack()) ## # Reloads the current document. Will not ask the user whether to save any # changes! # # \sa reloadDocumentAt() ## def reloadCurrentDocument(self): index = self.mTabWidget.currentIndex() if (index == -1): return False return self.reloadDocumentAt(index) ## # Reloads the document at the given \a index. It will lose any undo # history and current selections. Will not ask the user whether to save # any changes! # # Returns whether the map loaded successfully. ## def reloadDocumentAt(self, index): oldDocument = self.mDocuments.at(index) newDocument, error = MapDocument.load(oldDocument.fileName(), oldDocument.readerFormat()) if (not newDocument): self.reloadError.emit(self.tr("%s:\n\n%s"%(oldDocument.fileName(), error))) return False # Remember current view state mapView = self.viewForDocument(oldDocument) layerIndex = oldDocument.currentLayerIndex() scale = mapView.zoomable().scale() horizontalPosition = mapView.horizontalScrollBar().sliderPosition() verticalPosition = mapView.verticalScrollBar().sliderPosition() # Replace old tab self.addDocument(newDocument) self.closeDocumentAt(index) self.mTabWidget.moveTab(self.mDocuments.size() - 1, index) # Restore previous view state mapView = self.currentMapView() mapView.zoomable().setScale(scale) mapView.horizontalScrollBar().setSliderPosition(horizontalPosition) mapView.verticalScrollBar().setSliderPosition(verticalPosition) if (layerIndex > 0 and layerIndex < newDocument.map().layerCount()): newDocument.setCurrentLayerIndex(layerIndex) return True ## # Close all documents. Will not ask the user whether to save any changes! ## def closeAllDocuments(self): while (not self.mDocuments.isEmpty()): self.closeCurrentDocument() ## # Returns all open map documents. ## def documents(self): return self.mDocuments ## # Centers the current map on the tile coordinates \a x, \a y. ## def centerViewOn(self, *args): l = len(args) if l==2: x, y = args view = self.currentMapView() if (not view): return view.centerOn(self.currentDocument().renderer().pixelToScreenCoords(x, y)) elif l==1: pos = args[0] self.centerViewOn(pos.x(), pos.y()) def switchToLeftDocument(self): tabCount = self.mTabWidget.count() if (tabCount < 2): return currentIndex = self.mTabWidget.currentIndex() if currentIndex > 0: x = currentIndex else: x = tabCount self.switchToDocument(x - 1) def switchToRightDocument(self): tabCount = self.mTabWidget.count() if (tabCount < 2): return currentIndex = self.mTabWidget.currentIndex() self.switchToDocument((currentIndex + 1) % tabCount) def setSelectedTool(self, tool): if type(tool)==list: tool = tool[0] if (self.mSelectedTool == tool): return if self.mSelectedTool: self.mSelectedTool.cursorChanged.disconnect(self.cursorChanged) self.mSelectedTool = tool if self.mViewWithTool: mapScene = self.mViewWithTool.mapScene() mapScene.disableSelectedTool() if tool: mapScene.setSelectedTool(tool) mapScene.enableSelectedTool() if tool: self.mViewWithTool.viewport().setCursor(tool.cursor) else: self.mViewWithTool.viewport().unsetCursor() if tool: tool.cursorChanged.connect(self.cursorChanged) def currentIndexChanged(self): if self.mViewWithTool: mapScene = self.mViewWithTool.mapScene() mapScene.disableSelectedTool() self.mViewWithTool = None mapDocument = self.currentDocument() if (mapDocument): self.mUndoGroup.setActiveStack(mapDocument.undoStack()) self.currentDocumentChanged.emit([mapDocument]) mapView = self.currentMapView() if mapView: mapScene = mapView.mapScene() mapScene.setSelectedTool(self.mSelectedTool) mapScene.enableSelectedTool() if self.mSelectedTool: mapView.viewport().setCursor(self.mSelectedTool.cursor) else: mapView.viewport().unsetCursor() self.mViewWithTool = mapView def fileNameChanged(self, fileName, oldFileName): if fileName != '': self.mFileSystemWatcher.addPath(fileName) if oldFileName != '': self.mFileSystemWatcher.removePath(oldFileName) self.updateDocumentTab() def updateDocumentTab(self): mapDocument = self.sender() index = self.mDocuments.indexOf(mapDocument) tabText = mapDocument.displayName() if (mapDocument.isModified()): tabText = '*' + tabText self.mTabWidget.setTabText(index, tabText) self.mTabWidget.setTabToolTip(index, mapDocument.fileName()) def documentSaved(self): document = self.sender() index = self.mDocuments.indexOf(document) widget = self.mTabWidget.widget(index) container = widget container.setFileChangedWarningVisible(False) def documentTabMoved(self, _from, to): self.mDocuments.move(_from, to) def fileChanged(self, fileName): index = self.findDocument(fileName) # Most likely the file was removed if (index == -1): return document = self.mDocuments.at(index) # Ignore change event when it seems to be our own save if (QFileInfo(fileName).lastModified() == document.lastSaved()): return # Automatically reload when there are no unsaved changes if (not document.isModified()): self.reloadDocumentAt(index) return widget = self.mTabWidget.widget(index) container = widget container.setFileChangedWarningVisible(True) def cursorChanged(self, cursor): if self.mViewWithTool: self.mViewWithTool.viewport().setCursor(cursor) def reloadRequested(self): index = self.mTabWidget.indexOf(self.sender()) self.reloadDocumentAt(index)