コード例 #1
0
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
コード例 #2
0
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)
コード例 #3
0
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)