Esempio n. 1
0
class RecordSignal(QtCore.QObject):
    """"""
    onSaved = QtCore.Signal(object)
    onSaving = QtCore.Signal(object)
    onLoaded = QtCore.Signal(object)
    onDeleted = QtCore.Signal(object)
    onDeleting = QtCore.Signal(object)
Esempio n. 2
0
class SettingsDialogSignal(QtCore.QObject):
    """
    """
    onNameChanged = QtCore.Signal(object)
    onPathChanged = QtCore.Signal(object)
    onColorChanged = QtCore.Signal(object)
    onBackgroundColorChanged = QtCore.Signal(object)
Esempio n. 3
0
class BaseWidget(QtWidgets.QWidget):
    """Base widget for creating and previewing transfer items."""

    stateChanged = QtCore.Signal(object)

    def __init__(self, item, parent=None):
        """
        Create a new widget for the given item.

        :type item: BaseItem
        :type parent: studiolibrary.LibraryWidget
        """
        QtWidgets.QWidget.__init__(self, parent)
        self.setObjectName("studioLibraryItemWidget")

        studioqt.loadUi(self)

        self._item = None
        self._iconPath = ""
        self._scriptJob = None

        self.setItem(item)
        self.loadSettings()

        try:
            self.selectionChanged()
            self.enableScriptJob()
        except NameError, msg:
            logger.exception(msg)

        self.updateThumbnailSize()
Esempio n. 4
0
class ThemesMenu(QtWidgets.QMenu):

    themeTriggered = QtCore.Signal(object)

    def __init__(self, parent=None, themes=None):
        """
        :type themes: list[Theme]
        :rtype: QtWidgets.QMenu
        """
        QtWidgets.QMenu.__init__(self, "Themes", parent)

        if not themes:
            themes = themePresets()

        for theme in themes:
            action = ThemeAction(theme, self)
            self.addAction(action)

        self.triggered.connect(self._themeTriggered)

    def _themeTriggered(self, action):
        """
        Triggered when a theme has been clicked.

        :type action: Action
        :rtype: None
        """
        if isinstance(action, ThemeAction):
            self.themeTriggered.emit(action.theme())
Esempio n. 5
0
class BaseWidget(QtWidgets.QWidget):

    stateChanged = QtCore.Signal(object)

    def __init__(self, record, parent=None):
        """
        :type record: Record
        :type parent: studiolibrary.LibraryWidget
        """
        QtWidgets.QWidget.__init__(self, parent)
        self.setObjectName("studioLibraryPluginsWidget")

        studioqt.loadUi(self)

        self._record = None
        self._iconPath = ""
        self._scriptJob = None

        self.setRecord(record)
        self.loadSettings()

        try:
            self.selectionChanged()
            self.enableScriptJob()
        except NameError, msg:
            logger.exception(msg)
Esempio n. 6
0
class BasePreviewWidget(QtWidgets.QWidget):
    """Base widget for creating and previewing transfer items."""

    stateChanged = QtCore.Signal(object)

    def __init__(self, item, parent=None):
        """
        :type parent: QtWidgets.QWidget
        """
        QtWidgets.QWidget.__init__(self, parent)
        self.setObjectName("studioLibraryMayaPreviewWidget")
        self.setWindowTitle("Preview Item")

        studioqt.loadUi(self)

        self._item = None
        self._iconPath = ""
        self._scriptJob = None

        self.setItem(item)
        self.loadSettings()

        try:
            self.selectionChanged()
            self.setScriptJobEnabled(True)
            self.updateNamespaceEdit()
        except NameError, msg:
            logger.exception(msg)

        path = self.item().thumbnailPath()
        if os.path.exists(path):
            self.setIconPath(path)

        self.updateThumbnailSize()
        self.setupConnections()
Esempio n. 7
0
class LibraryItemSignals(QtCore.QObject):
    """"""
    saved = QtCore.Signal(object)
    saving = QtCore.Signal(object)
    loaded = QtCore.Signal(object)
    copied = QtCore.Signal(object, object, object)
    deleted = QtCore.Signal(object)
    renamed = QtCore.Signal(object, object, object)
Esempio n. 8
0
class IconThread(QtCore.QThread):
    """A convenience class for loading an icon in a thread."""

    triggered = QtCore.Signal(object)

    def __init__(self, path, *args):
        QtCore.QThread.__init__(self, *args)

        self._path = path

    def run(self):
        """
        The starting point for the thread.

        :rtype: None 
        """
        if self._path:
            image = QtGui.QImage(unicode(self._path))
            self.triggered.emit(image)
Esempio n. 9
0
class InvokeRepeatingThread(QtCore.QThread):
    """
    A convenience class for invoking a method to the given repeat rate.
    """

    triggered = QtCore.Signal()

    def __init__(self, repeatRate, *args):
        QtCore.QThread.__init__(self, *args)

        self._repeatRate = repeatRate

    def run(self):
        """
        The starting point for the thread.
        
        :rtype: None 
        """
        while True:
            QtCore.QThread.sleep(self._repeatRate)
            self.triggered.emit()
Esempio n. 10
0
class ThemesWidget(QtWidgets.QWidget):
    themeClicked = QtCore.Signal(object)

    def __init__(self, parent = None, themes = None):
        """
        :type parent: QtWidgets.QWidget
        :type themes: list[Theme]
        """
        QtWidgets.QWidget.__init__(self, parent)
        if not themes:
            themes = themePresets()
        layout = QtWidgets.QHBoxLayout(self)
        layout.setSpacing(0)
        layout.setContentsMargins(0, 0, 0, 0)
        self.setLayout(layout)
        for theme in themes:
            color = theme.accentColor().toString()
            themeWidget = QtWidgets.QPushButton(self)
            themeWidget.setStyleSheet('background-color: ' + color)
            callback = partial(self.themeClicked.emit, theme)
            themeWidget.clicked.connect(callback)
            layout.addWidget(themeWidget)
class ThumbnailCaptureDialog(QtWidgets.QDialog):

    DEFAULT_WIDTH = 250
    DEFAULT_HEIGHT = 250

    captured = QtCore.Signal(str)
    capturing = QtCore.Signal(str)

    def __init__(self,
                 path="",
                 parent=None,
                 startFrame=None,
                 endFrame=None,
                 step=1):
        """
        :type path: str
        :type parent: QtWidgets.QWidget
        :type startFrame: int
        :type endFrame:  int
        :type step: int
        """
        parent = parent or mutils.gui.mayaWindow()

        QtWidgets.QDialog.__init__(self, parent)

        self._path = path
        self._step = step
        self._endFrame = None
        self._startFrame = None
        self._capturedPath = None

        if endFrame is None:
            endFrame = int(maya.cmds.currentTime(query=True))

        if startFrame is None:
            startFrame = int(maya.cmds.currentTime(query=True))

        self.setEndFrame(endFrame)
        self.setStartFrame(startFrame)

        self.setObjectName("CaptureWindow")
        self.setWindowTitle("Capture Window")

        self._captureButton = QtWidgets.QPushButton("Capture")
        self._captureButton.clicked.connect(self.capture)

        self._modelPanelWidget = mutils.gui.modelpanelwidget.ModelPanelWidget(
            self)

        vbox = QtWidgets.QVBoxLayout(self)
        vbox.setObjectName(self.objectName() + "Layout")
        vbox.addWidget(self._modelPanelWidget)
        vbox.addWidget(self._captureButton)

        self.setLayout(vbox)

        width = (self.DEFAULT_WIDTH * 1.5)
        height = (self.DEFAULT_HEIGHT * 1.5) + 50

        self.setWidthHeight(width, height)
        self.centerWindow()

    def path(self):
        """
        Return the destination path.

        :rtype: str
        """
        return self._path

    def setPath(self, path):
        """
        Set the destination path.

        :type path: str
        """
        self._path = path

    def endFrame(self):
        """
        Return the end frame of the playblast.

        :rtype: int
        """
        return self._endFrame

    def setEndFrame(self, endFrame):
        """
        Specify the end frame of the playblast.

        :type endFrame: int
        """
        self._endFrame = int(endFrame)

    def startFrame(self):
        """
        Return the start frame of the playblast.

        :rtype: int
        """
        return self._startFrame

    def setStartFrame(self, startFrame):
        """
        Specify the start frame of the playblast.

        :type startFrame: int
        """
        self._startFrame = int(startFrame)

    def step(self):
        """
        Return the step amount of the playblast.

        Example:
            if step is set to 2 it will playblast every second frame.

        :rtype: int
        """
        return self._step

    def setStep(self, step):
        """
        Set the step amount of the playblast.

        :type step: int
        """
        self._step = step

    def setWidthHeight(self, width, height):
        """
        Set the width and height of the window.

        :type width: int
        :type height: int
        :rtype: None
        """
        x = self.geometry().x()
        y = self.geometry().y()
        self.setGeometry(x, y, width, height)

    def centerWindow(self):
        """
        Center the widget to the center of the desktop.

        :rtype: None
        """
        geometry = self.frameGeometry()
        pos = QtWidgets.QApplication.desktop().cursor().pos()
        screen = QtWidgets.QApplication.desktop().screenNumber(pos)
        centerPoint = QtWidgets.QApplication.desktop().screenGeometry(
            screen).center()
        geometry.moveCenter(centerPoint)
        self.move(geometry.topLeft())

    def capturedPath(self):
        """
        Return the location of the captured playblast.

        :rtype:
        """
        return self._capturedPath

    def capture(self):
        """
        Capture a playblast and save it to the given path.

        :rtype: None
        """
        path = self.path()

        self.capturing.emit(path)

        modelPanel = self._modelPanelWidget.name()
        startFrame = self.startFrame()
        endFrame = self.endFrame()
        step = self.step()
        width = self.DEFAULT_WIDTH
        height = self.DEFAULT_HEIGHT

        self._capturedPath = mutils.playblast.playblast(
            path,
            modelPanel,
            startFrame,
            endFrame,
            width,
            height,
            step=step,
        )

        self.accept()

        self.captured.emit(self._capturedPath)
        return self._capturedPath
Esempio n. 12
0
class PoseItemSignals(QtCore.QObject):
    """Signals need to be attached to a QObject"""
    mirrorChanged = QtCore.Signal(bool)
Esempio n. 13
0
class PoseItemSignals(QtCore.QObject):
    """"""
    mirrorChanged = QtCore.Signal(bool)
Esempio n. 14
0
class WorkerSignals(QtCore.QObject):
    triggered = QtCore.Signal(object)
Esempio n. 15
0
class LibraryItemSignals(QtCore.QObject):
    """"""
    saved = QtCore.Signal(object)
    saving = QtCore.Signal(object)
    loaded = QtCore.Signal(object)
    renamed = QtCore.Signal(str, str)
Esempio n. 16
0
class ControlWidget(QtWidgets.QWidget):

    controlChanged = QtCore.Signal(str)

    def __init__(self, *args):
        QtWidgets.QWidget.__init__(self, *args)

        layout = QtWidgets.QHBoxLayout(self)
        self.setLayout(layout)

        layout.setContentsMargins(0,0,0,0)
        layout.setSpacing(0)

        self._dataset = None
        self._iconPadding = 6
        self._iconButton = QtWidgets.QPushButton(self)
        self._iconButton.clicked.connect(self._onClicked)
        layout.addWidget(self._iconButton)

        icon = studiolibrary.resource().icon("pokeball")
        self._iconButton.setIcon(icon)

        self._comboBox = QtWidgets.QComboBox(self)
        layout.addWidget(self._comboBox, 1)

        self._comboBox.addItem("Select a character", "")
        self._comboBox.installEventFilter(self)
        self._comboBox.activated.connect(self._onActivated)

        self._comboBox.setSizePolicy( QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
        self.setSizePolicy( QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)

        self.update()

    def update(self):
        self.updateIconColor()


    def setDataset(self, dataset):
        """
        Set the data set for the search widget:
        
        :type dataset: studiolibrary.Dataset
        """
        self._dataset = dataset


    def dataset(self):
        """
        Get the data set for the search widget.
        
        :rtype: studiolibrary.Dataset 
        """
        return self._dataset


    def updateIconColor(self):
        """
        Update the icon colors to the current foregroundRole.

        :rtype: None
        """
        
        color = self.palette().color(self._iconButton.foregroundRole())
        color = studioqt.Color.fromColor(color)

        icon = self._iconButton.icon()
        icon = studioqt.Icon(icon)
        icon.setColor(color)
        self._iconButton.setIcon(icon)



    def resizeEvent(self, event):
        """
        Reimplemented so the icon maintains the same height as the widget.

        :type event:  QtWidgets.QResizeEvent
        :rtype: None
        """
        QtWidgets.QWidget.resizeEvent(self, event)
        size = QtCore.QSize(self.height(), self.height())
        self._iconButton.setIconSize(size)
        self._iconButton.setFixedSize(size)


    def eventFilter(self, source, event):
        if (event.type() == QtCore.QEvent.MouseButtonPress and
            source is self._comboBox):

            #populate the list
            self._comboBox.clear()
            self._comboBox.addItem("Select a character", "")
            for rig in self.dataset().findRigsNamespacesInScene():
                self._comboBox.addItem(rig, rig)

        return super(ControlWidget, self).eventFilter(source, event)


    def _onClicked(self):
        self._comboBox.setFocus()

    def _onActivated(self, id=None):

        userdata = str( self._comboBox.itemData(id) ) 
        self.dataset().setActiveCharacter(userdata)
        self.controlChanged.emit(userdata)
Esempio n. 17
0
class GlobalSignals(QtCore.QObject):
    """"""
    blendChanged = QtCore.Signal(float)
Esempio n. 18
0
class BasePreviewWidget(QtWidgets.QWidget):

    """Base widget for creating and previewing transfer items."""

    stateChanged = QtCore.Signal(object)

    def __init__(self, item, parent=None):
        """
        :type parent: QtWidgets.QWidget
        """
        QtWidgets.QWidget.__init__(self, parent)
        self.setObjectName("studioLibraryMayaPreviewWidget")
        self.setWindowTitle("Preview Item")

        studioqt.loadUi(self)

        self._item = None
        self._iconPath = ""
        self._scriptJob = None

        self.setItem(item)
        self.loadSettings()

        try:
            self.selectionChanged()
            self.setScriptJobEnabled(True)
            self.updateNamespaceEdit()
        except NameError as error:
            logger.exception(error)

        path = self.item().thumbnailPath()
        if os.path.exists(path):
            self.setIconPath(path)

        self.updateThumbnailSize()
        self.setupConnections()

    def setupConnections(self):
        """
        :rtype: None
        """
        self.ui.acceptButton.clicked.connect(self.accept)
        self.ui.selectionSetButton.clicked.connect(self.showSelectionSetsMenu)

        self.ui.useFileNamespace.clicked.connect(self._namespaceOptionClicked)
        self.ui.useCustomNamespace.clicked.connect(self._useCustomNamespaceClicked)
        self.ui.useSelectionNamespace.clicked.connect(self._namespaceOptionClicked)

        self.ui.namespaceComboBox.activated[str].connect(self._namespaceEditChanged)
        self.ui.namespaceComboBox.editTextChanged[str].connect(self._namespaceEditChanged)
        self.ui.namespaceComboBox.currentIndexChanged[str].connect(self._namespaceEditChanged)

        self.ui.iconToggleBoxButton.clicked.connect(self.saveSettings)
        self.ui.infoToggleBoxButton.clicked.connect(self.saveSettings)
        self.ui.optionsToggleBoxButton.clicked.connect(self.saveSettings)
        self.ui.namespaceToggleBoxButton.clicked.connect(self.saveSettings)

        self.ui.iconToggleBoxButton.toggled[bool].connect(self.ui.iconToggleBoxFrame.setVisible)
        self.ui.infoToggleBoxButton.toggled[bool].connect(self.ui.infoToggleBoxFrame.setVisible)
        self.ui.optionsToggleBoxButton.toggled[bool].connect(self.ui.optionsToggleBoxFrame.setVisible)
        self.ui.namespaceToggleBoxButton.toggled[bool].connect(self.ui.namespaceToggleBoxFrame.setVisible)

    def isEditable(self):
        """
        Return True if the user can edit the item.

        :rtype: bool 
        """
        item = self.item()
        editable = True

        if item and item.libraryWindow():
            editable = not item.libraryWindow().isLocked()

        return editable

    def setCaptureMenuEnabled(self, enable):
        """
        Enable the capture menu for editing the thumbnail.

        :rtype: None 
        """
        if enable:
            parent = self.item().libraryWindow()

            iconPath = self.iconPath()
            if iconPath == "":
                iconPath = self.item().thumbnailPath()

            menu = mutils.gui.ThumbnailCaptureMenu(iconPath, parent=parent)
            menu.captured.connect(self.setIconPath)
            self.ui.thumbnailButton.setMenu(menu)
        else:
            self.ui.thumbnailButton.setMenu(QtWidgets.QMenu(self))

    def item(self):
        """
        Return the library item to be created.

        :rtype: studiolibrarymaya.BaseItem
        """
        return self._item

    def setItem(self, item):
        """
        Set the item for the preview widget.

        :type item: BaseItem
        """
        self._item = item

        self.ui.name.setText(item.name())
        self.ui.owner.setText(item.owner())
        self.ui.comment.setText(item.description())

        self.updateContains()

        ctime = item.ctime()
        if ctime:
            self.ui.created.setText(studiolibrary.timeAgo(ctime))

    def iconPath(self):
        """
        Return the icon path to be used for the thumbnail.

        :rtype str
        """
        return self._iconPath

    def setIconPath(self, path):
        """
        Set the icon path to be used for the thumbnail.

        :type path: str
        :rtype: None
        """
        self._iconPath = path
        icon = QtGui.QIcon(QtGui.QPixmap(path))
        self.setIcon(icon)
        self.updateThumbnailSize()
        self.item().update()

    def setIcon(self, icon):
        """
        Set the icon to be shown for the preview.

        :type icon: QtGui.QIcon
        """
        self.ui.thumbnailButton.setIcon(icon)
        self.ui.thumbnailButton.setIconSize(QtCore.QSize(200, 200))
        self.ui.thumbnailButton.setText("")

    def showSelectionSetsMenu(self):
        """
        :rtype: None
        """
        item = self.item()
        item.showSelectionSetsMenu()

    def resizeEvent(self, event):
        """
        Overriding to adjust the image size when the widget changes size.

        :type event: QtCore.QSizeEvent
        """
        self.updateThumbnailSize()

    def updateThumbnailSize(self):
        """
        Update the thumbnail button to the size of the widget.

        :rtype: None
        """
        if hasattr(self.ui, "thumbnailButton"):
            width = self.width() - 10
            if width > 250:
                width = 250

            size = QtCore.QSize(width, width)
            self.ui.thumbnailButton.setIconSize(size)
            self.ui.thumbnailButton.setMaximumSize(size)
            self.ui.thumbnailFrame.setMaximumSize(size)

    def close(self):
        """
        Overriding the close method so that we can disable the script job.

        :rtype: None
        """
        self.setScriptJobEnabled(False)
        QtWidgets.QWidget.close(self)

    def scriptJob(self):
        """
        Return the script job object used when the users selection changes.

        :rtype: mutils.ScriptJob
        """
        return self._scriptJob

    def setScriptJobEnabled(self, enable):
        """
        Enable the script job used when the users selection changes.

        :rtype: None
        """
        if enable:
            if not self._scriptJob:
                event = ['SelectionChanged', self.selectionChanged]
                self._scriptJob = mutils.ScriptJob(event=event)
        else:
            sj = self.scriptJob()
            if sj:
                sj.kill()
            self._scriptJob = None

    def objectCount(self):
        """
        Return the number of controls contained in the item.

        :rtype: int
        """
        return self.item().objectCount()

    def updateContains(self):
        """
        Refresh the contains information.

        :rtype: None
        """
        count = self.objectCount()
        plural = "s" if count > 1 else ""
        self.ui.contains.setText(str(count) + " Object" + plural)

    def _namespaceEditChanged(self, text):
        """
        Triggered when the combox box has changed value.

        :type text: str
        :rtype: None
        """
        self.ui.useCustomNamespace.setChecked(True)
        self.ui.namespaceComboBox.setEditText(text)
        self.saveSettings()

    def _namespaceOptionClicked(self):
        self.updateNamespaceEdit()
        self.saveSettings()

    def _useCustomNamespaceClicked(self):
        """
        Triggered when the custom namespace radio button is clicked.

        :rtype: None
        """
        self.ui.namespaceComboBox.setFocus()
        self.updateNamespaceEdit()
        self.saveSettings()

    def namespaces(self):
        """
        Return the namespace names from the namespace edit widget.

        :rtype: list[str]
        """
        namespaces = str(self.ui.namespaceComboBox.currentText())
        namespaces = studiolibrary.stringToList(namespaces)
        return namespaces

    def setNamespaces(self, namespaces):
        """
        Set the namespace names for the namespace edit.

        :type namespaces: list
        :rtype: None
        """
        namespaces = studiolibrary.listToString(namespaces)
        self.ui.namespaceComboBox.setEditText(namespaces)

    def namespaceOption(self):
        """
        :rtype: NamespaceOption
        """
        if self.ui.useFileNamespace.isChecked():
            namespaceOption = NamespaceOption.FromFile
        elif self.ui.useCustomNamespace.isChecked():
            namespaceOption = NamespaceOption.FromCustom
        else:
            namespaceOption = NamespaceOption.FromSelection

        return namespaceOption

    def setNamespaceOption(self, namespaceOption):
        """
        :type namespaceOption: NamespaceOption
        """
        if namespaceOption == NamespaceOption.FromFile:
            self.ui.useFileNamespace.setChecked(True)
        elif namespaceOption == NamespaceOption.FromCustom:
            self.ui.useCustomNamespace.setChecked(True)
        else:
            self.ui.useSelectionNamespace.setChecked(True)

    def setSettings(self, settings):
        """
        :type settings: dict
        """
        namespaces = settings.get("namespaces", [])
        self.setNamespaces(namespaces)

        namespaceOption = settings.get("namespaceOption", NamespaceOption.FromFile)
        self.setNamespaceOption(namespaceOption)

        toggleBoxChecked = settings.get("iconToggleBoxChecked", True)
        self.ui.iconToggleBoxFrame.setVisible(toggleBoxChecked)
        self.ui.iconToggleBoxButton.setChecked(toggleBoxChecked)

        toggleBoxChecked = settings.get("infoToggleBoxChecked", True)
        self.ui.infoToggleBoxFrame.setVisible(toggleBoxChecked)
        self.ui.infoToggleBoxButton.setChecked(toggleBoxChecked)

        toggleBoxChecked = settings.get("optionsToggleBoxChecked", True)
        self.ui.optionsToggleBoxFrame.setVisible(toggleBoxChecked)
        self.ui.optionsToggleBoxButton.setChecked(toggleBoxChecked)

        toggleBoxChecked = settings.get("namespaceToggleBoxChecked", True)
        self.ui.namespaceToggleBoxFrame.setVisible(toggleBoxChecked)
        self.ui.namespaceToggleBoxButton.setChecked(toggleBoxChecked)

    def settings(self):
        """
        :rtype: dict
        """
        settings = {}

        settings["namespaces"] = self.namespaces()
        settings["namespaceOption"] = self.namespaceOption()

        settings["iconToggleBoxChecked"] = self.ui.iconToggleBoxButton.isChecked()
        settings["infoToggleBoxChecked"] = self.ui.infoToggleBoxButton.isChecked()
        settings["optionsToggleBoxChecked"] = self.ui.optionsToggleBoxButton.isChecked()
        settings["namespaceToggleBoxChecked"] = self.ui.namespaceToggleBoxButton.isChecked()

        return settings

    def loadSettings(self):
        """
        :rtype: None
        """
        data = studiolibrarymaya.settings()
        self.setSettings(data)

    def saveSettings(self):
        """
        :rtype: None
        """
        data = self.settings()
        studiolibrarymaya.saveSettings(data)

    def selectionChanged(self):
        """
        :rtype: None
        """
        self.updateNamespaceEdit()

    def updateNamespaceFromScene(self):
        """
        Update the namespaces in the combobox with the ones in the scene.

        :rtype: None
        """
        IGNORE_NAMESPACES = ['UI', 'shared']

        if studiolibrary.isMaya():
            namespaces = maya.cmds.namespaceInfo(listOnlyNamespaces=True)
        else:
            namespaces = []

        namespaces = list(set(namespaces) - set(IGNORE_NAMESPACES))
        namespaces = sorted(namespaces)

        text = self.ui.namespaceComboBox.currentText()

        if namespaces:
            self.ui.namespaceComboBox.setToolTip("")
        else:
            toolTip = "No namespaces found in scene."
            self.ui.namespaceComboBox.setToolTip(toolTip)

        self.ui.namespaceComboBox.clear()
        self.ui.namespaceComboBox.addItems(namespaces)
        self.ui.namespaceComboBox.setEditText(text)

    def updateNamespaceEdit(self):
        """
        :rtype: None
        """
        logger.debug('Updating namespace edit')

        self.ui.namespaceComboBox.blockSignals(True)

        self.updateNamespaceFromScene()

        namespaces = []

        if self.ui.useSelectionNamespace.isChecked():
            namespaces = mutils.namespace.getFromSelection()
        elif self.ui.useFileNamespace.isChecked():
            namespaces = self.item().transferObject().namespaces()

        if not self.ui.useCustomNamespace.isChecked():
            self.setNamespaces(namespaces)

            # Removes focus from the combobox
            self.ui.namespaceComboBox.setEnabled(False)
            self.ui.namespaceComboBox.setEnabled(True)

        self.ui.namespaceComboBox.blockSignals(False)

    def accept(self):
        """
        :rtype: None
        """
        try:
            self.item().load()
        except Exception as error:
            self.item().showErrorDialog("Error while loading", str(error))
            raise
Esempio n. 19
0
class BaseLoadWidget(QtWidgets.QWidget):
    """Base widget for creating and previewing transfer items."""

    stateChanged = QtCore.Signal(object)

    def __init__(self, item, parent=None):
        """
        :type parent: QtWidgets.QWidget
        """
        QtWidgets.QWidget.__init__(self, parent)
        self.setObjectName("studioLibraryMayaPreviewWidget")
        self.setWindowTitle("Preview Item")

        studioqt.loadUi(self)

        self._item = None
        self._iconPath = ""
        self._scriptJob = None
        self._optionsWidget = None

        self.setItem(item)
        self.loadSettings()

        try:
            self.selectionChanged()
            self.setScriptJobEnabled(True)
            self.updateNamespaceEdit()
        except NameError as error:
            logger.exception(error)

        self.createSequenceWidget()
        self.updateThumbnailSize()
        self.setupConnections()

    def setupConnections(self):
        """Setup the connections for all the widgets."""
        self.ui.acceptButton.clicked.connect(self.accept)
        self.ui.selectionSetButton.clicked.connect(self.showSelectionSetsMenu)

        self.ui.useFileNamespace.clicked.connect(self._namespaceOptionClicked)
        self.ui.useCustomNamespace.clicked.connect(
            self._useCustomNamespaceClicked)
        self.ui.useSelectionNamespace.clicked.connect(
            self._namespaceOptionClicked)

        self.ui.namespaceComboBox.activated[str].connect(
            self._namespaceEditChanged)
        self.ui.namespaceComboBox.editTextChanged[str].connect(
            self._namespaceEditChanged)
        self.ui.namespaceComboBox.currentIndexChanged[str].connect(
            self._namespaceEditChanged)

        self.ui.iconToggleBoxButton.clicked.connect(self.saveSettings)
        self.ui.infoToggleBoxButton.clicked.connect(self.saveSettings)
        self.ui.optionsToggleBoxButton.clicked.connect(self.saveSettings)
        self.ui.namespaceToggleBoxButton.clicked.connect(self.saveSettings)

        self.ui.iconToggleBoxButton.toggled[bool].connect(
            self.ui.iconToggleBoxFrame.setVisible)
        self.ui.infoToggleBoxButton.toggled[bool].connect(
            self.ui.infoToggleBoxFrame.setVisible)
        self.ui.optionsToggleBoxButton.toggled[bool].connect(
            self.ui.optionsToggleBoxFrame.setVisible)
        self.ui.namespaceToggleBoxButton.toggled[bool].connect(
            self.ui.namespaceToggleBoxFrame.setVisible)

    def createSequenceWidget(self):
        """
        Create a sequence widget to replace the static thumbnail widget.

        :rtype: None
        """
        self.ui.sequenceWidget = studiolibrary.widgets.ImageSequenceWidget(
            self)
        self.ui.sequenceWidget.setStyleSheet(
            self.ui.thumbnailButton.styleSheet())
        self.ui.sequenceWidget.setToolTip(self.ui.thumbnailButton.toolTip())

        self.ui.thumbnailFrame.layout().insertWidget(0, self.ui.sequenceWidget)
        self.ui.thumbnailButton.hide()
        self.ui.thumbnailButton = self.ui.sequenceWidget

        path = self.item().thumbnailPath()
        if os.path.exists(path):
            self.setIconPath(path)

        if self.item().imageSequencePath():
            self.ui.sequenceWidget.setDirname(self.item().imageSequencePath())

    def isEditable(self):
        """
        Return True if the user can edit the item.

        :rtype: bool 
        """
        item = self.item()
        editable = True

        if item and item.libraryWindow():
            editable = not item.libraryWindow().isLocked()

        return editable

    def setCaptureMenuEnabled(self, enable):
        """
        Enable the capture menu for editing the thumbnail.

        :rtype: None 
        """
        if enable:
            parent = self.item().libraryWindow()

            iconPath = self.iconPath()
            if iconPath == "":
                iconPath = self.item().thumbnailPath()

            menu = mutils.gui.ThumbnailCaptureMenu(iconPath, parent=parent)
            menu.captured.connect(self.setIconPath)
            self.ui.thumbnailButton.setMenu(menu)
        else:
            self.ui.thumbnailButton.setMenu(QtWidgets.QMenu(self))

    def item(self):
        """
        Return the library item to be created.

        :rtype: studiolibrarymaya.BaseItem
        """
        return self._item

    def _itemValueChanged(self, field, value):
        """
        :type field: str
        :type value: object
        """
        self._optionsWidget.setValue(field, value)

    def setItem(self, item):
        """
        Set the item for the preview widget.

        :type item: BaseItem
        """
        self._item = item

        if hasattr(self.ui, "titleLabel"):
            self.ui.titleLabel.setText(item.MenuName)

        if hasattr(self.ui, "iconLabel"):
            self.ui.iconLabel.setPixmap(QtGui.QPixmap(item.TypeIconPath))

        if hasattr(self.ui, "infoFrame"):
            infoWidget = studiolibrary.widgets.FormWidget(self)
            infoWidget.setSchema(item.info())
            self.ui.infoFrame.layout().addWidget(infoWidget)

        if hasattr(self.ui, "optionsFrame"):

            options = item.loadSchema()
            if options:
                item.loadValueChanged.connect(self._itemValueChanged)

                optionsWidget = studiolibrary.widgets.FormWidget(self)
                optionsWidget.setSchema(item.loadSchema())
                optionsWidget.setValidator(item.loadValidator)
                optionsWidget.setStateFromOptions(
                    self.item().optionsFromSettings())
                optionsWidget.stateChanged.connect(self.optionsChanged)
                self.ui.optionsFrame.layout().addWidget(optionsWidget)
                self._optionsWidget = optionsWidget
                optionsWidget.validate()
            else:
                self.ui.optionsToggleBox.setVisible(False)

    def optionsChanged(self):
        self.item().optionsChanged(**self._optionsWidget.optionsToDict())

    def iconPath(self):
        """
        Return the icon path to be used for the thumbnail.

        :rtype str
        """
        return self._iconPath

    def setIconPath(self, path):
        """
        Set the icon path to be used for the thumbnail.

        :type path: str
        :rtype: None
        """
        self._iconPath = path
        icon = QtGui.QIcon(QtGui.QPixmap(path))
        self.setIcon(icon)
        self.updateThumbnailSize()
        self.item().update()

    def setIcon(self, icon):
        """
        Set the icon to be shown for the preview.

        :type icon: QtGui.QIcon
        """
        self.ui.thumbnailButton.setIcon(icon)
        self.ui.thumbnailButton.setIconSize(QtCore.QSize(200, 200))
        self.ui.thumbnailButton.setText("")

    def showSelectionSetsMenu(self):
        """Show the selection sets menu."""
        item = self.item()
        item.showSelectionSetsMenu()

    def resizeEvent(self, event):
        """
        Overriding to adjust the image size when the widget changes size.

        :type event: QtCore.QSizeEvent
        """
        self.updateThumbnailSize()

    def updateThumbnailSize(self):
        """
        Update the thumbnail button to the size of the widget.

        :rtype: None
        """
        if hasattr(self.ui, "thumbnailButton"):
            width = self.width() - 10
            if width > 250:
                width = 250

            size = QtCore.QSize(width, width)
            self.ui.thumbnailButton.setIconSize(size)
            self.ui.thumbnailButton.setMaximumSize(size)
            self.ui.thumbnailFrame.setMaximumSize(size)

    def close(self):
        """
        Overriding the close method so that we can disable the script job.

        :rtype: None
        """
        self.setScriptJobEnabled(False)
        QtWidgets.QWidget.close(self)

    def scriptJob(self):
        """
        Get the script job object used when the users selection changes.

        :rtype: mutils.ScriptJob
        """
        return self._scriptJob

    def setScriptJobEnabled(self, enable):
        """
        Enable the script job used when the users selection changes.

        :rtype: None
        """
        if enable:
            if not self._scriptJob:
                event = ['SelectionChanged', self.selectionChanged]
                self._scriptJob = mutils.ScriptJob(event=event)
        else:
            sj = self.scriptJob()
            if sj:
                sj.kill()
            self._scriptJob = None

    def objectCount(self):
        """
        Return the number of controls contained in the item.

        :rtype: int
        """
        return self.item().objectCount()

    def _namespaceEditChanged(self, text):
        """
        Triggered when the combox box has changed value.

        :type text: str
        :rtype: None
        """
        self.ui.useCustomNamespace.setChecked(True)
        self.ui.namespaceComboBox.setEditText(text)
        self.saveSettings()

    def _namespaceOptionClicked(self):
        self.updateNamespaceEdit()
        self.saveSettings()

    def _useCustomNamespaceClicked(self):
        """
        Triggered when the custom namespace radio button is clicked.

        :rtype: None
        """
        self.ui.namespaceComboBox.setFocus()
        self.updateNamespaceEdit()
        self.saveSettings()

    def namespaces(self):
        """
        Return the namespace names from the namespace edit widget.

        :rtype: list[str]
        """
        namespaces = str(self.ui.namespaceComboBox.currentText())
        namespaces = studiolibrary.stringToList(namespaces)
        return namespaces

    def setNamespaces(self, namespaces):
        """
        Set the namespace names for the namespace edit.

        :type namespaces: list
        :rtype: None
        """
        namespaces = studiolibrary.listToString(namespaces)
        self.ui.namespaceComboBox.setEditText(namespaces)

    def namespaceOption(self):
        """
        Get the current namespace option.

        :rtype: NamespaceOption
        """
        if self.ui.useFileNamespace.isChecked():
            namespaceOption = NamespaceOption.FromFile
        elif self.ui.useCustomNamespace.isChecked():
            namespaceOption = NamespaceOption.FromCustom
        else:
            namespaceOption = NamespaceOption.FromSelection

        return namespaceOption

    def setNamespaceOption(self, namespaceOption):
        """
        Set the current namespace option.

        :type namespaceOption: NamespaceOption
        """
        if namespaceOption == NamespaceOption.FromFile:
            self.ui.useFileNamespace.setChecked(True)
        elif namespaceOption == NamespaceOption.FromCustom:
            self.ui.useCustomNamespace.setChecked(True)
        else:
            self.ui.useSelectionNamespace.setChecked(True)

    def setSettings(self, settings):
        """
        Set the state of the widget.

        :type settings: dict
        """
        namespaces = settings.get("namespaces", [])
        self.setNamespaces(namespaces)

        namespaceOption = settings.get("namespaceOption",
                                       NamespaceOption.FromFile)
        self.setNamespaceOption(namespaceOption)

        toggleBoxChecked = settings.get("iconToggleBoxChecked", True)
        self.ui.iconToggleBoxFrame.setVisible(toggleBoxChecked)
        self.ui.iconToggleBoxButton.setChecked(toggleBoxChecked)

        toggleBoxChecked = settings.get("infoToggleBoxChecked", True)
        self.ui.infoToggleBoxFrame.setVisible(toggleBoxChecked)
        self.ui.infoToggleBoxButton.setChecked(toggleBoxChecked)

        toggleBoxChecked = settings.get("optionsToggleBoxChecked", True)
        self.ui.optionsToggleBoxFrame.setVisible(toggleBoxChecked)
        self.ui.optionsToggleBoxButton.setChecked(toggleBoxChecked)

        toggleBoxChecked = settings.get("namespaceToggleBoxChecked", True)
        self.ui.namespaceToggleBoxFrame.setVisible(toggleBoxChecked)
        self.ui.namespaceToggleBoxButton.setChecked(toggleBoxChecked)

    def settings(self):
        """
        Get the current state of the widget.

        :rtype: dict
        """
        settings = {}

        settings["namespaces"] = self.namespaces()
        settings["namespaceOption"] = self.namespaceOption()

        settings[
            "iconToggleBoxChecked"] = self.ui.iconToggleBoxButton.isChecked()
        settings[
            "infoToggleBoxChecked"] = self.ui.infoToggleBoxButton.isChecked()
        settings[
            "optionsToggleBoxChecked"] = self.ui.optionsToggleBoxButton.isChecked(
            )
        settings[
            "namespaceToggleBoxChecked"] = self.ui.namespaceToggleBoxButton.isChecked(
            )

        return settings

    def loadSettings(self):
        """
        Load the user settings from disc.

        :rtype: None
        """
        data = studiolibrarymaya.settings()
        self.setSettings(data)

    def saveSettings(self):
        """
        Save the user settings to disc.

        :rtype: None
        """
        data = self.settings()
        studiolibrarymaya.saveSettings(data)

    def selectionChanged(self):
        """
        Triggered when the users Maya selection has changed.

        :rtype: None
        """
        self.updateNamespaceEdit()

    def updateNamespaceFromScene(self):
        """
        Update the namespaces in the combobox with the ones in the scene.

        :rtype: None
        """
        namespaces = mutils.namespace.getAll()

        text = self.ui.namespaceComboBox.currentText()

        if namespaces:
            self.ui.namespaceComboBox.setToolTip("")
        else:
            toolTip = "No namespaces found in scene."
            self.ui.namespaceComboBox.setToolTip(toolTip)

        self.ui.namespaceComboBox.clear()
        self.ui.namespaceComboBox.addItems(namespaces)
        self.ui.namespaceComboBox.setEditText(text)

    def updateNamespaceEdit(self):
        """
        Update the namespace edit.

        :rtype: None
        """
        logger.debug('Updating namespace edit')

        self.ui.namespaceComboBox.blockSignals(True)

        self.updateNamespaceFromScene()

        namespaces = []

        if self.ui.useSelectionNamespace.isChecked():
            namespaces = mutils.namespace.getFromSelection()
        elif self.ui.useFileNamespace.isChecked():
            namespaces = self.item().transferObject().namespaces()

        if not self.ui.useCustomNamespace.isChecked():
            self.setNamespaces(namespaces)

            # Removes focus from the combobox
            self.ui.namespaceComboBox.setEnabled(False)
            self.ui.namespaceComboBox.setEnabled(True)

        self.ui.namespaceComboBox.blockSignals(False)

    def accept(self):
        """
        Called when the user clicks the apply button.

        :rtype: None
        """
        self.item().loadFromCurrentOptions()
class Theme(QtCore.QObject):

    updated = QtCore.Signal()

    DEFAULT_DARK_COLOR = QtGui.QColor(60, 60, 60)
    DEFAULT_LIGHT_COLOR = QtGui.QColor(220, 220, 220)

    DEFAULT_ACCENT_COLOR = QtGui.QColor(0, 175, 255)
    DEFAULT_BACKGROUND_COLOR = QtGui.QColor(60, 60, 80)

    def __init__(self):
        QtCore.QObject.__init__(self)

        self._dpi = 1

        self._name = "Default"
        self._accentColor = None
        self._backgroundColor = None

        self.setAccentColor(self.DEFAULT_ACCENT_COLOR)
        self.setBackgroundColor(self.DEFAULT_BACKGROUND_COLOR)

    def settings(self):
        """
        Return a dictionary of settings for the current Theme.

        :rtype: dict
        """
        settings = {}

        settings["name"] = self.name()

        accentColor = self.accentColor()
        settings["accentColor"] = accentColor.toString()

        backgroundColor = self.backgroundColor()
        settings["backgroundColor"] = backgroundColor.toString()

        return settings

    def setSettings(self, settings):
        """
        Set a dictionary of settings for the current Theme.

        :type settings: dict
        :rtype: None
        """
        name = settings.get("name")
        self.setName(name)

        color = settings.get("accentColor")
        if color:
            color = studioqt.Color.fromString(color)
            self.setAccentColor(color)

        color = settings.get("backgroundColor")
        if color:
            color = studioqt.Color.fromString(color)
            self.setBackgroundColor(color)

    def dpi(self):
        """
        Return the dpi for the Theme

        :rtype: float
        """
        return self._dpi

    def setDpi(self, dpi):
        """
        Set the dpi for the Theme

        :type dpi: float
        :rtype: None
        """
        self._dpi = dpi

    def name(self):
        """
        Return the name for the Theme

        :rtype: str
        """
        return self._name

    def setName(self, name):
        """
        Set the name for the Theme

        :type name: str
        :rtype: None
        """
        self._name = name

    def isDark(self):
        """
        Return True if the current theme is dark.

        rtype: bool
        """
        # The luminance for digital formats are (0.299, 0.587, 0.114)
        red = self.backgroundColor().redF() * 0.299
        green = self.backgroundColor().greenF() * 0.587
        blue = self.backgroundColor().blueF() * 0.114

        darkness = red + green + blue

        if darkness < 0.6:
            return True

        return False

    def setDark(self):
        """
        Set the current theme to the default dark color.
        
        :rtype: None 
        """
        self.setBackgroundColor(self.DEFAULT_DARK_COLOR)

    def setLight(self):
        """
        Set the current theme to the default light color.

        :rtype: None 
        """
        self.setBackgroundColor(self.DEFAULT_LIGHT_COLOR)

    def iconColor(self):
        """
        Return the icon color for the theme.

        :rtype: studioqt.Color 
        """
        return self.forgroundColor()

    def accentForgroundColor(self):
        """
        Return the foreground color for the accent color.

        :rtype: studioqt.Color 
        """
        return studioqt.Color(255, 255, 255, 255)

    def forgroundColor(self):
        """
        Return the foreground color for the theme.

        :rtype: studioqt.Color 
        """
        if self.isDark():
            return studioqt.Color(250, 250, 250, 225)
        else:
            return studioqt.Color(0, 40, 80, 180)

    def itemBackgroundColor(self):
        """
        Return the item background color.

        :rtype: studioqt.Color 
        """
        if self.isDark():
            return studioqt.Color(255, 255, 255, 20)
        else:
            return studioqt.Color(255, 255, 255, 120)

    def itemBackgroundHoverColor(self):
        """
        Return the item background color when the mouse hovers over the item.

        :rtype: studioqt.Color 
        """
        return studioqt.Color(255, 255, 255, 60)

    def accentColor(self):
        """
        Return the accent color for the theme.

        :rtype: studioqt.Color or None
        """
        return self._accentColor

    def backgroundColor(self):
        """
        Return the background color for the theme.

        :rtype: studioqt.Color or None
        """
        return self._backgroundColor

    def setAccentColor(self, color):
        """
        Set the accent color for the theme.

        :type color: studioqt.Color | QtGui.QColor
        """
        if isinstance(color, basestring):
            color = studioqt.Color.fromString(color)

        if isinstance(color, QtGui.QColor):
            color = studioqt.Color.fromColor(color)

        self._accentColor = color

        self.updated.emit()

    def setBackgroundColor(self, color):
        """
        Set the background color for the theme.

        :type color: studioqt.Color | QtGui.QColor
        """
        if isinstance(color, basestring):
            color = studioqt.Color.fromString(color)

        if isinstance(color, QtGui.QColor):
            color = studioqt.Color.fromColor(color)

        self._backgroundColor = color

        self.updated.emit()

    def createColorDialog(
            self,
            parent,
            standardColors=None,
            currentColor=None,
    ):
        """
        Create a new instance of the color dialog.

        :type parent: QtWidgets.QWidget
        :type standardColors: list[int]
        :rtype: QtWidgets.QColorDialog
        """
        dialog = QtWidgets.QColorDialog(parent)

        if standardColors:
            index = -1
            for colorR, colorG, colorB in standardColors:
                index += 1

                color = QtGui.QColor(colorR, colorG, colorB).rgba()

                try:
                    # Support for new qt5 signature
                    color = QtGui.QColor(color)
                    dialog.setStandardColor(index, color)
                except:
                    # Support for new qt4 signature
                    color = QtGui.QColor(color).rgba()
                    dialog.setStandardColor(index, color)

        # PySide2 doesn't support d.open(), so we need to pass a blank slot.
        dialog.open(self, QtCore.SLOT("blankSlot()"))

        if currentColor:
            dialog.setCurrentColor(currentColor)

        return dialog

    def browseAccentColor(self, parent=None):
        """
        Show the color dialog for changing the accent color.
        
        :type parent: QtWidgets.QWidget
        :rtype: None
        """
        standardColors = [
            (230, 60, 60), (210, 40, 40), (190, 20, 20), (250, 80, 130),
            (230, 60, 110), (210, 40, 90), (255, 90, 40), (235, 70, 20),
            (215, 50, 0), (240, 100, 170), (220, 80, 150), (200, 60, 130),
            (255, 125, 100), (235, 105, 80), (215, 85, 60), (240, 200, 150),
            (220, 180, 130), (200, 160, 110), (250, 200, 0), (230, 180, 0),
            (210, 160, 0), (225, 200, 40), (205, 180, 20), (185, 160, 0),
            (80, 200, 140), (60, 180, 120), (40, 160, 100), (80, 225, 120),
            (60, 205, 100), (40, 185, 80), (50, 180, 240), (30, 160, 220),
            (10, 140, 200), (100, 200, 245), (80, 180, 225), (60, 160, 205),
            (130, 110, 240), (110, 90, 220), (90, 70, 200), (180, 160, 255),
            (160, 140, 235), (140, 120, 215), (180, 110, 240), (160, 90, 220),
            (140, 70, 200), (210, 110, 255), (190, 90, 235), (170, 70, 215)
        ]

        currentColor = self.accentColor()

        dialog = self.createColorDialog(parent, standardColors, currentColor)
        dialog.currentColorChanged.connect(self.setAccentColor)

        if dialog.exec_():
            self.setAccentColor(dialog.selectedColor())
        else:
            self.setAccentColor(currentColor)

    def browseBackgroundColor(self, parent=None):
        """
        Show the color dialog for changing the background color.

        :type parent: QtWidgets.QWidget
        :rtype: None
        """
        standardColors = [
            (0, 0, 0), (20, 20, 20), (40, 40, 40), (60, 60, 60),
            (80, 80, 80), (100, 100, 100), (20, 20, 30), (40, 40, 50),
            (60, 60, 70), (80, 80, 90), (100, 100, 110), (120, 120, 130),
            (0, 30, 60), (20, 50, 80), (40, 70, 100), (60, 90, 120),
            (80, 110, 140), (100, 130, 160), (0, 60, 60), (20, 80, 80),
            (40, 100, 100), (60, 120, 120), (80, 140, 140), (100, 160, 160),
            (0, 60, 30), (20, 80, 50), (40, 100, 70), (60, 120, 90),
            (80, 140, 110), (100, 160, 130), (60, 0, 10), (80, 20, 30),
            (100, 40, 50), (120, 60, 70), (140, 80, 90), (160, 100, 110),
            (60, 0, 40), (80, 20, 60), (100, 40, 80), (120, 60, 100),
            (140, 80, 120), (160, 100, 140), (40, 15, 5), (60, 35, 25),
            (80, 55, 45), (100, 75, 65), (120, 95, 85), (140, 115, 105)
        ]

        currentColor = self.backgroundColor()

        dialog = self.createColorDialog(parent, standardColors, currentColor)
        dialog.currentColorChanged.connect(self.setBackgroundColor)

        if dialog.exec_():
            self.setBackgroundColor(dialog.selectedColor())
        else:
            self.setBackgroundColor(currentColor)

    def options(self):
        """
        Return the variables used to customise the style sheet.

        :rtype: dict
        """
        accentColor = self.accentColor()
        accentForegroundColor = self.accentForgroundColor()

        foregroundColor = self.forgroundColor()
        backgroundColor = self.backgroundColor()

        itemBackgroundColor = self.itemBackgroundColor()
        itemBackgroundHoverColor = self.itemBackgroundHoverColor()

        if self.isDark():
            darkness = "white"
        else:
            darkness = "black"

        resourceDirname = studiolibrary.resource().dirname()
        resourceDirname = resourceDirname.replace("\\", "/")

        options = {
            "DARKNESS": darkness,
            "RESOURCE_DIRNAME": resourceDirname,

            "ACCENT_COLOR": accentColor.toString(),
            "ACCENT_COLOR_R": str(accentColor.red()),
            "ACCENT_COLOR_G": str(accentColor.green()),
            "ACCENT_COLOR_B": str(accentColor.blue()),

            "ACCENT_FOREGROUND_COLOR": accentForegroundColor.toString(),

            "FOREGROUND_COLOR": foregroundColor.toString(),
            "FOREGROUND_COLOR_R": str(foregroundColor.red()),
            "FOREGROUND_COLOR_G": str(foregroundColor.green()),
            "FOREGROUND_COLOR_B": str(foregroundColor.blue()),

            "BACKGROUND_COLOR": backgroundColor.toString(),
            "BACKGROUND_COLOR_R": str(backgroundColor.red()),
            "BACKGROUND_COLOR_G": str(backgroundColor.green()),
            "BACKGROUND_COLOR_B": str(backgroundColor.blue()),

            "ITEM_TEXT_COLOR": foregroundColor.toString(),
            "ITEM_TEXT_SELECTED_COLOR": accentForegroundColor.toString(),

            "ITEM_BACKGROUND_COLOR": itemBackgroundColor.toString(),
            "ITEM_BACKGROUND_HOVER_COLOR": itemBackgroundHoverColor.toString(),
            "ITEM_BACKGROUND_SELECTED_COLOR": accentColor.toString(),
        }

        return options

    def styleSheet(self):
        """
        Return the style sheet for this theme.

        :rtype: str
        """
        options = self.options()
        path = studiolibrary.resource().get("css", "default.css")
        styleSheet = studioqt.StyleSheet.fromPath(path, options=options, dpi=self.dpi())
        return styleSheet.data()
Esempio n. 21
0
class FormWidget(QtWidgets.QFrame):

    accepted = QtCore.Signal(object)
    stateChanged = QtCore.Signal()
    validated = QtCore.Signal()

    def __init__(self, *args, **kwargs):
        super(FormWidget, self).__init__(*args, **kwargs)

        self._schema = []
        self._widgets = []
        self._validator = None

        layout = QtWidgets.QVBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)

        self.setLayout(layout)

        self._optionsFrame = QtWidgets.QFrame(self)
        self._optionsFrame.setObjectName("optionsFrame")

        layout = QtWidgets.QVBoxLayout(self._optionsFrame)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)

        self._optionsFrame.setLayout(layout)

        self._titleWidget = QtWidgets.QPushButton(self)
        self._titleWidget.setCheckable(True)
        self._titleWidget.setObjectName("titleWidget")
        self._titleWidget.toggled.connect(self._titleClicked)
        self._titleWidget.hide()

        self.layout().addWidget(self._titleWidget)
        self.layout().addWidget(self._optionsFrame)

    def _titleClicked(self, toggle):
        """Triggered when the user clicks the title widget."""
        self.setExpanded(toggle)
        self.stateChanged.emit()

    def titleWidget(self):
        """
        Get the title widget.
        
        :rtype: QWidget
        """
        return self._titleWidget

    def setTitle(self, title):
        """
        Set the text for the title widget.
        
        :type title: str
        """
        self.titleWidget().setText(title)

    def setExpanded(self, expand):
        """
        Expands the options if expand is true, otherwise collapses the options.
        
        :type expand: bool
        """
        self._titleWidget.blockSignals(True)

        try:
            self._titleWidget.setChecked(expand)
            self._optionsFrame.setVisible(expand)
        finally:
            self._titleWidget.blockSignals(False)

    def isExpanded(self):
        """
        Returns true if the item is expanded, otherwise returns false.
        
        :rtype: bool
        """
        return self._titleWidget.isChecked()

    def setTitleVisible(self, visible):
        """
        A convenience method for setting the title visible.
        
        :type visible: bool
        """
        self.titleWidget().setVisible(visible)

    def reset(self):
        """Reset all option widgets back to their default value."""
        for widget in self._widgets:
            widget.reset()
        self.validate()

    def setSchema(self, schema, layout=None):
        """
        Set the schema for the widget.
        
        :type schema: list[dict]
        """
        self._schema = schema

        for data in schema:

            cls = FIELD_WIDGET_REGISTRY.get(data.get("type", "label"))

            if not cls:
                logger.warning("Cannot find widget for %s", data)
                continue

            if layout and not data.get("layout"):
                data["layout"] = layout

            widget = cls(data=data)
            widget.setData(data)

            value = data.get("value")
            default = data.get("default")
            if value is None and default is not None:
                widget.setValue(default)

            self._widgets.append(widget)

            callback = functools.partial(self._optionChanged, widget)
            widget.valueChanged.connect(callback)

            self._optionsFrame.layout().addWidget(widget)

    def _optionChanged(self, widget):
        """
        Triggered when the given option widget changes value.
        
        :type widget: FieldWidget 
        """
        self.validate()

    def accept(self):
        """Accept the current options"""
        self.emitAcceptedCallback()

    def closeEvent(self, event):
        super(FormWidget, self).closeEvent(event)

    def hasErrors(self):
        """
        Return True if the form contains any errors.

        :rtype: bool
        """
        for widget in self._widgets:
            if widget.data().get("error"):
                return True
        return False

    def setValidator(self, validator):
        """
        Set the validator for the options.
        
        :type validator: func
        """
        self._validator = validator

    def validator(self):
        """
        Return the validator for the form.

        :rtype: func
        """
        return self._validator

    def validate(self):
        """Validate the current options using the validator."""
        if self._validator:

            fields = self._validator(**self.values())
            if fields is not None:
                self._setState(fields)

            self.validated.emit()

        else:
            logger.debug("No validator set.")

    def setValue(self, name, value):
        """
        Set the value for the given field name and value

        :type name: str
        :type value: object
        """
        widget = self.widget(name)
        widget.setValue(value)

    def value(self, name):
        """
        Get the value for the given widget name.
        
        :type name: str 
        :rtype: object 
        """
        widget = self.widget(name)
        return widget.value()

    def widget(self, name):
        """
        Get the widget for the given widget name.
        
        :type name: str 
        :rtype: FieldWidget 
        """
        for widget in self._widgets:
            if widget.data().get("name") == name:
                return widget

    def options(self):
        options = []
        for widget in self._widgets:
            options.append(widget.data())
        return options

    def optionsToDict(self):
        """
        This method is deprecated.

        :rtype: dict
        """
        return self.values()

    def values(self):
        """
        Get the all the field values indexed by the field name.

        :rtype: dict
        """
        values = {}
        for widget in self._widgets:
            values[widget.data().get("name")] = widget.value()
        return values

    def defaultValues(self):
        """
        Get the all the default field values indexed by the field name.

        :rtype: dict
        """
        values = {}
        for widget in self._widgets:
            values[widget.data().get("name")] = widget.default()
        return values

    def state(self):
        """
        Get the current state.
        
        :rtype: dict 
        """
        options = []

        for widget in self._widgets:
            options.append(widget.state())

        state = {
            "options": options,
            "expanded": self.isExpanded()
        }

        return state

    def setState(self, state):
        """
        Set the current state.
        
        :type state: dict 
        """
        expanded = state.get("expanded")
        if expanded is not None:
            self.setExpanded(expanded)

        options = state.get("options")
        if options is not None:
            self._setState(options)

        self.validate()

    def optionsState(self):
        state = {}
        values = self.values()
        options = self.options()

        for option in options:
            name = option.get("name")
            persistent = option.get("persistent")
            if name and persistent:
                state[name] = values[name]

        return state

    def setStateFromOptions(self, options):
        state = []
        for option in options:
            state.append({"name": option, "value": options[option]})

        self._setState(state)

    def _setState(self, fields):
        """
        Set the state.
        
        :type fields: list[dict]
        """
        for widget in self._widgets:
            widget.blockSignals(True)

        for widget in self._widgets:
            widget.setError("")
            for field in fields:
                if field.get("name") == widget.data().get("name"):
                    widget.setData(field)

        for widget in self._widgets:
            widget.blockSignals(False)

        self.stateChanged.emit()
Esempio n. 22
0
class FieldWidget(QtWidgets.QFrame):
    """The base widget for all field widgets.
    
    Examples:
        
        data = {
            'name': 'startFrame',
            'type': 'int'
            'value': 1,
        }
        
        fieldWidget = FieldWidget(data)
        
    """
    valueChanged = QtCore.Signal()

    DefaultLayout = "horizontal"

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

        self._data = data or {}
        self._widget = None
        self._default = None
        self._required = None
        self._menuButton = None
        self._actionResult = None

        self.setObjectName("fieldWidget")

        direction = self._data.get("layout", self.DefaultLayout)

        if direction == "vertical":
            layout = QtWidgets.QVBoxLayout(self)
        else:
            layout = QtWidgets.QHBoxLayout(self)

        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)

        self.setLayout(layout)
        self.setContentsMargins(0, 0, 0, 0)

        self._label = QtWidgets.QLabel(self)
        self._label.setObjectName('label')
        self._label.setSizePolicy(
            QtWidgets.QSizePolicy.Preferred,
            QtWidgets.QSizePolicy.Preferred,
        )

        layout.addWidget(self._label)

        self._layout2 = QtWidgets.QHBoxLayout(self)
        layout.addLayout(self._layout2)

        if direction == "vertical":
            self._label.setAlignment(QtCore.Qt.AlignLeft
                                     | QtCore.Qt.AlignVCenter)
        else:
            self._label.setAlignment(QtCore.Qt.AlignRight
                                     | QtCore.Qt.AlignVCenter)

    def label(self):
        """
        Get the label widget.
        
        :rtype: QtWidgets.QLabel 
        """
        return self._label

    def state(self):
        """
        Get the current state of the data.
        
        :rtype: dict
        """
        return {"name": self._data["name"], "value": self.value()}

    def data(self):
        """
        Get the data for the widget.
        
        :rtype: dict 
        """
        return self._data

    def title(self):
        """
        Get the title to be displayed for the field.
        
        :rtype: str 
        """
        data = self.data()

        title = data.get("title", "") or data.get("name", "")
        if title:
            title = toTitle(title)

        if self.isRequired():
            title += '*'

        return title

    def setData(self, data):
        """
        Set the current state of the field widget using a dictionary.
        
        :type data: dict
        """
        self._data.update(data)
        state = self._data

        self.blockSignals(True)

        items = state.get('items')
        if items is not None:
            self.setItems(items)

        value = state.get('value')
        default = state.get('default')

        # Must set the default before value
        if default is not None:
            self.setDefault(default)
        else:
            self.setDefault(value)

        if value is not None:
            try:
                self.setValue(value)
            except TypeError as error:
                logger.exception(error)

        enabled = state.get('enabled')
        if enabled is not None:
            self.setEnabled(enabled)

        hidden = state.get('hidden')
        if hidden is not None:
            self.setHidden(hidden)

        required = state.get('required')
        if required is not None:
            self.setRequired(required)

        message = state.get('error', '')
        self.setError(message)

        annotation = state.get('annotation', '')
        self.setToolTip(annotation)

        toolTip = state.get('toolTip', '')
        self.setToolTip(toolTip)

        style = state.get("style")
        if style:
            self.setStyleSheet(style)

        title = self.title() or ""
        self.setText(title)

        label = state.get('label', {})
        if label:

            text = label.get("name")
            if text is not None:
                self.setText(text)

            visible = label.get('visible')
            if visible is not None:
                self.label().setVisible(visible)

        text = state.get("menu", {}).get("name")
        if text is not None:
            self.setMenuText(text)

        self.refresh()

        self.blockSignals(False)

    def setError(self, message):
        """
        Set the error message to be displayed for the field widget.
        
        :type message: str
        """
        error = True if message else False

        if error:
            self.setToolTip(message)
        else:
            self.setToolTip(self.data().get('annotation'))

        self.setProperty('error', error)
        self.setStyleSheet(self.styleSheet())

    def setText(self, text):
        """
        Set the label text for the field widget.
        
        :type text: str 
        """
        self._label.setText(text)

    def setValue(self, value):
        """
        Set the value of the field widget.
        
        Will emit valueChanged() if the new value is different from the old one.
        
        :type value: object
        """
        self.emitValueChanged()

    def value(self):
        """
        Get the value of the field widget.
        
        :rtype: object
        """
        raise NotImplementedError('The method "value" needs to be implemented')

    def setItems(self, items):
        """
        Set the items for the field widget.
        
        :type items: list[str]
        """
        raise NotImplementedError(
            'The method "setItems" needs to be implemented')

    def reset(self):
        """Reset the field widget back to the defaults."""
        self.setState(self._data)

    def setRequired(self, required):
        """
        Set True if a value is required for this field.
        
        :type required: bool
        """
        self._required = required
        self.setProperty('required', required)
        self.setStyleSheet(self.styleSheet())

    def isRequired(self):
        """
        Check if a value is required for the field widget.
        
        :rtype: bool
        """
        return bool(self._required)

    def setDefault(self, default):
        """
        Set the default value for the field widget.
        
        :type default: object
        """
        self._default = default

    def default(self):
        """
        Get the default value for the field widget.
        
        :rtype: object
        """
        return self._default

    def isDefault(self):
        """
        Check if the current value is the same as the default value.
        
        :rtype: bool
        """
        return self.value() == self.default()

    def emitValueChanged(self, *args):
        """
        Emit the value changed signal.
        
        :type args: list
        """
        self.valueChanged.emit()
        self.refresh()

    def setWidget(self, widget):
        """
        Set the widget used to set and get the field value.
        
        :type widget: QtWidgets.QWidget
        """
        self._widget = widget
        self._widget.setParent(self)
        self._widget.setObjectName('widget')
        self._widget.setSizePolicy(
            QtWidgets.QSizePolicy.Expanding,
            QtWidgets.QSizePolicy.Preferred,
        )

        self._layout2.addWidget(self._widget)

        self.createMenuButton()

    def setMenuText(self, text):
        self._menuButton.setText(text)

    def createMenuButton(self):
        """Create the menu button to show the actions."""
        menu = self.data().get("menu", {})
        actions = self.data().get("actions", {})

        if menu or actions:

            name = menu.get("name", "...")
            callback = menu.get("callback", self.showMenu)

            self._menuButton = QtWidgets.QPushButton(name)
            self._menuButton.setObjectName("menuButton")
            self._menuButton.clicked.connect(callback)

            self._layout2.addWidget(self._menuButton)

    def actionCallback(self, callback):
        """
        Wrap schema callback to get the return value.
        
        :type callback: func 
        """
        self._actionResult = callback()

    def showMenu(self):
        """Show the menu using the actions from the data."""
        menu = QtWidgets.QMenu(self)
        actions = self.data().get("actions", [])

        for action in actions:

            name = action.get("name", "No name found")
            callback = action.get("callback")

            func = functools.partial(self.actionCallback, callback)

            action = menu.addAction(name)
            action.triggered.connect(func)

        point = QtGui.QCursor.pos()
        point.setX(point.x() + 3)
        point.setY(point.y() + 3)

        # Reset the action results
        self._actionResult = None

        menu.exec_(point)

        if self._actionResult is not None:
            self.setValue(self._actionResult)

    def widget(self, ):
        """
        Get the widget used to set and get the field value.
        
        :rtype: QtWidgets.QWidget
        """
        return self._widget

    def refresh(self):
        """Refresh the style properties."""
        direction = self._data.get("layout", self.DefaultLayout)

        self.setProperty("layout", direction)
        self.setProperty('default', self.isDefault())

        self.setStyleSheet(self.styleSheet())
Esempio n. 23
0
class Library(QtCore.QObject):

    ColumnLabels = [
        "icon",
        "name",
        "path",
        "type",
        "category",
        "folder",
        # "modified"
    ]

    SortLabels = [
        "name",
        "path",
        "type",
        "category",
        "folder",
        # "modified"
    ]

    GroupLabels = [
        "type",
        "category",
        # "modified",
    ]

    DatabasePath = "{path}/.studiolibrary/database.json"

    dataChanged = QtCore.Signal()

    def __init__(self, path, *args):
        QtCore.QObject.__init__(self, *args)

        self._path = None
        self._mtime = None
        self._data = {}
        self._items = []
        self._currentItems = []

        self.setPath(path)
        self.setDirty(True)

    def currentItems(self):
        """
        The items that are displayed in the view.
        
        :rtype: list[studiolibrary.LibraryItem]
        """
        return self._currentItems

    def recursiveDepth(self):
        """
        Return the recursive search depth.
        
        :rtype: int
        """
        return studiolibrary.config().get('recursiveSearchDepth')

    def path(self):
        """
        Return the disc location of the db.

        :rtype: str
        """
        return self._path

    def setPath(self, path):
        """
        Set the disc location of the db.

        :type path: str
        """
        self._path = path

    def databasePath(self):
        """
        Return the path to the database.
        
        :rtype: str 
        """
        return studiolibrary.formatPath(self.DatabasePath, path=self.path())

    def mtime(self):
        """
        Return when the database was last modified.

        :rtype: float or None
        """
        path = self.databasePath()
        mtime = None

        if os.path.exists(path):
            mtime = os.path.getmtime(path)

        return mtime

    def setDirty(self, value):
        """
        Update the model object with the current database timestamp.

        :type: bool
        """
        if value:
            self._mtime = None
        else:
            self._mtime = self.mtime()

    def isDirty(self):
        """
        Return True if the database has changed on disc.

        :rtype: bool
        """
        return not self._items or self._mtime != self.mtime()

    def read(self):
        """
        Read the database from disc and return a dict object.

        :rtype: dict
        """
        if self.isDirty():
            self._data = studiolibrary.readJson(self.databasePath())
            self.setDirty(False)

        return self._data

    def save(self, data):
        """
        Write the given dict object to the database on disc.

        :type data: dict
        :rtype: None
        """
        studiolibrary.saveJson(self.databasePath(), data)

    def sync(self):
        """
        Sync the file system with the database. 
        
        :rtype: None 
        """
        data = self.read()

        isDirty = False

        for path in data.keys():
            if not os.path.exists(path):
                isDirty = True
                del data[path]

        depth = self.recursiveDepth()
        items = studiolibrary.findItems(
            self.path(),
            depth=depth,
        )

        for item in items:
            path = item.path()
            if item.path() not in data:
                isDirty = True
                data[path] = {}

        if isDirty:
            self.save(data)
            self.dataChanged.emit()

    def findItems(self, queries, libraryWidget=None):
        """
        Get the items that match the given queries.
        
        Examples:
            
            queries = [
                {
                    'operator': 'or',
                    'filters': [
                        ('folder', 'is' '/library/proj/test'),
                        ('folder', 'startswith', '/library/proj/test'),
                    ]
                },
                {
                    'operator': 'and',
                    'filters': [
                        ('path', 'contains' 'test'),
                        ('path', 'contains', 'run'),
                    ]
                }
            ]
            
            print(library.find(queries))
            
        :type queries: list[dict]
        :type libraryWidget: studiolibrary.LibraryWIdget or None
            
        :rtype: list[studiolibrary.LibraryItem]
        """
        items = self.createItems(libraryWidget=libraryWidget)

        self._currentItems = []

        for item in items:

            matches = []

            for query in queries:

                filters = query.get('filters')
                operator = query.get('operator', 'and')

                if not filters:
                    continue

                match = False

                for key, cond, text in filters:

                    text = text.lower()
                    itemText = item.text(key).lower()

                    if cond == 'contains':
                        match = text in itemText

                    elif cond == 'is':
                        match = text == itemText

                    elif cond == 'startswith':
                        match = itemText.startswith(text)

                    if operator == 'or' and match:
                        break

                    if operator == 'and' and not match:
                        break

                matches.append(match)

            if all(matches):
                self._currentItems.append(item)

        return self._currentItems

    def addItem(self, item, data=None):
        """
        Add the given item to the database.    
    
        :type item: studiolibrary.LibraryItem
        :type data: dict or None
        :rtype: None 
        """
        self.addItems([item], data)

    def addItems(self, items, data=None):
        """
        Add the given items to the database.
        
        :type items: list[studiolibrary.LibraryItem]
        :type data: dict or None
        :rtype: None 
        """
        logger.info("Add items %s", items)

        paths = [item.path() for item in items]

        isDirty = self.isDirty()

        self.addPaths(paths, data)

        self.setDirty(isDirty)

        self._items.extend(items)

        self.dataChanged.emit()

    def createItems(self, libraryWidget=None):
        """
        Create all the items for the model.

        :rtype: list[studiolibrary.LibraryItem] 
        """
        # Check if the database has changed since the last read call
        if self.isDirty():

            paths = self.read().keys()
            items = studiolibrary.itemsFromPaths(paths,
                                                 library=self,
                                                 libraryWidget=libraryWidget)

            self._items = list(items)
            self.loadItemData(self._items)

        return self._items

    def saveItemData(self, items, columns=None):
        """
        Save the item data to the database for the given items and columns.

        :type columns: list[str]
        :type items: list[studiolibrary.LibraryItem]
        """
        data = {}
        columns = columns or ["Custom Order"]

        for item in items:
            path = item.path()
            data.setdefault(path, {})

            for column in columns:
                value = item.text(column)
                data[path].setdefault(column, value)

        studiolibrary.updateJson(self.databasePath(), data)

    def loadItemData(self, items):
        """
        Load the item data from the database to the given items.

        :type items: list[studiolibrary.LibraryItem]
        :rtype: None
        """
        data = self.read()

        for item in items:
            key = item.id()
            if key in data:
                item.setItemData(data[key])

    def addPaths(self, paths, data=None):
        """
        Add the given path and the given data to the database.    
    
        :type paths: list[str]
        :type data: dict or None
        :rtype: None 
        """
        data = data or {}
        self.updatePaths(paths, data)

    def updatePaths(self, paths, data):
        """
        Update the given paths with the given data in the database.

        :type paths: list[str]
        :type data: dict
        :rtype: None
        """
        data_ = self.read()
        paths = studiolibrary.normPaths(paths)

        for path in paths:
            if path in data_:
                data_[path].update(data)
            else:
                data_[path] = data

        self.save(data_)

    def copyPath(self, src, dst):
        """
        Copy the given source path to the given destination path.

        :type src: str
        :type dst: str
        :rtype: str
        """
        self.addPaths([dst])
        return dst

    def renamePath(self, src, dst):
        """
        Rename the source path to the given name.

        :type src: str
        :type dst: str
        :rtype: str
        """
        studiolibrary.renamePathInFile(self.databasePath(), src, dst)
        return dst

    def removePath(self, path):
        """
        Remove the given path from the database.

        :type path: str
        :rtype: None
        """
        self.removePaths([path])

    def removePaths(self, paths):
        """
        Remove the given paths from the database.

        :type paths: list[str]
        :rtype: None
        """
        data = self.read()

        paths = studiolibrary.normPaths(paths)

        for path in paths:
            if path in data:
                del data[path]

        self.save(data)
Esempio n. 24
0
class SearchWidget(QtWidgets.QLineEdit):

    DEFAULT_PLACEHOLDER_TEXT = "Search"

    searchChanged = QtCore.Signal()

    def __init__(self, *args):
        QtWidgets.QLineEdit.__init__(self, *args)

        self._iconPadding = 6
        self._iconButton = QtWidgets.QPushButton(self)
        self._iconButton.clicked.connect(self._iconClicked)
        self._searchFilter = studioqt.SearchFilter("")

        icon = studioqt.icon("search")
        self.setIcon(icon)

        self._clearButton = QtWidgets.QPushButton(self)
        self._clearButton.setCursor(QtCore.Qt.ArrowCursor)
        icon = studioqt.icon("cross")
        self._clearButton.setIcon(icon)
        self._clearButton.setToolTip("Clear all search text")
        self._clearButton.clicked.connect(self._clearClicked)

        self.setPlaceholderText(self.DEFAULT_PLACEHOLDER_TEXT)

        self.textChanged.connect(self._textChanged)
        self.searchChanged = self.searchFilter().searchChanged

        self.update()

    def update(self):
        self.updateIconColor()
        self.updateClearButton()

    def updateIconColor(self):
        """
        Update the color of the icons from the current palette.

        :rtype: None
        """
        color = self.palette().color(self.foregroundRole())
        color = studioqt.Color.fromColor(color)
        self.setIconColor(color)

    def _clearClicked(self):
        """
        Triggered when the user clicks the cross icon.

        :rtype: None
        """
        self.setText("")
        self.setFocus()

    def _iconClicked(self):
        """
        Triggered when the user clicks on the icon.

        :rtype: None
        """
        if not self.hasFocus():
            self.setFocus()

    def _textChanged(self, text):
        """
        Triggered when the text changes.

        :type text: str
        :rtype: None
        """
        self.searchFilter().setPattern(text)
        self.updateClearButton()

    def updateClearButton(self):
        """
        Update the clear button depending on the current text.

        :rtype: None
        """
        text = self.text()
        if text:
            self._clearButton.show()
        else:
            self._clearButton.hide()

    def contextMenuEvent(self, event):
        """
        Triggered when the user right clicks on the search widget.

        :type event: QtCore.QEvent
        :rtype: None
        """
        self.showContextMenu()

    def setSpaceOperator(self, operator):
        """
        Set the space operator for the search filter.

        :type operator: studioqt.SearchFilter.Operator
        :rtype: None
        """
        self._searchFilter.setSpaceOperator(operator)

    def createSpaceOperatorMenu(self, parent=None):
        """
        Return the menu for changing the space operator.

        :type parent: QGui.QMenu
        :rtype: QGui.QMenu
        """
        searchFilter = self.searchFilter()

        menu = QtWidgets.QMenu(parent)
        menu.setTitle("Space Operator")

        # Create the space operator for the OR operator
        action = QtWidgets.QAction(menu)
        action.setText("OR")
        action.setCheckable(True)

        callback = partial(self.setSpaceOperator, searchFilter.Operator.OR)
        action.triggered.connect(callback)

        if searchFilter.spaceOperator() == searchFilter.Operator.OR:
            action.setChecked(True)

        menu.addAction(action)

        # Create the space operator for the AND operator
        action = QtWidgets.QAction(menu)
        action.setText("AND")
        action.setCheckable(True)

        callback = partial(self.setSpaceOperator, searchFilter.Operator.AND)
        action.triggered.connect(callback)

        if searchFilter.spaceOperator() == searchFilter.Operator.AND:
            action.setChecked(True)

        menu.addAction(action)

        return menu

    def showContextMenu(self):
        """
        Create and show the context menu for the search widget.

        :rtype QtWidgets.QAction
        """
        menu = QtWidgets.QMenu(self)

        subMenu = self.createStandardContextMenu()
        subMenu.setTitle("Edit")
        menu.addMenu(subMenu)

        subMenu = self.createSpaceOperatorMenu(menu)
        menu.addMenu(subMenu)

        point = QtGui.QCursor.pos()
        action = menu.exec_(point)

        return action

    def searchFilter(self):
        """
        Return the search filter for the widget.

        :rtype: studioqt.SearchFilter
        """
        return self._searchFilter

    def setIcon(self, icon):
        """
        Set the icon for the search widget.

        :type icon: QtWidgets.QIcon
        :rtype: None
        """
        self._iconButton.setIcon(icon)

    def setIconColor(self, color):
        """
        Set the icon color for the search widget icon.

        :type color: QtGui.QColor
        :rtype: None
        """
        icon = self._iconButton.icon()
        icon = studioqt.Icon(icon)
        icon.setColor(color)
        self._iconButton.setIcon(icon)

        icon = self._clearButton.icon()
        icon = studioqt.Icon(icon)
        icon.setColor(color)
        self._clearButton.setIcon(icon)

    def settings(self):
        """
        Return a dictionary of the current widget state.

        :rtype: dict
        """
        settings = {"text": self.text()}
        settings["searchFilter"] = self.searchFilter().settings()
        return settings

    def setSettings(self, settings):
        """
        Restore the widget state from a settings dictionary.

        :type settings: dict
        :rtype: None
        """
        searchFilterSettings = settings.get("searchFilter", None)
        if searchFilterSettings is not None:
            self.searchFilter().setSettings(searchFilterSettings)

        text = settings.get("text", "")
        self.setText(text)

    def resizeEvent(self, event):
        """
        Reimplemented so the icon maintains the same height as the widget.

        :type event:  QtWidgets.QResizeEvent
        :rtype: None
        """
        QtWidgets.QLineEdit.resizeEvent(self, event)

        self.setTextMargins(self.height(), 0, 0, 0)
        size = QtCore.QSize(self.height(), self.height())

        self._iconButton.setIconSize(size)
        self._iconButton.setFixedSize(size)

        self._clearButton.setIconSize(size)

        x = self.width() - self.height()
        self._clearButton.setGeometry(x, 0, self.height(), self.height())
Esempio n. 25
0
class FormDialog(QtWidgets.QFrame):

    accepted = QtCore.Signal(object)
    rejected = QtCore.Signal(object)

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

        layout = QtWidgets.QVBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)

        self.setLayout(layout)

        self._widgets = []
        self._validator = None

        self._title = QtWidgets.QLabel(self)
        self._title.setObjectName('title')
        self._title.setText('FORM')
        self.layout().addWidget(self._title)

        self._description = QtWidgets.QLabel(self)
        self._description.setObjectName('description')
        self.layout().addWidget(self._description)

        self._formWidget = FormWidget(self)
        self._formWidget.setObjectName("formWidget")
        self._formWidget.validated.connect(self._validated)
        self.layout().addWidget(self._formWidget)

        self.layout().addStretch(1)

        buttonLayout = QtWidgets.QHBoxLayout(self)
        buttonLayout.setContentsMargins(0, 0, 0, 0)
        buttonLayout.setSpacing(0)

        self.layout().addLayout(buttonLayout)

        buttonLayout.addStretch(1)

        self._acceptButton = QtWidgets.QPushButton(self)
        self._acceptButton.setObjectName('acceptButton')
        self._acceptButton.setText('Submit')
        self._acceptButton.clicked.connect(self.accept)

        self._rejectButton = QtWidgets.QPushButton(self)
        self._rejectButton.setObjectName('rejectButton')
        self._rejectButton.setText('Cancel')
        self._rejectButton.clicked.connect(self.reject)

        buttonLayout.addWidget(self._acceptButton)
        buttonLayout.addWidget(self._rejectButton)

        if form:
            self.setSettings(form)
        # buttonLayout.addStretch(1)

    def _validated(self):
        """Triggered when the form has been validated"""
        self._acceptButton.setEnabled(not self._formWidget.hasErrors())

    def acceptButton(self):
        """
        Return the accept button.

        :rtype: QWidgets.QPushButton
        """
        return self._acceptButton

    def rejectButton(self):
        """
        Return the reject button.

        :rtype: QWidgets.QPushButton
        """
        return self._rejectButton

    def validateAccepted(self, **kwargs):
        """
        Triggered when the accept button has been clicked.

        :type kwargs: The values of the fields
        """
        self._formWidget.validator()(**kwargs)

    def validateRejected(self, **kwargs):
        """
        Triggered when the reject button has been clicked.

        :type kwargs: The default values of the fields
        """
        self._formWidget.validator()(**kwargs)

    def setSettings(self, settings):

        self._settings = settings

        title = settings.get("title")
        if title is not None:
            self._title.setText(title)

        callback = settings.get("accepted")
        if not callback:
            self._settings["accepted"] = self.validateAccepted

        callback = settings.get("rejected")
        if not callback:
            self._settings["rejected"] = self.validateRejected

        description = settings.get("description")
        if description is not None:
            self._description.setText(description)

        validator = settings.get("validator")
        if validator is not None:
            self._formWidget.setValidator(validator)

        layout = settings.get("layout")

        schema = settings.get("schema")
        if schema is not None:
            self._formWidget.setSchema(schema, layout=layout)

    def accept(self):
        """Call this method to accept the dialog."""
        callback = self._settings.get("accepted")
        if callback:
            callback(**self._formWidget.values())
        self.close()

    def reject(self):
        """Call this method to rejected the dialog."""
        callback = self._settings.get("rejected")
        if callback:
            callback(**self._formWidget.defaultValues())
        self.close()
Esempio n. 26
0
class ImageSequence(QtCore.QObject):
    DEFAULT_FPS = 24
    frameChanged = QtCore.Signal()

    def __init__(self, *args):
        QtCore.QObject.__init__(self, *args)
        self._fps = self.DEFAULT_FPS
        self._timer = None
        self._frame = 0
        self._frames = []
        self._dirname = None
        self._paused = False

    def setDirname(self, dirname):
        """
        Set the location to the image sequence.
        
        :type dirname: str
        :rtype: None
        """
        def naturalSortItems(items):
            """
            Sort the given list in the way that humans expect.
            """
            convert = lambda text: (int(text) if text.isdigit() else text)
            alphanum_key = lambda key: [
                convert(c) for c in re.split('([0-9]+)', key)
            ]
            items.sort(key=alphanum_key)

        self._dirname = dirname
        if os.path.isdir(dirname):
            self._frames = [
                dirname + '/' + filename for filename in os.listdir(dirname)
            ]
            naturalSortItems(self._frames)

    def dirname(self):
        """
        Return the location to the image sequence.
        
        :rtype: str
        """
        return self._dirname

    def reset(self):
        """
        Stop and reset the current frame to 0.
        
        :rtype: None
        """
        if not self._timer:
            self._timer = QtCore.QTimer(self.parent())
            self._timer.setSingleShot(False)
            self.connect(self._timer, QtCore.SIGNAL('timeout()'),
                         self._frameChanged)
        if not self._paused:
            self._frame = 0
        self._timer.stop()

    def pause(self):
        """
        ImageSequence will enter Paused state.
        
        :rtype: None
        """
        self._paused = True
        self._timer.stop()

    def resume(self):
        """
        ImageSequence will enter Playing state.
        
        :rtype: None
        """
        if self._paused:
            self._paused = False
            self._timer.start()

    def stop(self):
        """
        Stops the movie. ImageSequence enters NotRunning state.
        
        :rtype: None
        """
        self._frame = 0
        self._timer.stop()

    def start(self):
        """
        Starts the movie. ImageSequence will enter Running state
        
        :rtype: None
        """
        self.reset()
        if self._timer:
            self._timer.start(1000.0 / self._fps)

    def frames(self):
        """
        Return all the filenames in the image sequence.
        
        :rtype: list[str]
        """
        return self._frames

    def _frameChanged(self):
        """
        Triggered when the current frame changes.
        
        :rtype: None
        """
        if not self._frames:
            return
        frame = self._frame
        frame += 1
        self.setCurrentFrame(frame)

    def percent(self):
        """
        Return the current frame position as a percentage.
        
        :rtype: None
        """
        if len(self._frames) == self._frame + 1:
            _percent = 1
        else:
            _percent = float(len(self._frames) + self._frame) / len(
                self._frames) - 1
        return _percent

    def duration(self):
        """
        Return the number of frames.
        
        :rtype: int
        """
        return len(self._frames)

    def currentFilename(self):
        """
        Return the current file name.
        
        :rtype: str or None
        """
        try:
            return self._frames[self.currentFrame()]
        except IndexError:
            pass

    def currentFrame(self):
        """
        Return the current frame.
        
        :rtype: int or None
        """
        return self._frame

    def setCurrentFrame(self, frame):
        """
        Set the current frame.
        
        :rtype: int or None
        """
        if frame >= self.duration():
            frame = 0
        self._frame = frame
        self.frameChanged.emit()
Esempio n. 27
0
class Library(QtCore.QObject):

    Fields = [
        "icon",
        "name",
        "path",
        "type",
        "folder",
        "category",
        # "modified"
    ]

    SortFields = [
        "name",
        "path",
        "type",
        "folder",
        "category",
        "Custom Order",  # legacy case
        # "modified"
    ]

    GroupFields = [
        "type",
        "category",
        # "modified",
    ]

    dataChanged = QtCore.Signal()
    searchStarted = QtCore.Signal()
    searchFinished = QtCore.Signal()
    searchTimeFinished = QtCore.Signal()

    def __init__(self, path=None, libraryWindow=None, *args):
        QtCore.QObject.__init__(self, *args)

        self._path = path
        self._mtime = None
        self._data = {}
        self._items = []
        self._fields = []
        self._sortBy = []
        self._groupBy = []
        self._results = []
        self._queries = {}
        self._globalQueries = {}
        self._groupedResults = {}
        self._searchTime = 0
        self._searchEnabled = True
        self._libraryWindow = libraryWindow

        self.setPath(path)
        self.setDirty(True)

    def sortBy(self):
        """
        Get the list of fields to sort by.
        
        :rtype: list[str] 
        """
        return self._sortBy

    def setSortBy(self, fields):
        """
        Set the list of fields to group by.
        
        Example:
            library.setSortBy(["name:asc", "type:asc"])
        
        :type fields: list[str] 
        """
        self._sortBy = fields

    def groupBy(self):
        """
        Get the list of fields to group by.
        
        :rtype: list[str] 
        """
        return self._groupBy

    def setGroupBy(self, fields):
        """
        Set the list of fields to group by.
        
        Example:
            library.setGroupBy(["name:asc", "type:asc"])
        
        :type fields: list[str] 
        """
        self._groupBy = fields

    def settings(self):
        """
        Get the settings for the dataset.
        
        :rtype: dict 
        """
        return {"sortBy": self.sortBy(), "groupBy": self.groupBy()}

    def setSettings(self, settings):
        """
        Set the settings for the dataset object.
        
        :type settings: dict
        """
        value = settings.get('sortBy')
        if value is not None:
            self.setSortBy(value)

        value = settings.get('groupBy')
        if value is not None:
            self.setGroupBy(value)

    def setSearchEnabled(self, enabled):
        """Enable or disable the search the for the library."""
        self._searchEnabled = enabled

    def isSearchEnabled(self):
        """Check if search is enabled for the library."""
        return self._searchEnabled

    def recursiveDepth(self):
        """
        Return the recursive search depth.
        
        :rtype: int
        """
        return studiolibrary.config().get('recursiveSearchDepth')

    def fields(self):
        """Return all the fields for the library."""
        return self._fields

    def path(self):
        """
        Return the disc location of the db.

        :rtype: str
        """
        return self._path

    def setPath(self, path):
        """
        Set the disc location of the db.

        :type path: str
        """
        self._path = path

    def databasePath(self):
        """
        Return the path to the database.
        
        :rtype: str 
        """
        formatString = studiolibrary.config().get('databasePath')
        return studiolibrary.formatPath(formatString, path=self.path())

    def distinct(self, field, queries=None, sortBy="name"):
        """
        Get all the values for the given field.
        
        :type field: str
        :type queries None or list[dict]
        :type sortBy: str
        :rtype: list 
        """
        results = {}
        queries = queries or []
        queries.extend(self._globalQueries.values())

        items = self.createItems()
        for item in items:
            value = item.itemData().get(field)
            if value:
                results.setdefault(value, {'count': 0, 'name': value})
                match = self.match(item.itemData(), queries)
                if match:
                    results[value]['count'] += 1

        def sortKey(facet):
            return facet.get(sortBy)

        return sorted(results.values(), key=sortKey)

    def mtime(self):
        """
        Return when the database was last modified.

        :rtype: float or None
        """
        path = self.databasePath()
        mtime = None

        if os.path.exists(path):
            mtime = os.path.getmtime(path)

        return mtime

    def setDirty(self, value):
        """
        Update the model object with the current database timestamp.

        :type: bool
        """
        if value:
            self._mtime = None
        else:
            self._mtime = self.mtime()

    def isDirty(self):
        """
        Return True if the database has changed on disc.

        :rtype: bool
        """
        return not self._items or self._mtime != self.mtime()

    def read(self):
        """
        Read the database from disc and return a dict object.

        :rtype: dict
        """
        if self.path():
            if self.isDirty():
                self._data = studiolibrary.readJson(self.databasePath())
                self.setDirty(False)
        else:
            logger.info('No path set for reading the data from disc.')

        return self._data

    def save(self, data):
        """
        Write the given dict object to the database on disc.

        :type data: dict
        :rtype: None
        """
        if self.path():
            studiolibrary.saveJson(self.databasePath(), data)
            self.setDirty(True)
        else:
            logger.info('No path set for saving the data to disc.')

    def clear(self):
        """Clear all the item data."""
        self._items = []
        self._results = []
        self._groupedResults = {}
        self.dataChanged.emit()

    def sync(self, progressCallback=None):
        """Sync the file system with the database."""
        if not self.path():
            logger.info('No path set for syncing data')
            return

        if progressCallback:
            progressCallback("Syncing")

        data = self.read()

        for path in data.keys():
            if not os.path.exists(path):
                del data[path]

        depth = self.recursiveDepth()
        items = studiolibrary.findItems(
            self.path(),
            depth=depth,
        )

        items = list(items)
        count = len(items)

        for i, item in enumerate(items):
            percent = (float(i + 1) / float(count))
            if progressCallback:
                percent *= 100
                label = "{0:.0f}%".format(percent)
                progressCallback(label, percent)

            path = item.path()

            itemData = data.get(path, {})
            itemData.update(item.createItemData())

            data[path] = itemData

        if progressCallback:
            progressCallback("Post Callbacks")

        self.postSync(data)

        if progressCallback:
            progressCallback("Saving Cache")

        self.save(data)

        self.dataChanged.emit()

    def postSync(self, data):
        """
        Use this function to execute code on the data after sync, but before save and dataChanged.emit

        :type data: dict
        :rtype: None
        """
        pass

    def createItems(self):
        """
        Create all the items for the model.

        :rtype: list[studiolibrary.LibraryItem] 
        """
        # Check if the database has changed since the last read call
        if self.isDirty():

            paths = self.read().keys()
            items = studiolibrary.itemsFromPaths(
                paths, library=self, libraryWindow=self._libraryWindow)

            self._items = list(items)
            self.loadItemData(self._items)

        return self._items

    def findItems(self, queries):
        """
        Get the items that match the given queries.
        
        Examples:
            
            queries = [
                {
                    'operator': 'or',
                    'filters': [
                        ('folder', 'is' '/library/proj/test'),
                        ('folder', 'startswith', '/library/proj/test'),
                    ]
                },
                {
                    'operator': 'and',
                    'filters': [
                        ('path', 'contains' 'test'),
                        ('path', 'contains', 'run'),
                    ]
                }
            ]
            
            print(library.find(queries))
            
        :type queries: list[dict]            
        :rtype: list[studiolibrary.LibraryItem]
        """
        fields = []
        results = []

        queries = copy.copy(queries)
        queries.extend(self._globalQueries.values())

        logger.debug("Search queries:")
        for query in queries:
            logger.debug('Query: %s', query)

        items = self.createItems()
        for item in items:
            match = self.match(item.itemData(), queries)
            if match:
                results.append(item)
            fields.extend(item.itemData().keys())

        self._fields = list(set(fields))

        if self.sortBy():
            results = self.sorted(results, self.sortBy())

        return results

    def queries(self, exclude=None):
        """
        Get all the queries for the dataset excluding the given ones.
        
        :type exclude: list[str] or None
        
        :rtype: list[dict] 
        """
        queries = []
        exclude = exclude or []

        for query in self._queries.values():
            if query.get('name') not in exclude:
                queries.append(query)

        return queries

    def addGlobalQuery(self, query):
        """
        Add a global query to library.
        
        :type query: dict 
        """
        self._globalQueries[query["name"]] = query

    def addQuery(self, query):
        """
        Add a search query to the library.
        
        Examples:
            addQuery({
                'name': 'My Query',
                'operator': 'or',
                'filters': [
                    ('folder', 'is' '/library/proj/test'),
                    ('folder', 'startswith', '/library/proj/test'),
                ]
            })
        
        :type query: dict
        """
        self._queries[query["name"]] = query

    def removeQuery(self, name):
        """
        Remove the query with the given name.
        
        :type name: str 
        """
        if name in self._queries:
            del self._queries[name]

    def queryExists(self, name):
        """
        Check if the given query name exists.
        
        :type name: str
        :rtype: bool 
        """
        return name in self._queries

    def search(self):
        """Run a search using the queries added to this dataset."""
        if not self.isSearchEnabled():
            logger.debug('Search is disabled')
            return

        t = time.time()

        logger.debug("Searching items")

        self.searchStarted.emit()

        self._results = self.findItems(self.queries())

        self._groupedResults = self.groupItems(self._results, self.groupBy())

        self.searchFinished.emit()

        self._searchTime = time.time() - t

        self.searchTimeFinished.emit()

        logger.debug('Search time: %s', self._searchTime)

    def results(self):
        """
        Return the items found after a search is ran.
        
        :rtype: list[Item] 
        """
        return self._results

    def groupedResults(self):
        """
        Get the results grouped after a search is ran.
        
        :rtype: dict
        """
        return self._groupedResults

    def searchTime(self):
        """
        Return the time taken to run a search.
        
        :rtype: float 
        """
        return self._searchTime

    def addItem(self, item):
        """
        Add the given item to the database.    
    
        :type item: studiolibrary.LibraryItem
        :rtype: None 
        """
        self.saveItemData([item])

    def addItems(self, items):
        """
        Add the given items to the database.
        
        :type items: list[studiolibrary.LibraryItem]
        """
        self.saveItemData(items)

    def updateItem(self, item):
        """
        Update the given item in the database.    
    
        :type item: studiolibrary.LibraryItem
        :rtype: None 
        """
        self.saveItemData([item])

    def saveItemData(self, items, emitDataChanged=True):
        """
        Add the given items to the database.

        :type items: list[studiolibrary.LibraryItem]
        :type emitDataChanged: bool
        """
        logger.debug("Save item data %s", items)

        data_ = self.read()

        for item in items:
            path = item.path()
            data = item.itemData()
            data_.setdefault(path, {})
            data_[path].update(data)

        self.save(data_)

        if emitDataChanged:
            self.search()
            self.dataChanged.emit()

    def loadItemData(self, items):
        """
        Load the item data from the database to the given items.

        :type items: list[studiolibrary.LibraryItem]
        """
        logger.debug("Loading item data %s", items)

        data = self.read()

        for item in items:
            key = item.id()
            if key in data:
                item.setItemData(data[key])

    def addPaths(self, paths, data=None):
        """
        Add the given path and the given data to the database.    
    
        :type paths: list[str]
        :type data: dict or None
        :rtype: None 
        """
        data = data or {}
        self.updatePaths(paths, data)

    def updatePaths(self, paths, data):
        """
        Update the given paths with the given data in the database.

        :type paths: list[str]
        :type data: dict
        :rtype: None
        """
        data_ = self.read()
        paths = studiolibrary.normPaths(paths)

        for path in paths:
            if path in data_:
                data_[path].update(data)
            else:
                data_[path] = data

        self.save(data_)

    def copyPath(self, src, dst):
        """
        Copy the given source path to the given destination path.

        :type src: str
        :type dst: str
        :rtype: str
        """
        self.addPaths([dst])
        return dst

    def renamePath(self, src, dst):
        """
        Rename the source path to the given name.

        :type src: str
        :type dst: str
        :rtype: str
        """
        studiolibrary.renamePathInFile(self.databasePath(), src, dst)
        self.setDirty(True)
        return dst

    def removePath(self, path):
        """
        Remove the given path from the database.

        :type path: str
        :rtype: None
        """
        self.removePaths([path])

    def removePaths(self, paths):
        """
        Remove the given paths from the database.

        :type paths: list[str]
        :rtype: None
        """
        data = self.read()

        paths = studiolibrary.normPaths(paths)

        for path in paths:
            if path in data:
                del data[path]

        self.save(data)

    @staticmethod
    def match(data, queries):
        """
        Match the given data with the given queries.
        
        Examples:
            
            queries = [
                {
                    'operator': 'or',
                    'filters': [
                        ('folder', 'is' '/library/proj/test'),
                        ('folder', 'startswith', '/library/proj/test'),
                    ]
                },
                {
                    'operator': 'and',
                    'filters': [
                        ('path', 'contains' 'test'),
                        ('path', 'contains', 'run'),
                    ]
                }
            ]
            
            print(library.find(queries))
        """
        matches = []

        for query in queries:

            filters = query.get('filters')
            operator = query.get('operator', 'and')

            if not filters:
                continue

            match = False

            for key, cond, value in filters:

                if key == '*':
                    itemValue = unicode(data)
                else:
                    itemValue = data.get(key)

                if isinstance(value, basestring):
                    value = value.lower()

                if isinstance(itemValue, basestring):
                    itemValue = itemValue.lower()

                if not itemValue:
                    match = False

                elif cond == 'contains':
                    match = value in itemValue

                elif cond == 'not_contains':
                    match = value not in itemValue

                elif cond == 'is':
                    match = value == itemValue

                elif cond == 'not':
                    match = value != itemValue

                elif cond == 'startswith':
                    match = itemValue.startswith(value)

                if operator == 'or' and match:
                    break

                if operator == 'and' and not match:
                    break

            matches.append(match)

        return all(matches)

    @staticmethod
    def sorted(items, sortBy):
        """
        Return the given data sorted using the sortBy argument.
        
        Example:
            data = [
                {'name':'red', 'index':1},
                {'name':'green', 'index':2},
                {'name':'blue', 'index':3},
            ]
            
            sortBy = ['index:asc', 'name']
            # sortBy = ['index:dsc', 'name']
            
            print(sortedData(data, sortBy))
            
        :type items: list[Item]
        :type sortBy: list[str]
        :rtype: list[Item]
        """
        logger.debug('Sort by: %s', sortBy)

        t = time.time()

        for field in reversed(sortBy):

            tokens = field.split(':')

            reverse = False
            if len(tokens) > 1:
                field = tokens[0]
                reverse = tokens[1] != 'asc'

            def sortKey(item):

                default = False if reverse else ''

                return item.itemData().get(field, default)

            items = sorted(items, key=sortKey, reverse=reverse)

        logger.debug("Sort items took %s", time.time() - t)

        return items

    @staticmethod
    def groupItems(items, fields):
        """
        Group the given items by the given field.

        :type items: list[Item]
        :type fields: list[str]
        :rtype: dict
        """
        logger.debug('Group by: %s', fields)

        # Only support for top level grouping at the moment.
        if fields:
            field = fields[0]
        else:
            return {'None': items}

        t = time.time()

        results_ = {}
        tokens = field.split(':')

        reverse = False
        if len(tokens) > 1:
            field = tokens[0]
            reverse = tokens[1] != 'asc'

        for item in items:
            value = item.itemData().get(field)
            if value:
                results_.setdefault(value, [])
                results_[value].append(item)

        groups = sorted(results_.keys(), reverse=reverse)

        results = collections.OrderedDict()
        for group in groups:
            results[group] = results_[group]

        logger.debug("Group Items Took %s", time.time() - t)

        return results
Esempio n. 28
0
class FoldersWidget(QtWidgets.QTreeView):

    itemDropped = QtCore.Signal(object)
    itemClicked = QtCore.Signal()
    itemSelectionChanged = QtCore.Signal()

    def __init__(self, parent=None):
        """
        :type parent: QtWidgets.QWidget
        """
        QtWidgets.QTreeView.__init__(self, parent)

        self._filter = []
        self._folders = {}
        self._isLocked = False
        self._signalsEnabled = True
        self._enableFolderSettings = False

        self._sourceModel = FileSystemModel(self)

        self.setDpi(1)

        proxyModel = SortFilterProxyModel(self)
        proxyModel.setSourceModel(self._sourceModel)
        proxyModel.sort(0)

        self.setAcceptDrops(True)
        self.setModel(proxyModel)
        self.setHeaderHidden(True)
        self.setFrameShape(QtWidgets.QFrame.NoFrame)
        self.setSelectionMode(QtWidgets.QTreeWidget.ExtendedSelection)
        self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)

        signal = "selectionChanged(const QItemSelection&,const QItemSelection&)"

        self.connect(
            self.selectionModel(),
            QtCore.SIGNAL(signal),
            self._selectionChanged,
        )

    def enableFolderSettings(self, value):
        self._enableFolderSettings = value

    def folders(self):
        return self._folders.values()

    def createEditMenu(self, parent=None):
        """
        Return the edit menu for deleting, renaming folders.

        :rtype: QtWidgets.QMenu
        """
        selectedFolders = self.selectedFolders()

        menu = QtWidgets.QMenu(parent)
        menu.setTitle("Edit")

        if len(selectedFolders) == 1:
            action = QtWidgets.QAction("Rename", menu)
            action.triggered.connect(self.showRenameDialog)
            menu.addAction(action)

            action = QtWidgets.QAction("Show in folder", menu)
            action.triggered.connect(self.openSelectedFolders)
            menu.addAction(action)

        separator = QtWidgets.QAction("Separator2", menu)
        separator.setSeparator(True)
        menu.addAction(separator)

        action = QtWidgets.QAction("Show icon", menu)
        action.setCheckable(True)
        action.setChecked(self.isFolderIconVisible())
        action.triggered[bool].connect(self.setFolderIconVisible)
        menu.addAction(action)

        action = QtWidgets.QAction("Show bold", menu)
        action.setCheckable(True)
        action.setChecked(self.isFolderBold())
        action.triggered[bool].connect(self.setFolderBold)

        menu.addAction(action)
        separator = QtWidgets.QAction("Separator2", menu)
        separator.setSeparator(True)
        menu.addAction(separator)

        action = QtWidgets.QAction("Change icon", menu)
        action.triggered.connect(self.browseFolderIcon)
        menu.addAction(action)

        action = QtWidgets.QAction("Change color", menu)
        action.triggered.connect(self.browseFolderColor)
        menu.addAction(action)

        separator = QtWidgets.QAction("Separator3", menu)
        separator.setSeparator(True)
        menu.addAction(separator)

        action = QtWidgets.QAction("Reset settings", menu)
        action.triggered.connect(self.resetFolderSettings)
        menu.addAction(action)

        return menu

    def setDpi(self, dpi):
        size = 24 * dpi
        self.setIndentation(15 * dpi)
        self.setMinimumWidth(35 * dpi)
        self.setIconSize(QtCore.QSize(size, size))
        self.setStyleSheet("height: {size}".format(size=size))

    def openSelectedFolders(self):
        folders = self.selectedFolders()
        for folder in folders:
            folder.openLocation()

    def reload(self):
        """
        Force the root path and state to be reloaded.

        :rtype: None
        """
        path = self.rootPath()
        settings = self.settings()
        ignoreFilter = self.ignoreFilter()

        self.setRootPath("")
        self.setRootPath(path)
        self.setSettings(settings)
        self.setIgnoreFilter(ignoreFilter)

    def setLocked(self, value):
        """
        :rtype: bool
        """
        self._isLocked = value

    def isLocked(self):
        """
        :rtype: bool
        """
        return self._isLocked

    def folderFromIndex(self, index):
        """
        :type index: QtCore.QModelIndex
        :rtype: Folder
        """
        path = self.pathFromIndex(index)
        return self.folderFromPath(path)

    def folderFromPath(self, path):
        """
        :type path: str
        :rtype: Folder
        """
        folders = self._folders
        if path not in folders:
            folders[path] = folderitem.FolderItem(path, self)
        return folders[path]

    def indexFromFolder(self, folder):
        """
        :type path: FolderItem
        :rtype: QtCore.QModelIndex
        """
        return self.indexFromPath(folder.path())

    def indexFromPath(self, path):
        """
        :type path: str
        :rtype: QtCore.QModelIndex
        """
        index = self.model().sourceModel().index(path)
        return self.model().mapFromSource(index)

    def pathFromIndex(self, index):
        """
        :type index: QtCore.QModelIndex
        :rtype: str
        """
        index = self.model().mapToSource(index)
        return self.model().sourceModel().filePath(index)

    def _selectionChanged(self, selected, deselected):
        """
        :type selected: list[Folder]
        :type deselected: list[Folder]
        :rtype: None
        """
        if self._signalsEnabled:
            self.itemSelectionChanged.emit()

    def saveSettings(self, path):
        data = self.settings()
        studioqt.saveJson(path, data)

    def loadSettings(self, path):
        if os.path.exists(path):
            data = studioqt.readJson(path)
            self.setSettings(data)

    def folderSettings(self):
        settings = {}

        for folder in self.folders():
            if folder.settings() and folder.exists():
                settings[folder.path()] = folder.settings()

        return settings

    def setFolderSettings(self, settings):
        for path in settings:
            folder = self.folderFromPath(path)
            folder.setSettings(settings[path])

    def settings(self):
        """
        :rtype: dict
        """
        settings = {
            "selectedPaths": self.selectedPaths(),
            # Saving the state of expanded folders is not supported yet!
            # "expandedPaths": self.expandedPaths(),
            # "folderSettings": self.folderSettings(),
        }

        if self._enableFolderSettings:
            settings["folderSettings"] = self.folderSettings()

        return settings

    def setSettings(self, settings):
        """
        :rtype state: list
        """
        # Saving the state of expanded folders is not supported yet!
        # expandedPaths = settings.get("expandedPaths", [])
        # self.expandPaths(expandedPaths)

        selectedPaths = settings.get("selectedPaths", [])
        self.selectPaths(selectedPaths)

        if self._enableFolderSettings:
            folderSettings = settings.get("folderSettings", {})
            self.setFolderSettings(folderSettings)

    def setFolderOrderIndex(self, path, orderIndex):
        """
        :type path:
        :type: position:
        :rtype: None
        """
        folder = self.folderFromPath(path)
        folder.setOrderIndex(orderIndex)

    def setIgnoreFilter(self, ignoreFilter):
        """
        :type ignoreFilter: list[str]
        """
        self.model().sourceModel().setIgnoreFilter(ignoreFilter)

    def ignoreFilter(self):
        return self.model().sourceModel().ignoreFilter()

    def setRootPath(self, path):
        """
        :type path: str
        """
        self.model().sourceModel().setRootPath(path)
        index = self.indexFromPath(path)
        self.setRootIndex(index)

    def rootPath(self):
        """
        :rtype: str
        """
        return self.model().sourceModel().rootPath()

    def selectFolders(self, folders):
        """
        :type folders: list[Folder]
        :rtype: None
        """
        paths = [folder.path() for folder in folders]
        self.selectPaths(paths)

    def selectPaths(self, paths):
        """
        :type paths: list[str]
        :rtype: None
        """
        if not paths:
            return

        self._signalsEnabled = False
        for path in paths[:-1]:
            self.selectPath(path)
        self._signalsEnabled = True
        self.selectPath(paths[-1])

    def selectFolder(self, folder, mode=QtCore.QItemSelectionModel.Select):
        """
        :type folder: Folder
        :rtype: None
        """
        self.selectPath(folder.path(), mode=mode)

    def expandedPaths(self):
        """
        Return the expanded folder paths.

        :rtype: list[str]
        """
        return [folder.path() for folder in self.expandedFolders()]

    def expandedFolders(self):
        """
        Return the expanded folder paths.

        :rtype: list[studioqt.FolderItem]
        """
        folders = []

        for folder in self.folders():
            index = self.indexFromPath(folder.path())
            if self.isExpanded(index):
                folders.append(folder)

        return folders

    def expandPaths(self, paths):
        self._signalsEnabled = False
        for path in paths:
            self.expandParentsFromPath(path)
        self._signalsEnabled = True

    def expandParentsFromPath(self, path):
        """
        :type path: str
        :rtype: None
        """
        for i in range(0, 4):
            path = os.path.dirname(path)
            index = self.indexFromPath(path)
            if index and not self.isExpanded(index):
                self.setExpanded(index, True)

    def selectPath(self, path, mode=QtCore.QItemSelectionModel.Select):
        """
        Select the given folders.

        :type path: str
        :rtype: Nones
        """
        isSelected = path in self.selectedPaths()

        if not isSelected:
            self.expandParentsFromPath(path)
            index = self.indexFromPath(path)
            self.selectionModel().select(index, mode)

    def showCreateDialog(self, parent=None):
        """
        :rtype: None
        """
        name, accepted = QtWidgets.QInputDialog.getText(
            parent, "Create Folder", "Folder name", QtWidgets.QLineEdit.Normal)
        name = name.strip()

        if accepted and name:
            folders = self.selectedFolders()

            if len(folders) == 1:
                folder = folders[-1]
                path = folder.path() + "/" + name
            else:
                path = self.rootPath() + "/" + name

            if not os.path.exists(path):
                os.makedirs(path)

            folder = self.folderFromPath(path)

            self.reload()
            self.clearSelection()
            self.selectFolder(folder)

    def showRenameDialog(self, parent=None):
        """
        :rtype: None
        """
        parent = parent or self.parent()

        folder = self.selectedFolder()
        if folder:
            name, accept = QtWidgets.QInputDialog.getText(
                parent, "Rename Folder", "New Name",
                QtWidgets.QLineEdit.Normal, folder.name())

            if accept:
                self.renameFolder(folder, str(name))

    def renameFolder(self, folder, name):
        """
        :type folder: Folder
        :type name: str
        """
        oldPath = folder.path()
        folder.rename(str(name))
        newPath = folder.path()

        del self._folders[oldPath]
        self._folders[newPath] = folder

        self.reload()
        self.selectPath(newPath)

    def selectedFolder(self):
        """
        :rtype: None | Folder
        """
        folders = self.selectedFolders()
        if folders:
            return folders[-1]
        return None

    def selectedPaths(self):
        return [folder.path() for folder in self.selectedFolders()]

    def selectedFolders(self):
        """
        :rtype: list[Folder]
        """

        folders = []

        for index in self.selectionModel().selectedIndexes():
            path = self.pathFromIndex(index)
            folder = self.folderFromPath(path)
            folders.append(folder)

        return folders

    def dragEnterEvent(self, event):
        """
        :type event: QtCore.QEvent
        :rtype: None
        """
        event.accept()

    def clearSelection(self):
        """
        :rtype: None
        """
        self.selectionModel().clearSelection()

    def dragMoveEvent(self, event):
        """
        :type event: QtCore.QEvent
        :rtype: None
        """
        mimeData = event.mimeData()

        if mimeData.hasUrls() and not self.isLocked():
            event.accept()
        else:
            event.ignore()

        folder = self.folderAt(event.pos())
        if folder:
            self.selectFolder(folder,
                              QtCore.QItemSelectionModel.ClearAndSelect)

    def dropEvent(self, event):
        """
        :type event: QtCore.QEvent
        :rtype: None
        """
        if self.isLocked():
            logger.debug("Folder is locked! Cannot accept drop!")
            return

        self.itemDropped.emit(event)

    def mouseMoveEvent(self, event):

        if studioqt.isControlModifier():
            return

        folder = self.folderAt(event.pos())
        selectedFolders = self.selectedFolders()
        isSelected = folder in selectedFolders

        if folder:
            self.clearSelection()
            self.selectFolder(folder)

    def mousePressEvent(self, event):
        """
        :type event: QtCore.QEvent
        :rtype: None
        """
        folder = self.folderAt(event.pos())
        selectedFolders = self.selectedFolders()
        isSelected = folder in selectedFolders

        if event.button() == QtCore.Qt.RightButton:
            QtWidgets.QTreeView.mousePressEvent(self, event)
        else:
            QtWidgets.QTreeView.mousePressEvent(self, event)

        if not folder:
            self.clearSelection()

        elif event.button(
        ) == QtCore.Qt.LeftButton and studioqt.isControlModifier():

            if folder and isSelected:
                self.clearSelection()
                selectedFolders.remove(folder)
                self.selectFolders(selectedFolders)

            if folder and not isSelected:
                selectedFolders.append(folder)
                self.selectFolders(selectedFolders)

        self.repaint()

    def mouseReleaseEvent(self, event):
        """
        :type event: QtCore.QEvent
        :rtype: None
        """
        if event.button() == QtCore.Qt.MidButton:
            event.ignore()
        else:
            QtWidgets.QTreeView.mouseReleaseEvent(self, event)

    def showContextMenu(self):
        """
        :rtype: None
        """
        menu = QtWidgets.QMenu(self)

        if self.isLocked():
            self.lockedMenu(menu)
        else:
            self.createContextMenu(menu)

        action = menu.exec_(QtGui.QCursor.pos())
        menu.close()

        return action

    def lockedMenu(self, menu):
        """
        :type menu: QtWidgets.QMenu
        :rtype: None
        """
        action = QtWidgets.QAction("Locked", menu)
        action.setEnabled(False)
        menu.addAction(action)

    def createContextMenu(self, menu):
        """
        :type menu: QtWidgets.QMenu
        :rtype: None
        """
        folders = self.selectedFolders()

        if not folders:
            action = menu.addAction("No folder selected")
            action.setEnabled(False)
            return

        editMenu = self.createEditMenu(menu)
        menu.addMenu(editMenu)

        return menu

    def folderAt(self, pos):
        """
        :type pos: QtGui.QPoint
        :rtype: None or Folder
        """
        index = self.indexAt(pos)
        if not index.isValid():
            return

        path = self.pathFromIndex(index)
        folder = self.folderFromPath(path)
        return folder

    def setFolderIconVisible(self, value):
        """
        :type value: Bool
        """
        for folder in self.selectedFolders():
            folder.setIconVisible(value)

    def isFolderIconVisible(self):
        """
        :rtype: bool
        """
        for folder in self.selectedFolders():
            if not folder.isIconVisible():
                return False
        return True

    def setFolderBold(self, value):
        """
        :type value: Bool
        """
        for folder in self.selectedFolders():
            folder.setBold(value)

    def isFolderBold(self):
        """
        :rtype: bool
        """
        for folder in self.selectedFolders():
            if not folder.isBold():
                return False
        return True

    def setFolderColor(self, color):
        """
        :type color:
        :return:
        """
        for folder in self.selectedFolders():
            folder.setColor(color)

    def resetFolderSettings(self):
        """
        :rtype:
        """
        for folder in self.selectedFolders():
            folder.reset()

    def browseFolderIcon(self):
        """
        :rtype: None
        """
        path, ext = QtWidgets.QFileDialog.getOpenFileName(
            self.parent(), "Select an image", "", "*.png")

        path = str(path).replace("\\", "/")
        if path:
            for folder in self.selectedFolders():
                folder.setIconPath(path)

    def browseFolderColor(self):
        """
        :rtype: None
        """
        dialog = QtWidgets.QColorDialog(self.parent())
        dialog.currentColorChanged.connect(self.setFolderColor)

        # PySide2 doesn't support d.open(), so we need to pass a blank slot.
        dialog.open(self, QtCore.SLOT("blankSlot()"))

        if dialog.exec_():
            self.setFolderColor(dialog.selectedColor())

    @QtCore.Slot()
    def blankSlot(self):
        """
        Blank slot to fix an issue with PySide2.QColorDialog.open()
        """
        pass
Esempio n. 29
0
class ColorPickerWidget(QtWidgets.QFrame):

    COLOR_BUTTON_CLASS = ColorButton

    colorChanged = QtCore.Signal(object)

    def __init__(self, *args):
        QtWidgets.QFrame.__init__(self, *args)

        self._buttons = []
        self._currentColor = None
        self._browserColors = None

        layout = QtWidgets.QHBoxLayout()
        layout.setSpacing(0)
        layout.setContentsMargins(0, 0, 0, 0)
        self.setLayout(layout)

    def _colorChanged(self, color):
        """
        Triggered when the user clicks or browses for a color.

        :type color: studioqt.Color
        :rtype: None
        """
        self._currentColor = color
        self.colorChanged.emit(color)

    def deleteButtons(self):
        """
        Delete all the color buttons.

        :rtype: None
        """
        layout = self.layout()
        while layout.count():
            item = layout.takeAt(0)
            item.widget().deleteLater()

    def currentColor(self):
        """
        Return the current color.

        :rtype: studioqt.Color
        """
        return self._currentColor

    def setCurrentColor(self, color):
        """
        Set the current color.

        :type color: studioqt.Color
        """
        self._currentColor = color

    def setColors(self, colors):
        """
        Set the colors for the color bar.

        :type colors: list[str] or list[studioqt.Color]
        """
        self.deleteButtons()

        self.layout().addStretch()

        for color in colors:

            if not isinstance(color, str):
                color = studioqt.Color(color)
                color = color.toString()

            callback = partial(self._colorChanged, color)
            css = "background-color: " + color

            button = self.COLOR_BUTTON_CLASS(self)
            button.setObjectName('colorButton')
            button.setStyleSheet(css)
            button.setSizePolicy(QtWidgets.QSizePolicy.Preferred,
                                 QtWidgets.QSizePolicy.Preferred)
            button.clicked.connect(callback)
            self.layout().addWidget(button)

        button = QtWidgets.QPushButton("...", self)
        button.setObjectName('browseColorButton')
        button.setSizePolicy(QtWidgets.QSizePolicy.Preferred,
                             QtWidgets.QSizePolicy.Preferred)

        button.clicked.connect(self.browseColor)
        self.layout().addWidget(button)
        self.layout().addStretch()

    def setBrowserColors(self, colors):
        """
        :type colors: list((int,int,int))
        """
        self._browserColors = colors

    def browserColors(self):
        """
        Get the colors to be displayed in the browser
    
        :rtype: list[studioqt.Color]
        """
        return self._browserColors

    @QtCore.Slot()
    def blankSlot(self):
        """Blank slot to fix an issue with PySide2.QColorDialog.open()"""
        pass

    def browseColor(self):
        """
        Show the color dialog.

        :rtype: None
        """
        color = self.currentColor()

        d = QtWidgets.QColorDialog(self)
        d.setCurrentColor(color)

        standardColors = self.browserColors()

        if standardColors:
            index = -1
            for standardColor in standardColors:
                index += 1

                try:
                    # Support for new qt5 signature
                    standardColor = QtGui.QColor(standardColor)
                    d.setStandardColor(index, standardColor)
                except:
                    # Support for new qt4 signature
                    standardColor = QtGui.QColor(standardColor).rgba()
                    d.setStandardColor(index, standardColor)

        d.currentColorChanged.connect(self._colorChanged)

        # PySide2 doesn't support d.open(), so we need to pass a blank slot.
        d.open(self, QtCore.SLOT("blankSlot()"))

        if d.exec_():
            self._colorChanged(d.selectedColor())
        else:
            self._colorChanged(color)
Esempio n. 30
0
class ItemsWidget(QtWidgets.QWidget):

    IconMode = "icon"
    TableMode = "table"

    DEFAULT_PADDING = 5

    DEFAULT_ZOOM_AMOUNT = 90
    DEFAULT_TEXT_HEIGHT = 20
    DEFAULT_WHEEL_SCROLL_STEP = 2

    DEFAULT_MIN_SPACING = 0
    DEFAULT_MAX_SPACING = 50

    DEFAULT_MIN_LIST_SIZE = 15
    DEFAULT_MIN_ICON_SIZE = 50

    itemClicked = QtCore.Signal(object)
    itemDoubleClicked = QtCore.Signal(object)

    zoomChanged = QtCore.Signal(object)
    spacingChanged = QtCore.Signal(object)

    groupClicked = QtCore.Signal(object)

    def __init__(self, *args):
        QtWidgets.QWidget.__init__(self, *args)

        self._dpi = 1
        self._padding = self.DEFAULT_PADDING

        w, h = self.DEFAULT_ZOOM_AMOUNT, self.DEFAULT_ZOOM_AMOUNT

        self._iconSize = QtCore.QSize(w, h)
        self._zoomAmount = self.DEFAULT_ZOOM_AMOUNT
        self._isItemTextVisible = True

        self._dataset = None
        self._treeWidget = TreeWidget(self)

        self._listView = ListView(self)
        self._listView.setTreeWidget(self._treeWidget)

        self._delegate = ItemDelegate()
        self._delegate.setItemsWidget(self)

        self._listView.setItemDelegate(self._delegate)
        self._treeWidget.setItemDelegate(self._delegate)

        self._toastWidget = ToastWidget(self)
        self._toastWidget.hide()
        self._toastEnabled = True

        self._textColor = QtGui.QColor(255, 255, 255, 200)
        self._textSelectedColor = QtGui.QColor(255, 255, 255, 200)
        self._backgroundColor = QtGui.QColor(255, 255, 255, 30)
        self._backgroundHoverColor = QtGui.QColor(255, 255, 255, 35)
        self._backgroundSelectedColor = QtGui.QColor(30, 150, 255)

        layout = QtWidgets.QHBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.addWidget(self._treeWidget)
        layout.addWidget(self._listView)

        header = self.treeWidget().header()
        header.sortIndicatorChanged.connect(self._sortIndicatorChanged)

        self.setLayout(layout)

        self.listView().itemClicked.connect(self._itemClicked)
        self.listView().itemDoubleClicked.connect(self._itemDoubleClicked)

        self.treeWidget().itemClicked.connect(self._itemClicked)
        self.treeWidget().itemDoubleClicked.connect(self._itemDoubleClicked)

        self.itemMoved = self._listView.itemMoved
        self.itemDropped = self._listView.itemDropped
        self.itemSelectionChanged = self._treeWidget.itemSelectionChanged

    def _sortIndicatorChanged(self):
        """
        Triggered when the sort indicator changes.

        :rtype: None
        """
        pass

    def _itemClicked(self, item):
        """
        Triggered when the given item has been clicked.

        :type item: studioqt.ItemsWidget
        :rtype: None
        """
        if isinstance(item, GroupItem):
            self.groupClicked.emit(item)
        else:
            self.itemClicked.emit(item)

    def _itemDoubleClicked(self, item):
        """
        Triggered when the given item has been double clicked.

        :type item: studioqt.Item
        :rtype: None
        """
        self.itemDoubleClicked.emit(item)

    def setDataset(self, dataset):
        self._dataset = dataset
        self.setColumnLabels(dataset.Fields)
        dataset.searchFinished.connect(self.updateItems)

    def dataset(self):
        return self._dataset

    def updateItems(self):
        """Sets the items to the widget."""
        selectedItems = self.selectedItems()
        self.clearSelection()

        results = self.dataset().groupedResults()

        items = []

        for group in results:
            if group != "None":
                groupItem = self.createGroupItem(group)
                items.append(groupItem)
            items.extend(results[group])

        self.treeWidget().setItems(items)

        if selectedItems:
            self.selectItems(selectedItems)
            self.scrollToSelectedItem()

    def createGroupItem(self, text, children=None):
        """
        Create a new group item for the given text and children.

        :type text: str
        :type children: list[studioqt.Item]
        
        :rtype: GroupItem
        """
        groupItem = GroupItem()
        groupItem.setName(text)
        groupItem.setStretchToWidget(self)
        groupItem.setChildren(children)

        return groupItem

    def setToastEnabled(self, enabled):
        """
        :type enabled: bool
        :rtype: None
        """
        self._toastEnabled = enabled

    def toastEnabled(self):
        """
        :rtype: bool
        """
        return self._toastEnabled

    def showToastMessage(self, text, duration=500):
        """
        Show a toast with the given text for the given duration.

        :type text: str
        :type duration: None or int
        :rtype: None
        """
        if self.toastEnabled():
            self._toastWidget.setDuration(duration)
            self._toastWidget.setText(text)
            self._toastWidget.show()

    def columnFromLabel(self, *args):
        """
        Reimplemented for convenience.
        
        :return: int 
        """
        return self.treeWidget().columnFromLabel(*args)

    def setColumnHidden(self, column, hidden):
        """
        Reimplemented for convenience.

        Calls self.treeWidget().setColumnHidden(column, hidden)
        """
        self.treeWidget().setColumnHidden(column, hidden)

    def setLocked(self, value):
        """
        Disables drag and drop.

        :Type value: bool
        :rtype: None
        """
        self.listView().setDragEnabled(not value)
        self.listView().setDropEnabled(not value)

    def verticalScrollBar(self):
        """
        Return the active vertical scroll bar.
        
        :rtype: QtWidget.QScrollBar 
        """
        if self.isTableView():
            return self.treeWidget().verticalScrollBar()
        else:
            return self.listView().verticalScrollBar()

    def visualItemRect(self, item):
        """
        Return the visual rect for the item.

        :type item: QtWidgets.QTreeWidgetItem
        :rtype: QtCore.QRect
        """
        if self.isTableView():
            visualRect = self.treeWidget().visualItemRect(item)
        else:
            index = self.treeWidget().indexFromItem(item)
            visualRect = self.listView().visualRect(index)

        return visualRect

    def isItemVisible(self, item):
        """
        Return the visual rect for the item.

        :type item: QtWidgets.QTreeWidgetItem
        :rtype: bool
        """
        height = self.height()
        itemRect = self.visualItemRect(item)
        scrollBarY = self.verticalScrollBar().value()

        y = (scrollBarY - itemRect.y()) + height
        return y > scrollBarY and y < scrollBarY + height

    def scrollToItem(self, item):
        """
        Ensures that the item is visible.

        :type item: QtWidgets.QTreeWidgetItem
        :rtype: None
        """
        position = QtWidgets.QAbstractItemView.PositionAtCenter

        if self.isTableView():
            self.treeWidget().scrollToItem(item, position)
        elif self.isIconView():
            self.listView().scrollToItem(item, position)

    def scrollToSelectedItem(self):
        """
        Ensures that the item is visible.

        :rtype: None
        """
        item = self.selectedItem()
        if item:
            self.scrollToItem(item)

    def dpi(self):
        """
        return the zoom multiplier.

        Used for high resolution devices.

        :rtype: int
        """
        return self._dpi

    def setDpi(self, dpi):
        """
        Set the zoom multiplier.

        Used for high resolution devices.

        :type dpi: int
        """
        self._dpi = dpi
        self.refreshSize()

    def itemAt(self, pos):
        """
        Return the current item at the given pos.

        :type pos: QtWidgets.QPoint
        :rtype: studioqt.Item
        """
        if self.isIconView():
            return self.listView().itemAt(pos)
        else:
            return self.treeView().itemAt(pos)

    def insertItems(self, items, itemAt=None):
        """
        Insert the given items at the given itemAt position.

        :type items: list[studioqt.Item]
        :type itemAt: studioqt.Item
        :rtype: Nones
        """
        self.addItems(items)
        self.moveItems(items, itemAt=itemAt)
        self.treeWidget().setItemsSelected(items, True)

    def moveItems(self, items, itemAt=None):
        """
        Move the given items to the given itemAt position.

        :type items: list[studioqt.Item]
        :type itemAt: studioqt.Item
        :rtype: None
        """
        self.listView().moveItems(items, itemAt=itemAt)

    def listView(self):
        """
        Return the list view that contains the items.

        :rtype: ListView
        """
        return self._listView

    def treeWidget(self):
        """
        Return the tree widget that contains the items.

        :rtype: TreeWidget
        """
        return self._treeWidget

    def clear(self):
        """
        Reimplemented for convenience.

        Calls self.treeWidget().clear()
        """
        self.treeWidget().clear()

    def refresh(self):
        """Refresh the item size."""
        self.refreshSize()

    def refreshSize(self):
        """
        Refresh the size of the items.

        :rtype: None
        """
        self.setZoomAmount(self.zoomAmount() + 1)
        self.setZoomAmount(self.zoomAmount() - 1)
        self.repaint()

    def itemFromIndex(self, index):
        """
        Return a pointer to the QTreeWidgetItem assocated with the given index.

        :type index: QtCore.QModelIndex
        :rtype: QtWidgets.QTreeWidgetItem
        """
        return self._treeWidget.itemFromIndex(index)

    def textFromItems(self, *args, **kwargs):
        """
        Return all data for the given items and given column.

        :rtype: list[str]
        """
        return self.treeWidget().textFromItems(*args, **kwargs)

    def textFromColumn(self, *args, **kwargs):
        """
        Return all data for the given column.

        :rtype: list[str]
        """
        return self.treeWidget().textFromColumn(*args, **kwargs)

    def toggleTextVisible(self):
        """
        Toggle the item text visibility.

        :rtype: None
        """
        if self.isItemTextVisible():
            self.setItemTextVisible(False)
        else:
            self.setItemTextVisible(True)

    def setItemTextVisible(self, value):
        """
        Set the visibility of the item text.

        :type value: bool
        :rtype: None
        """
        self._isItemTextVisible = value
        self.refreshSize()

    def isItemTextVisible(self):
        """
        Return the visibility of the item text.

        :rtype: bool
        """
        if self.isIconView():
            return self._isItemTextVisible
        else:
            return True

    def itemTextHeight(self):
        """
        Return the height of the item text.

        :rtype: int
        """
        return self.DEFAULT_TEXT_HEIGHT * self.dpi()

    def itemDelegate(self):
        """
        Return the item delegate for the views.

        :rtype: ItemDelegate
        """
        return self._delegate

    def settings(self):
        """
        Return the current state of the widget.

        :rtype: dict
        """
        settings = {}

        settings["columnLabels"] = self.columnLabels()
        settings["padding"] = self.padding()
        settings["spacing"] = self.spacing()
        settings["zoomAmount"] = self.zoomAmount()
        settings["selectedPaths"] = self.selectedPaths()
        settings["textVisible"] = self.isItemTextVisible()
        settings.update(self.treeWidget().settings())

        return settings

    def setSettings(self, settings):
        """
        Set the current state of the widget.

        :type settings: dict
        :rtype: None
        """
        self.setToastEnabled(False)

        padding = settings.get("padding", 5)
        self.setPadding(padding)

        spacing = settings.get("spacing", 2)
        self.setSpacing(spacing)

        zoomAmount = settings.get("zoomAmount", 100)
        self.setZoomAmount(zoomAmount)

        selectedPaths = settings.get("selectedPaths", [])
        self.selectPaths(selectedPaths)

        itemTextVisible = settings.get("textVisible", True)
        self.setItemTextVisible(itemTextVisible)

        self.treeWidget().setSettings(settings)

        self.setToastEnabled(True)

        return settings

    def createCopyTextMenu(self):
        return self.treeWidget().createCopyTextMenu()

    def createItemSettingsMenu(self):

        menu = QtWidgets.QMenu("Item View", self)

        action = studioqt.SeparatorAction("View Settings", menu)
        menu.addAction(action)

        action = studioqt.SliderAction("Size", menu)
        action.slider().setMinimum(10)
        action.slider().setMaximum(200)
        action.slider().setValue(self.zoomAmount())
        action.slider().valueChanged.connect(self.setZoomAmount)
        menu.addAction(action)

        action = studioqt.SliderAction("Border", menu)
        action.slider().setMinimum(0)
        action.slider().setMaximum(20)
        action.slider().setValue(self.padding())
        action.slider().valueChanged.connect(self.setPadding)
        menu.addAction(action)
        #
        action = studioqt.SliderAction("Spacing", menu)
        action.slider().setMinimum(self.DEFAULT_MIN_SPACING)
        action.slider().setMaximum(self.DEFAULT_MAX_SPACING)
        action.slider().setValue(self.spacing())
        action.slider().valueChanged.connect(self.setSpacing)
        menu.addAction(action)

        action = studioqt.SeparatorAction("Item Options", menu)
        menu.addAction(action)

        action = QtWidgets.QAction("Show labels", menu)
        action.setCheckable(True)
        action.setChecked(self.isItemTextVisible())
        action.triggered[bool].connect(self.setItemTextVisible)
        menu.addAction(action)

        return menu

    def createSettingsMenu(self):
        """
        Create and return the settings menu for the widget.

        :rtype: QtWidgets.QMenu
        """
        menu = QtWidgets.QMenu("Item View", self)

        menu.addSeparator()

        action = QtWidgets.QAction("Show labels", menu)
        action.setCheckable(True)
        action.setChecked(self.isItemTextVisible())
        action.triggered[bool].connect(self.setItemTextVisible)
        menu.addAction(action)

        menu.addSeparator()

        copyTextMenu = self.treeWidget().createCopyTextMenu()
        menu.addMenu(copyTextMenu)

        menu.addSeparator()

        action = studioqt.SliderAction("Size", menu)
        action.slider().setMinimum(10)
        action.slider().setMaximum(200)
        action.slider().setValue(self.zoomAmount())
        action.slider().valueChanged.connect(self.setZoomAmount)
        menu.addAction(action)

        action = studioqt.SliderAction("Border", menu)
        action.slider().setMinimum(0)
        action.slider().setMaximum(20)
        action.slider().setValue(self.padding())
        action.slider().valueChanged.connect(self.setPadding)
        menu.addAction(action)
        #
        action = studioqt.SliderAction("Spacing", menu)
        action.slider().setMinimum(self.DEFAULT_MIN_SPACING)
        action.slider().setMaximum(self.DEFAULT_MAX_SPACING)
        action.slider().setValue(self.spacing())
        action.slider().valueChanged.connect(self.setSpacing)
        menu.addAction(action)

        return menu

    def createItemsMenu(self, items=None):
        """
        Create the item menu for given item.

        :rtype: QtWidgets.QMenu
        """
        item = items or self.selectedItem()

        menu = QtWidgets.QMenu(self)

        if item:
            try:
                item.contextMenu(menu)
            except Exception as error:
                logger.exception(error)
        else:
            action = QtWidgets.QAction(menu)
            action.setText("No Item selected")
            action.setDisabled(True)

            menu.addAction(action)

        return menu

    def createContextMenu(self):
        """
        Create and return the context menu for the widget.

        :rtype: QtWidgets.QMenu
        """
        menu = self.createItemsMenu()

        settingsMenu = self.createSettingsMenu()
        menu.addMenu(settingsMenu)

        return menu

    def contextMenuEvent(self, event):
        """
        Show the context menu.

        :type event: QtCore.QEvent
        :rtype: None
        """
        menu = self.createContextMenu()
        point = QtGui.QCursor.pos()
        return menu.exec_(point)

    # ------------------------------------------------------------------------
    # Support for saving the current item order.
    # ------------------------------------------------------------------------

    def itemData(self, columnLabels):
        """
        Return all column data for the given column labels.

        :type columnLabels: list[str]
        :rtype: dict
                
        """
        data = {}

        for item in self.items():
            key = item.id()

            for columnLabel in columnLabels:
                column = self.treeWidget().columnFromLabel(columnLabel)
                value = item.data(column, QtCore.Qt.EditRole)

                data.setdefault(key, {})
                data[key].setdefault(columnLabel, value)

        return data

    def setItemData(self, data):
        """
        Set the item data for all the current items.

        :type data: dict
        :rtype: None
        """
        for item in self.items():
            key = item.id()
            if key in data:
                item.setItemData(data[key])

    def updateColumns(self):
        """
        Update the column labels with the current item data.

        :rtype: None
        """
        self.treeWidget().updateHeaderLabels()

    def columnLabels(self):
        """
        Set all the column labels.

        :rtype: list[str]
        """
        return self.treeWidget().columnLabels()

    def _removeDuplicates(self, labels):
        """
        Removes duplicates from a list in Python, whilst preserving order.

        :type labels: list[str]
        :rtype: list[str]
        """
        s = set()
        sadd = s.add
        return [x for x in labels if x.strip() and not (x in s or sadd(x))]

    def setColumnLabels(self, labels):
        """
        Set the columns for the widget.

        :type labels: list[str]
        :rtype: None
        """
        labels = self._removeDuplicates(labels)

        if "Custom Order" not in labels:
            labels.append("Custom Order")

        # if "Search Order" not in labels:
        #     labels.append("Search Order")

        self.treeWidget().setHeaderLabels(labels)

        self.setColumnHidden("Custom Order", True)
        # self.setColumnHidden("Search Order", True)

    def items(self):
        """
        Return all the items in the widget.

        :rtype: list[studioqt.Item]
        """
        return self._treeWidget.items()

    def addItems(self, items):
        """
        Add the given items to the items widget.

        :type items: list[studioqt.Item]
        :rtype: None
        """
        self._treeWidget.addTopLevelItems(items)

    def addItem(self, item):
        """
        Add the item to the tree widget.

        :type item: Item
        :rtype: None
        """
        self.addItems([item])

    def columnLabelsFromItems(self):
        """
        Return the column labels from all the items.

        :rtype: list[str]
        """
        seq = []
        for item in self.items():
            seq.extend(item._textColumnOrder)

        seen = set()
        return [x for x in seq if x not in seen and not seen.add(x)]

    def refreshColumns(self):
        self.setColumnLabels(self.columnLabelsFromItems())

    def padding(self):
        """
        Return the item padding.

        :rtype: int
        """
        return self._padding

    def setPadding(self, value):
        """
        Set the item padding.

        :type: int
        :rtype: None
        """
        if value % 2 == 0:
            self._padding = value
        else:
            self._padding = value + 1
        self.repaint()

        self.showToastMessage("Border: " + str(value))

    def spacing(self):
        """
        Return the spacing between the items.

        :rtype: int
        """
        return self._listView.spacing()

    def setSpacing(self, spacing):
        """
        Set the spacing between the items.

        :type spacing: int
        :rtype: None
        """
        self._listView.setSpacing(spacing)
        self.scrollToSelectedItem()

        self.showToastMessage("Spacing: " + str(spacing))

    def iconSize(self):
        """
        Return the icon size for the views.

        :rtype: QtCore.QSize
        """
        return self._iconSize

    def setIconSize(self, size):
        """
        Set the icon size for the views.

        :type size: QtCore.QSize
        :rtype: None
        """
        self._iconSize = size
        self._listView.setIconSize(size)
        self._treeWidget.setIconSize(size)

    def clearSelection(self):
        """
        Clear the user selection.

        :rtype: None
        """
        self._treeWidget.clearSelection()

    def wheelScrollStep(self):
        """
        Return the wheel scroll step amount.

        :rtype: int
        """
        return self.DEFAULT_WHEEL_SCROLL_STEP

    def model(self):
        """
        Return the model that this view is presenting.

        :rtype: QAbstractItemModel
        """
        return self._treeWidget.model()

    def indexFromItem(self, item):
        """
        Return the QModelIndex assocated with the given item.

        :type item: QtWidgets.QTreeWidgetItem.
        :rtype: QtCore.QModelIndex
        """
        return self._treeWidget.indexFromItem(item)

    def selectionModel(self):
        """
        Return the current selection model.

        :rtype: QtWidgets.QItemSelectionModel
        """
        return self._treeWidget.selectionModel()

    def selectedItem(self):
        """
        Return the last selected non-hidden item.

        :rtype: QtWidgets.QTreeWidgetItem
        """
        return self._treeWidget.selectedItem()

    def selectedItems(self):
        """
        Return a list of all selected non-hidden items.

        :rtype: list[QtWidgets.QTreeWidgetItem]
        """
        return self._treeWidget.selectedItems()

    def setItemHidden(self, item, value):
        """
        Set the visibility of given item.

        :type item: QtWidgets.QTreeWidgetItem
        :type value: bool
        :rtype: None
        """
        item.setHidden(value)

    def setItemsHidden(self, items, value):
        """
        Set the visibility of given items.

        :type items: list[QtWidgets.QTreeWidgetItem]
        :type value: bool
        :rtype: None
        """
        for item in items:
            self.setItemHidden(item, value)

    def selectedPaths(self):
        """
        Return the selected item paths.

        :rtype: list[str]
        """
        paths = []
        for item in self.selectedItems():
            path = item.url().toLocalFile()
            paths.append(path)
        return paths

    def selectPaths(self, paths):
        """
        Selected the items that have the given paths.

        :type paths: list[str]
        :rtype: None
        """
        for item in self.items():
            path = item.id()
            if path in paths:
                item.setSelected(True)

    def selectItems(self, items):
        """
        Select the given items.

        :type items: list[studiolibrary.LibraryItem]
        :rtype: None
        """
        paths = [item.id() for item in items]
        self.selectPaths(paths)

    def isIconView(self):
        """
        Return True if widget is in Icon mode.

        :rtype: bool
        """
        return not self._listView.isHidden()

    def isTableView(self):
        """
        Return True if widget is in List mode.

        :rtype: bool
        """
        return not self._treeWidget.isHidden()

    def setViewMode(self, mode):
        """
        Set the view mode for this widget.

        :type mode: str
        :rtype: None
        """
        if mode == self.IconMode:
            self.setZoomAmount(self.DEFAULT_MIN_ICON_SIZE)
        elif mode == self.TableMode:
            self.setZoomAmount(self.DEFAULT_MIN_ICON_SIZE)

    def _setViewMode(self, mode):
        """
        Set the view mode for this widget.

        :type mode: str
        :rtype: None
        """
        if mode == self.IconMode:
            self.setIconMode()
        elif mode == self.TableMode:
            self.setListMode()

    def setListMode(self):
        """
        Set the tree widget visible.

        :rtype: None
        """
        self._listView.hide()
        self._treeWidget.show()
        self._treeWidget.setFocus()

    def setIconMode(self):
        """
        Set the list view visible.

        :rtype: None
        """
        self._treeWidget.hide()
        self._listView.show()
        self._listView.setFocus()

    def zoomAmount(self):
        """
        Return the zoom amount for the widget.

        :rtype: int
        """
        return self._zoomAmount

    def setZoomAmount(self, value):
        """
        Set the zoom amount for the widget.

        :type value: int
        :rtype: None
        """
        if value < self.DEFAULT_MIN_LIST_SIZE:
            value = self.DEFAULT_MIN_LIST_SIZE

        self._zoomAmount = value
        size = QtCore.QSize(value * self.dpi(), value * self.dpi())
        self.setIconSize(size)

        if value >= self.DEFAULT_MIN_ICON_SIZE:
            self._setViewMode(self.IconMode)
        else:
            self._setViewMode(self.TableMode)

        columnWidth = value * self.dpi() + self.itemTextHeight()

        self._treeWidget.setIndentation(0)
        self._treeWidget.setColumnWidth(0, columnWidth)
        self.scrollToSelectedItem()

        msg = "Size: {0}%".format(value)
        self.showToastMessage(msg)

    def wheelEvent(self, event):
        """
        Triggered on any wheel events for the current viewport.

        :type event: QtWidgets.QWheelEvent
        :rtype: None
        """
        modifier = QtWidgets.QApplication.keyboardModifiers()

        validModifiers = (
            QtCore.Qt.AltModifier,
            QtCore.Qt.ControlModifier,
        )

        if modifier in validModifiers:
            numDegrees = event.delta() / 8
            numSteps = numDegrees / 15

            delta = (numSteps * self.wheelScrollStep())
            value = self.zoomAmount() + delta
            self.setZoomAmount(value)

    def setTextColor(self, color):
        """
        Set the item text color.

        :type color: QtWidgets.QtColor
        """
        self._textColor = color

    def setTextSelectedColor(self, color):
        """
        Set the text color when an item is selected.

        :type color: QtWidgets.QtColor
        """
        self._textSelectedColor = color

    def setBackgroundColor(self, color):
        """
        Set the item background color.

        :type color: QtWidgets.QtColor
        """
        self._backgroundColor = color

    def setBackgroundHoverColor(self, color):
        """
        Set the background color when the mouse hovers over the item.

        :type color: QtWidgets.QtColor
        """
        self._backgroundHoverColor = color

    def setBackgroundSelectedColor(self, color):
        """
        Set the background color when an item is selected.

        :type color: QtWidgets.QtColor
        """
        self._backgroundSelectedColor = color
        self._listView.setRubberBandColor(QtGui.QColor(200, 200, 200, 255))

    def textColor(self):
        """
        Return the item text color.

        :rtype: QtGui.QColor
        """
        return self._textColor

    def textSelectedColor(self):
        """
        Return the item text color when selected.

        :rtype: QtGui.QColor
        """
        return self._textSelectedColor

    def backgroundColor(self):
        """
        Return the item background color.

        :rtype: QtWidgets.QtColor
        """
        return self._backgroundColor

    def backgroundHoverColor(self):
        """
        Return the background color for when the mouse is over an item.

        :rtype: QtWidgets.QtColor
        """
        return self._backgroundHoverColor

    def backgroundSelectedColor(self):
        """
        Return the background color when an item is selected.

        :rtype: QtWidgets.QtColor
        """
        return self._backgroundSelectedColor