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 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 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)