Exemplo n.º 1
0
class ActionForm(BuildItemForm):
    """
    Form for editing Actions that displays an attr form
    for every attribute on the action.
    """

    convertToBatchClicked = QtCore.Signal()

    def setupUi(self, parent):
        super(ActionForm, self).setupUi(parent)

        # add batch conversion button to header
        convertToBatchBtn = QtWidgets.QPushButton(parent)
        convertToBatchBtn.setIcon(
            viewutils.getIcon("convertActionToBatch.png"))
        convertToBatchBtn.setFixedSize(QtCore.QSize(18, 18))
        convertToBatchBtn.clicked.connect(self.convertToBatchClicked.emit)
        self.headerLayout.addWidget(convertToBatchBtn)

    def setupContentUi(self, parent):
        for attr in self.buildItem.config['attrs']:
            attrValue = getattr(self.buildItem, attr['name'])
            attrForm = ActionAttrForm.createForm(attr,
                                                 attrValue,
                                                 parent=parent)
            attrForm.valueChanged.connect(
                partial(self.attrValueChanged, attrForm))
            self.mainLayout.addWidget(attrForm)

    def attrValueChanged(self, attrForm, attrValue, isValueValid):
        setattr(self.buildItem, attrForm.attr['name'], attrValue)
        self.buildItemChanged.emit()
Exemplo n.º 2
0
class CollapsibleFrame(QtWidgets.QFrame):
    """
    A QFrame that can be collapsed when clicked.
    """

    collapsedChanged = QtCore.Signal(bool)

    def __init__(self, parent):
        super(CollapsibleFrame, self).__init__(parent)
        self._isCollapsed = False

    def mouseReleaseEvent(self, QMouseEvent):
        if QMouseEvent.button() == QtCore.Qt.MouseButton.LeftButton:
            self.setIsCollapsed(not self._isCollapsed)
        else:
            return super(CollapsibleFrame, self).mouseReleaseEvent(QMouseEvent)

    def setIsCollapsed(self, newCollapsed):
        """
        Set the collapsed state of this frame.
        """
        self._isCollapsed = newCollapsed
        self.collapsedChanged.emit(self._isCollapsed)

    def isCollapsed(self):
        """
        Return True if the frame is currently collapsed.
        """
        return self._isCollapsed
Exemplo n.º 3
0
class BatchAttrForm(QtWidgets.QWidget):
    """
    The base class for an attribute form designed to
    bulk edit all variants of an attribute on a batch action.
    This appears where the default attr form usually appears
    when the attribute is marked as variant.
    
    BatchAttrForms should only exist if they provide an
    easy way to bulk set different values for all variants,
    as its pointless to provide functionality for setting all
    variants to the same value (would make the attribute constant).
    """

    TYPEMAP = {}

    valuesChanged = QtCore.Signal()
    variantCountChanged = QtCore.Signal()

    @staticmethod
    def doesFormExist(attr):
        return attr['type'] in BatchAttrForm.TYPEMAP

    @staticmethod
    def createForm(action, attr, parent=None):
        """
        Create a new ActionAttrForm of the appropriate
        type based on a BuildAction attribute.

        Args:
            attr: A dict representing the config of a BuildAction attribute
        """
        attrType = attr['type']
        if attrType in BatchAttrForm.TYPEMAP:
            return BatchAttrForm.TYPEMAP[attrType](action, attr, parent=parent)

    def __init__(self, batchAction, attr, parent=None):
        super(BatchAttrForm, self).__init__(parent=parent)
        self.batchAction = batchAction
        self.attr = attr
        self.setupUi(self)

    def setupUi(self, parent):
        raise NotImplementedError
Exemplo n.º 4
0
class CollapsibleFrame(QtWidgets.QFrame):

    collapsedChanged = QtCore.Signal(bool)

    def __init__(self, parent):
        super(CollapsibleFrame, self).__init__(parent)
        self._isCollapsed = False
    
    def mouseReleaseEvent(self, QMouseEvent):
        if QMouseEvent.button() == QtCore.Qt.MouseButton.LeftButton:
            self.setIsCollapsed(not self._isCollapsed)
        else:
            return super(CollapsibleFrame, self).mouseReleaseEvent(QMouseEvent)
    
    def setIsCollapsed(self, newCollapsed):
        self._isCollapsed = newCollapsed
        self.collapsedChanged.emit(self._isCollapsed)
    
    def isCollapsed(self):
        return self._isCollapsed
Exemplo n.º 5
0
class BlueprintUIModel(QtCore.QObject):
    """
    The owner and manager of various models representing a Blueprint
    in the scene. All reading and writing for the Blueprint through
    the UI should be done using this model.
    """

    # shared instances, mapped by name
    INSTANCES = {}

    @classmethod
    def getDefaultModel(cls):
        return cls.getSharedModel(None)

    @classmethod
    def getSharedModel(cls, name):
        """
        Return a shared UI model by name, creating a new
        model if necessary. Will always return a valid
        BlueprintUIModel.
        """
        if name not in cls.INSTANCES:
            cls.INSTANCES[name] = cls(name)
        return cls.INSTANCES[name]

    @classmethod
    def deleteSharedModel(cls, name):
        if name in cls.INSTANCES:
            del cls.INSTANCES[name]

    # a config property on the blueprint changed
    # TODO: add more generic blueprint property data model
    rigNameChanged = QtCore.Signal(str)

    def __init__(self, parent=None):
        super(BlueprintUIModel, self).__init__(parent=parent)

        # the blueprint of this model
        self.blueprint = Blueprint()

        # the tree item model and selection model for BuildItems
        self.buildStepTreeModel = BuildStepTreeModel(self.blueprint)
        self.buildStepSelectionModel = BuildStepSelectionModel(
            self.buildStepTreeModel)

        # attempt to load from the scene
        self.loadFromFile(suppressWarnings=True)

    def isReadOnly(self):
        """
        Return True if the Blueprint is not able to be modified.
        """
        return False

    def getBlueprintFilepath(self):
        """
        Return the filepath for the Blueprint being edited
        """
        sceneName = pm.sceneName()
        if not sceneName:
            return None

        filepath = os.path.splitext(sceneName)[0] + '.yaml'
        return filepath

    def saveToFile(self, suppressWarnings=False):
        """
        Save the Blueprint data to the file associated with this model
        """
        filepath = self.getBlueprintFilepath()
        if not filepath:
            if not suppressWarnings:
                LOG.warning("Scene is not saved")
            return

        success = self.blueprint.saveToFile(filepath)
        if not success:
            LOG.error("Failed to save Blueprint to file: {0}".format(filepath))

    def loadFromFile(self, suppressWarnings=False):
        """
        Load the Blueprint from the file associated with this model
        """
        filepath = self.getBlueprintFilepath()
        if not filepath:
            if not suppressWarnings:
                LOG.warning("Scene is not saved")
            return

        success = self.blueprint.loadFromFile(filepath)
        self.emitAllModelResets()

        if not success:
            LOG.error(
                "Failed to load Blueprint from file: {0}".format(filepath))

    def emitAllModelResets(self):
        self.buildStepTreeModel.modelReset.emit()
        self.rigNameChanged.emit(self.getRigName())

    def getRigName(self):
        return self.blueprint.rigName

    def setRigName(self, newRigName):
        if not self.isReadOnly():
            self.blueprint.rigName = newRigName
            self.rigNameChanged.emit(self.blueprint.rigName)

    def initializeBlueprint(self):
        """
        Initialize the Blueprint to its default state.
        """
        self.blueprint.rootStep.clearChildren()
        self.blueprint.initializeDefaultActions()
        self.emitAllModelResets()

    def getActionDataForAttrPath(self, attrPath):
        """
        Return serialized data for an action represented
        by an attribute path.
        """
        stepPath, _ = attrPath.split('.')

        step = self.blueprint.getStepByPath(stepPath)
        if not step.isAction():
            LOG.error(
                'getActionDataForAttrPath: {0} is not an action'.format(step))
            return

        return step.actionProxy.serialize()

    def setActionDataForAttrPath(self, attrPath, data):
        """
        Replace all values on an action represented by an
        attribute path by deserializng data.
        """
        stepPath, _ = attrPath.split('.')

        step = self.blueprint.getStepByPath(stepPath)
        if not step.isAction():
            LOG.error(
                'setActionDataForAttrPath: {0} is not an action'.format(step))
            return

        step.actionProxy.deserialize(data)

        index = self.buildStepTreeModel.indexByStepPath(stepPath)
        self.buildStepTreeModel.dataChanged.emit(index, index, [])

    def getActionAttr(self, attrPath, variantIndex=-1):
        stepPath, attrName = attrPath.split('.')

        step = self.blueprint.getStepByPath(stepPath)
        if not step:
            LOG.error("Could not find step: {0}".format(stepPath))
            return

        if not step.isAction():
            LOG.error('getActionAttr: {0} is not an action'.format(step))
            return

        if variantIndex >= 0:
            if step.actionProxy.numVariants() > variantIndex:
                actionData = step.actionProxy.getVariant(variantIndex)
                return actionData.getAttrValue(attrName)
        else:
            return step.actionProxy.getAttrValue(attrName)

    def setActionAttr(self, attrPath, value, variantIndex=-1):
        """
        Set the value for an attribute on the Blueprint
        """
        if self.isReadOnly():
            return

        stepPath, attrName = attrPath.split('.')

        step = self.blueprint.getStepByPath(stepPath)
        if not step.isAction():
            LOG.error('setActionAttr: {0} is not an action'.format(step))
            return

        if variantIndex >= 0:
            variant = step.actionProxy.getOrCreateVariant(variantIndex)
            variant.setAttrValue(attrName, value)
        else:
            step.actionProxy.setAttrValue(attrName, value)

        index = self.buildStepTreeModel.indexByStepPath(stepPath)
        self.buildStepTreeModel.dataChanged.emit(index, index, [])

    def isActionAttrVariant(self, attrPath):
        stepPath, attrName = attrPath.split('.')

        step = self.blueprint.getStepByPath(stepPath)
        if not step.isAction():
            LOG.error("isActionAttrVariant: {0} is not an action".format(step))
            return

        return step.actionProxy.isVariantAttr(attrName)

    def setIsActionAttrVariant(self, attrPath, isVariant):
        """
        """
        if self.isReadOnly():
            return

        stepPath, attrName = attrPath.split('.')

        step = self.blueprint.getStepByPath(stepPath)
        if not step.isAction():
            LOG.error(
                "setIsActionAttrVariant: {0} is not an action".format(step))
            return

        step.actionProxy.setIsVariantAttr(attrName, isVariant)

        index = self.buildStepTreeModel.indexByStepPath(stepPath)
        self.buildStepTreeModel.dataChanged.emit(index, index, [])

    def moveStep(self, sourcePath, targetPath):
        """
        Move a BuildStep from source path to target path.

        Returns:
            The new path (str) of the build step, or None if
            the operation failed.
        """
        if self.isReadOnly():
            return

        step = self.blueprint.getStepByPath(sourcePath)
        if not step:
            LOG.error("moveStep: failed to find step: {0}".format(sourcePath))
            return

        if step == self.blueprint.rootStep:
            LOG.error("moveStep: cannot move root step")
            return

        index = self.buildStepTreeModel.indexByStepPath(sourcePath)

        # TODO: handle moving between new parents
        newName = targetPath.split('/')[-1]
        step.setName(newName)
        self.buildStepTreeModel.dataChanged.emit(index, index, [])
        return step.getFullPath()
Exemplo n.º 6
0
class BlueprintUIModel(QtCore.QObject):
    """
    The owner and manager of various models representing a Blueprint
    in the scene. All reading and writing for the Blueprint through
    the UI should be done using this model.

    Blueprints edited though the UI are saved to yaml files
    associated with a maya scene file. This model is also responsible
    for loading and saving the blueprint yaml file.
    """

    # shared instances, mapped by name
    INSTANCES = {}

    @classmethod
    def getDefaultModel(cls):
        # type: (class) -> BlueprintUIModel
        """
        Return the default model instance used by editor views.
        """
        return cls.getSharedModel(None)

    @classmethod
    def getSharedModel(cls, name):
        # type: (class, str) -> BlueprintUIModel
        """
        Return a shared UI model by name, creating a new
        model if necessary. Will always return a valid
        BlueprintUIModel.
        """
        if name not in cls.INSTANCES:
            cls.INSTANCES[name] = cls(name)
        return cls.INSTANCES[name]

    @classmethod
    def deleteSharedModel(cls, name):
        if name in cls.INSTANCES:
            cls.INSTANCES[name].onDelete()
            del cls.INSTANCES[name]

    @classmethod
    def deleteAllSharedModels(cls):
        keys = cls.INSTANCES.keys()
        for key in keys:
            cls.INSTANCES[key].onDelete()
            del cls.INSTANCES[key]

    # a config property on the blueprint changed
    # TODO: add more generic blueprint property data model
    rigNameChanged = QtCore.Signal(str)

    rigExistsChanged = QtCore.Signal()

    autoSave = optionVarProperty('pulse.editor.autoSave', True)
    autoLoad = optionVarProperty('pulse.editor.autoLoad', True)

    def setAutoSave(self, value):
        self.autoSave = value

    def setAutoLoad(self, value):
        self.autoLoad = value

    def __init__(self, parent=None):
        super(BlueprintUIModel, self).__init__(parent=parent)

        # the blueprint of this model
        self._blueprint = Blueprint()

        # the tree item model and selection model for BuildSteps
        self.buildStepTreeModel = BuildStepTreeModel(self.blueprint, self)
        self.buildStepSelectionModel = BuildStepSelectionModel(
            self.buildStepTreeModel, self)

        # keeps track of whether a rig is currently in the scene,
        # which will affect the ability to edit the Blueprint
        self.rigExists = len(pulse.getAllRigs()) > 0

        # attempt to load from the scene
        if self.autoLoad:
            self.load(suppressWarnings=True)

        # register maya scene callbacks that can be used
        # for auto-saving the Blueprint
        self._callbackIds = []
        self.addSceneCallbacks()

    def onDelete(self):
        self.removeSceneCallbacks()

    @property
    def blueprint(self):
        """
        The Blueprint object represented by this Model.
        """
        return self._blueprint

    def isReadOnly(self):
        """
        Return True if the Blueprint is not able to be modified.
        """
        return self.rigExists

    def getBlueprintFilepath(self):
        """
        Return the filepath for the Blueprint being edited
        """
        sceneName = None

        if self.rigExists:
            # get filepath from rig
            rig = pulse.getAllRigs()[0]
            rigdata = meta.getMetaData(rig, pulse.RIG_METACLASS)
            sceneName = rigdata.get('blueprintFile')
        else:
            sceneName = pm.sceneName()

        if sceneName:
            filepath = os.path.splitext(sceneName)[0] + '.yaml'
            return filepath

    def addSceneCallbacks(self):
        if not self._callbackIds:
            saveId = api.MSceneMessage.addCallback(
                api.MSceneMessage.kBeforeSave, self.onBeforeSaveScene)
            openId = api.MSceneMessage.addCallback(
                api.MSceneMessage.kAfterOpen, self.onAfterOpenScene)
            newId = api.MSceneMessage.addCallback(
                api.MSceneMessage.kAfterNew, self.onAfterNewScene)
            self._callbackIds.append(saveId)
            self._callbackIds.append(openId)
            self._callbackIds.append(newId)
        LOG.debug(
            'BlueprintUIModel: added scene callbacks')

    def removeSceneCallbacks(self):
        if self._callbackIds:
            while self._callbackIds:
                cbid = self._callbackIds.pop()
                api.MMessage.removeCallback(cbid)
            LOG.debug(
                'BlueprintUIModel: removed scene callbacks')

    def onBeforeSaveScene(self, clientData=None):
        if self.autoSave:
            self.refreshRigExists()
            if not self.isReadOnly():
                LOG.debug('Auto-saving Pulse Blueprint...')
                self.save()

    def onAfterOpenScene(self, clientData=None):
        if self.autoLoad:
            self.load(suppressWarnings=True)

    def onAfterNewScene(self, clientData=None):
        self.initializeBlueprint()

    def save(self, suppressWarnings=False):
        """
        Save the Blueprint data to the file associated with this model
        """
        self.refreshRigExists()

        if self.isReadOnly():
            return

        filepath = self.getBlueprintFilepath()
        if not filepath:
            if not suppressWarnings:
                LOG.warning("Scene is not saved")
            return

        success = self.blueprint.saveToFile(filepath)
        if not success:
            LOG.error("Failed to save Blueprint to file: {0}".format(filepath))

    def load(self, suppressWarnings=False):
        """
        Load the Blueprint from the file associated with this model
        """
        self.refreshRigExists()
        filepath = self.getBlueprintFilepath()
        if not filepath:
            if not suppressWarnings:
                LOG.warning("Scene is not saved")

            self.initializeBlueprint()
            return

        if not os.path.isfile(filepath):
            if not suppressWarnings:
                LOG.warning(
                    "Blueprint file does not exist: {0}".format(filepath))
            return

        self.buildStepTreeModel.beginResetModel()
        success = self.blueprint.loadFromFile(filepath)
        self.buildStepTreeModel.endResetModel()
        self.rigNameChanged.emit(self.getRigName())

        if not success:
            LOG.error(
                "Failed to load Blueprint from file: {0}".format(filepath))

    def refreshRigExists(self):
        self.rigExists = len(pulse.getAllRigs()) > 0
        self.rigExistsChanged.emit()

    def getRigName(self):
        return self.blueprint.rigName

    def setRigName(self, newRigName):
        if self.isReadOnly():
            LOG.error('Cannot edit readonly Blueprint')
            return

        self.blueprint.rigName = newRigName
        self.rigNameChanged.emit(self.blueprint.rigName)

    def initializeBlueprint(self):
        """
        Initialize the Blueprint to an empty state.
        """
        self.buildStepTreeModel.beginResetModel()
        self.blueprint.rigName = None
        self.blueprint.rootStep.clearChildren()
        self.buildStepTreeModel.endResetModel()
        self.rigNameChanged.emit(self.blueprint.rigName)

    def initializeBlueprintToDefaultActions(self):
        """
        Initialize the Blueprint to its default state based
        on the current blueprint config.
        """
        self.buildStepTreeModel.beginResetModel()
        self.blueprint.rootStep.clearChildren()
        self.blueprint.initializeDefaultActions()
        self.buildStepTreeModel.endResetModel()

    def createStep(self, parentPath, childIndex, data):
        """
        Create a new BuildStep

        Args:
            parentPath (str): The path to the parent step
            childIndex (int): The index at which to insert the new step
            data (str): The serialized data for the BuildStep to create

        Returns:
            The newly created BuildStep, or None if the operation failed.
        """
        if self.isReadOnly():
            LOG.error('Cannot edit readonly Blueprint')
            return

        parentStep = self.blueprint.getStepByPath(parentPath)
        if not parentStep:
            LOG.error("createStep: failed to find parent step: %s", parentPath)
            return

        parentIndex = self.buildStepTreeModel.indexByStep(parentStep)
        self.buildStepTreeModel.beginInsertRows(
            parentIndex, childIndex, childIndex)

        step = BuildStep.fromData(data)
        parentStep.insertChild(childIndex, step)

        self.buildStepTreeModel.endInsertRows()
        return step

    def deleteStep(self, stepPath):
        """
        Delete a BuildStep

        Returns:
            True if the step was deleted successfully
        """
        if self.isReadOnly():
            LOG.error('Cannot edit readonly Blueprint')
            return False

        step = self.blueprint.getStepByPath(stepPath)
        if not step:
            LOG.error("deleteStep: failed to find step: %s", stepPath)
            return False

        stepIndex = self.buildStepTreeModel.indexByStep(step)
        self.buildStepTreeModel.beginRemoveRows(
            stepIndex.parent(), stepIndex.row(), stepIndex.row())

        step.removeFromParent()

        self.buildStepTreeModel.endRemoveRows()
        return True

    def moveStep(self, sourcePath, targetPath):
        """
        Move a BuildStep from source path to target path.

        Returns:
            The new path (str) of the build step, or None if
            the operation failed.
        """
        if self.isReadOnly():
            LOG.error('Cannot edit readonly Blueprint')
            return

        step = self.blueprint.getStepByPath(sourcePath)
        if not step:
            LOG.error("moveStep: failed to find step: %s", sourcePath)
            return

        if step == self.blueprint.rootStep:
            LOG.error("moveStep: cannot move root step")
            return

        self.buildStepTreeModel.layoutAboutToBeChanged.emit()

        sourceParentPath = os.path.dirname(sourcePath)
        targetParentPath = os.path.dirname(targetPath)
        if sourceParentPath != targetParentPath:
            step.setParent(self.blueprint.getStepByPath(targetParentPath))
        targetName = os.path.basename(targetPath)
        step.setName(targetName)

        self.buildStepTreeModel.layoutChanged.emit()

        return step.getFullPath()

    def renameStep(self, stepPath, targetName):
        if self.isReadOnly():
            LOG.error('Cannot edit readonly Blueprint')
            return

        step = self.blueprint.getStepByPath(stepPath)
        if not step:
            LOG.error("moveStep: failed to find step: %s", stepPath)
            return

        if step == self.blueprint.rootStep:
            LOG.error("moveStep: cannot rename root step")
            return

        oldName = step.name
        step.setName(targetName)

        if step.name != oldName:
            index = self.buildStepTreeModel.indexByStep(step)
            self.buildStepTreeModel.dataChanged.emit(index, index, [])

        return step.getFullPath()

    def getStep(self, stepPath):
        """
        Return the BuildStep at a path
        """
        return self.blueprint.getStepByPath(stepPath)

    def getStepData(self, stepPath):
        """
        Return the serialized data for a step at a path
        """
        step = self.getStep(stepPath)
        if step:
            return step.serialize()

    def getActionData(self, stepPath):
        """
        Return serialized data for a BuildActionProxy
        """
        step = self.getStep(stepPath)
        if not step:
            return

        if not step.isAction():
            LOG.error(
                'getActionData: %s step is not an action', step)
            return

        return step.actionProxy.serialize()

    def setActionData(self, stepPath, data):
        """
        Replace all attribute values on a BuildActionProxy.
        """
        step = self.getStep(stepPath)
        if not step:
            return

        if not step.isAction():
            LOG.error(
                'setActionData: %s step is not an action', step)
            return

        step.actionProxy.deserialize(data)

        index = self.buildStepTreeModel.indexByStepPath(stepPath)
        self.buildStepTreeModel.dataChanged.emit(index, index, [])

    def getActionAttr(self, attrPath, variantIndex=-1):
        """
        Return the value of an attribute of a BuildAction

        Args:
            attrPath (str): The full path to an action attribute, e.g. 'My/Action.myAttr'
            variantIndex (int): The index of the variant to retrieve, if the action has variants

        Returns:
            The attribute value, of varying types
        """
        stepPath, attrName = attrPath.split('.')

        step = self.getStep(stepPath)
        if not step:
            return

        if not step.isAction():
            LOG.error('getActionAttr: %s is not an action', step)
            return

        if variantIndex >= 0:
            if step.actionProxy.numVariants() > variantIndex:
                actionData = step.actionProxy.getVariant(variantIndex)
                return actionData.getAttrValue(attrName)
        else:
            return step.actionProxy.getAttrValue(attrName)

    def setActionAttr(self, attrPath, value, variantIndex=-1):
        """
        Set the value for an attribute on the Blueprint
        """
        if self.isReadOnly():
            LOG.error('Cannot edit readonly Blueprint')
            return

        stepPath, attrName = attrPath.split('.')

        step = self.getStep(stepPath)
        if not step:
            return

        if not step.isAction():
            LOG.error('setActionAttr: %s is not an action', step)
            return

        if variantIndex >= 0:
            variant = step.actionProxy.getOrCreateVariant(variantIndex)
            variant.setAttrValue(attrName, value)
        else:
            step.actionProxy.setAttrValue(attrName, value)

        index = self.buildStepTreeModel.indexByStepPath(stepPath)
        self.buildStepTreeModel.dataChanged.emit(index, index, [])

    def isActionAttrVariant(self, attrPath):
        stepPath, attrName = attrPath.split('.')

        step = self.getStep(stepPath)
        if not step.isAction():
            LOG.error(
                "isActionAttrVariant: {0} is not an action".format(step))
            return

        return step.actionProxy.isVariantAttr(attrName)

    def setIsActionAttrVariant(self, attrPath, isVariant):
        """
        """
        if self.isReadOnly():
            LOG.error('Cannot edit readonly Blueprint')
            return

        stepPath, attrName = attrPath.split('.')

        step = self.getStep(stepPath)
        if not step:
            return

        if not step.isAction():
            LOG.error(
                "setIsActionAttrVariant: {0} is not an action".format(step))
            return

        step.actionProxy.setIsVariantAttr(attrName, isVariant)

        index = self.buildStepTreeModel.indexByStepPath(stepPath)
        self.buildStepTreeModel.dataChanged.emit(index, index, [])
Exemplo n.º 7
0
class BuildItemForm(QtWidgets.QWidget):
    """
    Base class for a form for editing any type of BuildItem
    """

    buildItemChanged = QtCore.Signal()

    @staticmethod
    def createItemWidget(buildItem, parent=None):
        if isinstance(buildItem, pulse.BuildGroup):
            return BuildGroupForm(buildItem, parent=parent)
        elif isinstance(buildItem, pulse.BuildAction):
            return ActionForm(buildItem, parent=parent)
        elif isinstance(buildItem, pulse.BatchBuildAction):
            return BatchActionForm(buildItem, parent=parent)
        return QtWidgets.QWidget(parent=parent)

    def __init__(self, buildItem, parent=None):
        super(BuildItemForm, self).__init__(parent=parent)
        self.buildItem = buildItem
        self.setupUi(self)
        self.setupContentUi(self)

    def getItemDisplayName(self):
        return self.buildItem.getDisplayName()

    def getItemIcon(self):
        iconFile = self.buildItem.getIconFile()
        if iconFile:
            return QtGui.QIcon(iconFile)

    def getItemColor(self):
        color = self.buildItem.getColor()
        if color:
            return [int(c * 255) for c in color]
        else:
            return [255, 255, 255]

    def setupUi(self, parent):
        """
        Create the UI that is common to all BuildItem editors, including
        a basic header and layout.
        """
        # main layout containing header and body
        layout = QtWidgets.QVBoxLayout(parent)
        layout.setSpacing(4)
        layout.setMargin(0)

        # header frame
        self.headerFrame = QtWidgets.QFrame(parent)
        headerColor = 'rgba({0}, {1}, {2}, 40)'.format(*self.getItemColor())
        self.headerFrame.setStyleSheet(
            ".QFrame{{ background-color: {color}; border-radius: 2px; }}".
            format(color=headerColor))
        layout.addWidget(self.headerFrame)
        # header layout
        self.headerLayout = QtWidgets.QHBoxLayout(self.headerFrame)
        self.headerLayout.setContentsMargins(10, 4, 4, 4)
        # display name label
        font = QtGui.QFont()
        font.setWeight(75)
        font.setBold(True)
        self.displayNameLabel = QtWidgets.QLabel(self.headerFrame)
        self.displayNameLabel.setMinimumHeight(18)
        self.displayNameLabel.setFont(font)
        self.displayNameLabel.setText(self.getItemDisplayName())
        self.headerLayout.addWidget(self.displayNameLabel)

        # body layout
        bodyFrame = QtWidgets.QFrame(parent)
        bodyFrame.setObjectName("bodyFrame")
        bodyColor = 'rgba(255, 255, 255, 5)'.format(*self.getItemColor())
        bodyFrame.setStyleSheet(
            ".QFrame#bodyFrame{{ background-color: {color}; }}".format(
                color=bodyColor))
        layout.addWidget(bodyFrame)

        self.mainLayout = QtWidgets.QVBoxLayout(bodyFrame)
        self.mainLayout.setMargin(6)
        self.mainLayout.setSpacing(0)

    def setupContentUi(self, parent):
        pass
Exemplo n.º 8
0
class BatchActionForm(BuildItemForm):
    """
    Form for editing Batch Actions. Very similar
    to the standard ActionForm, with a few key
    differences.

    Each attribute has a toggle that controls
    whether the attribute is variant or not.

    All variants are displayed in a list, with the ability
    to easily add and remove variants. If a BatchAttrForm
    exists for a variant attribute type, it will be displayed
    in place of the normal AttrEditor form (only when that
    attribute is marked as variant).
    """

    convertToActionClicked = QtCore.Signal()

    def getItemDisplayName(self):
        return 'Batch {0} (x{1})'.format(self.buildItem.getDisplayName(),
                                         self.buildItem.getActionCount())

    def setupUi(self, parent):
        super(BatchActionForm, self).setupUi(parent)

        # add action conversion button to header
        convertToActionBtn = QtWidgets.QPushButton(parent)
        convertToActionBtn.setIcon(
            viewutils.getIcon("convertBatchToAction.png"))
        convertToActionBtn.setFixedSize(QtCore.QSize(18, 18))
        convertToActionBtn.clicked.connect(self.convertToActionClicked.emit)
        self.headerLayout.addWidget(convertToActionBtn)

    def setupContentUi(self, parent):
        """
        Build the content ui for this BatchBuildAction.
        Creates ui to manage the array of variant attributes.
        """

        # constants main layout
        self.constantsLayout = QtWidgets.QVBoxLayout(parent)
        self.constantsLayout.setContentsMargins(0, 0, 0, 0)
        self.mainLayout.addLayout(self.constantsLayout)

        spacer = QtWidgets.QSpacerItem(20, 4, QtWidgets.QSizePolicy.Expanding,
                                       QtWidgets.QSizePolicy.Minimum)
        self.mainLayout.addItem(spacer)

        # variant header
        variantHeader = QtWidgets.QFrame(parent)
        variantHeader.setStyleSheet(
            ".QFrame{ background-color: rgb(255, 255, 255, 15); border-radius: 2px }"
        )
        self.mainLayout.addWidget(variantHeader)

        variantHeaderLayout = QtWidgets.QHBoxLayout(variantHeader)
        variantHeaderLayout.setContentsMargins(10, 4, 4, 4)
        variantHeaderLayout.setSpacing(4)

        self.variantsLabel = QtWidgets.QLabel(variantHeader)
        self.variantsLabel.setText("Variants: {0}".format(
            len(self.buildItem.variantValues)))
        variantHeaderLayout.addWidget(self.variantsLabel)

        spacer = QtWidgets.QSpacerItem(20, 4, QtWidgets.QSizePolicy.Expanding,
                                       QtWidgets.QSizePolicy.Minimum)
        self.mainLayout.addItem(spacer)

        # add variant button
        addVariantBtn = QtWidgets.QPushButton(variantHeader)
        addVariantBtn.setText('+')
        addVariantBtn.setFixedSize(QtCore.QSize(20, 20))
        addVariantBtn.clicked.connect(self.addVariant)
        variantHeaderLayout.addWidget(addVariantBtn)

        # variant list main layout
        self.variantLayout = QtWidgets.QVBoxLayout(parent)
        self.variantLayout.setContentsMargins(0, 0, 0, 0)
        self.variantLayout.setSpacing(4)
        self.mainLayout.addLayout(self.variantLayout)

        self.setupConstantsUi(parent)
        self.setupVariantsUi(parent)

    def setupConstantsUi(self, parent):
        viewutils.clearLayout(self.constantsLayout)

        # create attr form all constant attributes
        for attr in self.buildItem.actionClass.config['attrs']:
            isConstant = (attr['name'] in self.buildItem.constantValues)
            # make an HBox with a button to toggle variant state
            attrHLayout = QtWidgets.QHBoxLayout(parent)
            attrHLayout.setSpacing(10)
            attrHLayout.setMargin(0)
            self.constantsLayout.addLayout(attrHLayout)

            if isConstant:
                # constant value, make an attr form
                attrValue = self.buildItem.constantValues[attr['name']]
                context = self.buildItem.constantValues
                attrForm = ActionAttrForm.createForm(attr,
                                                     attrValue,
                                                     parent=parent)
                attrForm.valueChanged.connect(
                    partial(self.attrValueChanged, context, attrForm))
                attrHLayout.addWidget(attrForm)
            else:
                # variant value, check for batch editor, or just display a label
                attrLabel = QtWidgets.QLabel(parent)
                # extra 2 to account for the left-side frame padding that occurs in the ActionAttrForm
                attrLabel.setFixedSize(
                    QtCore.QSize(ActionAttrForm.LABEL_WIDTH + 2,
                                 ActionAttrForm.LABEL_HEIGHT))
                attrLabel.setAlignment(QtCore.Qt.AlignRight
                                       | QtCore.Qt.AlignTrailing
                                       | QtCore.Qt.AlignTop)
                attrLabel.setMargin(2)
                attrLabel.setText(pulse.names.toTitle(attr['name']))
                attrLabel.setEnabled(False)
                attrHLayout.addWidget(attrLabel)

                if BatchAttrForm.doesFormExist(attr):
                    batchEditor = BatchAttrForm.createForm(self.buildItem,
                                                           attr,
                                                           parent=parent)
                    batchEditor.valuesChanged.connect(
                        self.batchEditorValuesChanged)
                    attrHLayout.addWidget(batchEditor)
                else:
                    spacer = QtWidgets.QSpacerItem(
                        24, 24, QtWidgets.QSizePolicy.Expanding,
                        QtWidgets.QSizePolicy.Minimum)
                    attrHLayout.addItem(spacer)

            # not a constant value, add a line with button to make it constant
            # button to toggle variant
            toggleVariantBtn = QtWidgets.QPushButton(parent)
            toggleVariantBtn.setText("v")
            toggleVariantBtn.setFixedSize(QtCore.QSize(20, 20))
            toggleVariantBtn.setCheckable(True)
            toggleVariantBtn.setChecked(not isConstant)
            attrHLayout.addWidget(toggleVariantBtn)
            attrHLayout.setAlignment(toggleVariantBtn, QtCore.Qt.AlignTop)
            toggleVariantBtn.clicked.connect(
                partial(self.setIsVariantAttr, attr['name'], isConstant))

    def setupVariantsUi(self, parent):
        viewutils.clearLayout(self.variantLayout)

        self.variantsLabel.setText("Variants: {0}".format(
            len(self.buildItem.variantValues)))
        for i, variant in enumerate(self.buildItem.variantValues):

            if i > 0:
                # divider line
                dividerLine = QtWidgets.QFrame(parent)
                dividerLine.setStyleSheet(
                    ".QFrame{ background-color: rgb(0, 0, 0, 15); border-radius: 2px }"
                )
                dividerLine.setMinimumHeight(2)
                self.variantLayout.addWidget(dividerLine)

            variantHLayout = QtWidgets.QHBoxLayout(parent)

            # remove variant button
            removeVariantBtn = QtWidgets.QPushButton(parent)
            removeVariantBtn.setText('x')
            removeVariantBtn.setFixedSize(QtCore.QSize(20, 20))
            removeVariantBtn.clicked.connect(
                partial(self.removeVariantAtIndex, i))
            variantHLayout.addWidget(removeVariantBtn)

            # create attr form for all variant attributes
            variantVLayout = QtWidgets.QVBoxLayout(parent)
            variantVLayout.setSpacing(0)
            variantHLayout.addLayout(variantVLayout)

            if self.buildItem.variantAttributes:
                for attr in self.buildItem.actionClass.config['attrs']:
                    if attr['name'] not in self.buildItem.variantAttributes:
                        continue
                    attrValue = variant[attr['name']]
                    # context = variant
                    attrForm = ActionAttrForm.createForm(attr,
                                                         attrValue,
                                                         parent=parent)
                    attrForm.valueChanged.connect(
                        partial(self.attrValueChanged, variant, attrForm))
                    variantVLayout.addWidget(attrForm)
            else:
                noAttrsLabel = QtWidgets.QLabel(parent)
                noAttrsLabel.setText("No variant attributes")
                noAttrsLabel.setMinimumHeight(24)
                noAttrsLabel.setContentsMargins(10, 0, 0, 0)
                noAttrsLabel.setEnabled(False)
                variantVLayout.addWidget(noAttrsLabel)

            self.variantLayout.addLayout(variantHLayout)

    def setIsVariantAttr(self, attrName, isVariant):
        if isVariant:
            self.buildItem.addVariantAttr(attrName)
        else:
            self.buildItem.removeVariantAttr(attrName)
        self.setupConstantsUi(self)
        self.setupVariantsUi(self)

    def addVariant(self):
        self.buildItem.addVariant()
        self.setupVariantsUi(self)

    def removeVariantAtIndex(self, index):
        self.buildItem.removeVariantAt(index)
        self.setupVariantsUi(self)

    def removeVariantFromEnd(self):
        self.buildItem.removeVariantAt(-1)
        self.setupVariantsUi(self)

    def attrValueChanged(self, context, attrForm, attrValue, isValueValid):
        """
        Args:
            context: A dict representing the either constantValues object or
                a variant within the batch action
        """
        attrName = attrForm.attr['name']
        # prevent adding new keys to the context dict
        if attrName in context:
            context[attrName] = attrValue
            self.buildItemChanged.emit()

    def batchEditorValuesChanged(self):
        self.setupVariantsUi(self)
Exemplo n.º 9
0
class ActionAttrForm(QtWidgets.QWidget):
    """
    The base class for all forms used to edit action attributes.
    Provides input validation and basic signals for keeping
    track of value changes.
    """

    TYPEMAP = {}

    LABEL_WIDTH = 150
    LABEL_HEIGHT = 20
    FORM_WIDTH_SMALL = 80

    # valueChanged(newValue, isValueValid)
    valueChanged = QtCore.Signal(object, bool)

    @staticmethod
    def createForm(attr, attrValue, parent=None):
        """
        Create a new ActionAttrForm of the appropriate
        type based on a BuildAction attribute.

        Args:
            attr: A dict representing the config of a BuildAction attribute
            attrValue: The current value of the attribute
        """
        attrType = attr['type']
        if attrType in ActionAttrForm.TYPEMAP:
            return ActionAttrForm.TYPEMAP[attrType](attr,
                                                    attrValue,
                                                    parent=parent)
        # fallback to the default widget
        return DefaultAttrForm(attr, attrValue)

    def __init__(self, attr, attrValue, parent=None):
        super(ActionAttrForm, self).__init__(parent=parent)
        # the config data of the attribute being edited
        self.attr = attr
        # the current value of the attribute
        self.attrValue = attrValue
        # build the ui
        self.setupUi(self)
        # update valid state, check both type and value here
        # because the current value may be of an invalid type
        self.isValueValid = self._isValueTypeValid(
            self.attrValue) and self._isValueValid(self.attrValue)
        self._setUiValidState(self.isValueValid)

    def setAttrValue(self, newValue):
        """
        Set the current value of the attribute in this form.
        Performs partial validation and prevents setting
        the value if it's type is invalid.
        """
        # value doesn't need to be valid as long
        # as it has the right type
        if self._isValueTypeValid(newValue):
            self.attrValue = newValue
            self._setFormValue(newValue)
            self.isValueValid = self._isValueValid(newValue)
            self._setUiValidState(self.isValueValid)
            return True
        else:
            return False

    def setupUi(self, parent):
        """
        Build the appropriate ui for the attribute
        """
        raise NotImplementedError

    def _setFormValue(self, attrValue):
        """
        Set the current value displayed in the UI form
        """
        raise NotImplementedError

    def _getFormValue(self):
        """
        Return the current attribute value from the UI form.
        The result must always be of a valid type for this attr,
        though the value itself can be invalid.
        """
        raise NotImplementedError

    def _isFormValid(self):
        """
        Return True if the current form contains valid data.
        """
        return True

    def _isValueTypeValid(self, attrValue):
        """
        Return True if a potential value for the attribute matches
        the type of attribute. Attributes of at least a valid
        type can be saved, even though they may cause issues if not
        fixed before building.
        """
        return True

    def _isValueValid(self, attrValue):
        """
        Return True if a potential value for the attribute is valid
        """
        return True

    def _valueChanged(self):
        """
        Update the current attrValue and isValueValid state.
        Should be called whenever relevant UI values change.
        The new value will be retrieved by using `_getFormValue`,
        and validated using `_isValueValid`
        """
        # only emit when form is valid
        if self._isFormValid():
            self.attrValue = self._getFormValue()
            self.isValueValid = self._isValueValid(self.attrValue)
            self._setUiValidState(self.isValueValid)
            self.valueChanged.emit(self.attrValue, self.isValueValid)
        else:
            self._setUiValidState(False)

    def _setUiValidState(self, isValid):
        if hasattr(self, 'frame'):
            if isValid:
                self.frame.setStyleSheet('')
            else:
                self.frame.setStyleSheet(
                    '.QFrame{ background-color: rgb(255, 0, 0, 35); }')

    def setupDefaultFormUi(self, parent):
        """
        Optional UI setup that builds a standardized layout.
        Includes a form layout and a label with the attributes name.
        Should be called at the start of setupUi if desired.
        """
        layout = QtWidgets.QVBoxLayout(parent)
        layout.setContentsMargins(0, 0, 0, 0)

        self.frame = QtWidgets.QFrame(parent)
        layout.addWidget(self.frame)

        self.formLayout = QtWidgets.QFormLayout(self.frame)
        # margin that will give us some visible area of
        # the frame that can change color based on valid state
        self.formLayout.setContentsMargins(2, 2, 2, 2)
        self.formLayout.setFieldGrowthPolicy(
            QtWidgets.QFormLayout.ExpandingFieldsGrow)
        self.formLayout.setLabelAlignment(QtCore.Qt.AlignRight
                                          | QtCore.Qt.AlignTop
                                          | QtCore.Qt.AlignTrailing)
        self.formLayout.setHorizontalSpacing(10)

        # attribute name
        self.label = QtWidgets.QLabel(self.frame)
        self.label.setMinimumSize(
            QtCore.QSize(self.LABEL_WIDTH, self.LABEL_HEIGHT))
        self.label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignTrailing
                                | QtCore.Qt.AlignTop)
        # add some space above the label so it lines up
        self.label.setMargin(2)
        self.label.setText(pulse.names.toTitle(self.attr['name']))
        self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole,
                                  self.label)

    def setDefaultFormWidget(self, widget):
        """
        Set the widget to be used as the field in the default form layout
        Requires `setupDefaultFormUi` to be used.
        """
        self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, widget)

    def setDefaultFormLayout(self, layout):
        """
        Set the layout to be used as the field in the default form layout.
        Requires `setupDefaultFormUi` to be used.
        """
        self.formLayout.setLayout(0, QtWidgets.QFormLayout.FieldRole, layout)