Exemplo n.º 1
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)
    dataChanged = QtCore.Signal(object)
class SidebarWidget(QtWidgets.QWidget):

    itemDropped = QtCore.Signal(object)
    itemRenamed = QtCore.Signal(str, str)
    itemSelectionChanged = QtCore.Signal()
    settingsMenuRequested = QtCore.Signal(object)

    def __init__(self, *args):
        super(SidebarWidget, self).__init__(*args)

        self._dataset = None
        self._lineEdit = None
        self._previousFilterText = ""

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

        self.setLayout(layout)

        self._treeWidget = TreeWidget(self)
        self._treeWidget.itemDropped = self.itemDropped
        self._treeWidget.itemRenamed = self.itemRenamed
        self._treeWidget.itemSelectionChanged.connect(self._itemSelectionChanged)

        self._titleWidget = self.createTitleWidget()
        self._titleWidget.ui.menuButton.clicked.connect(self.showSettingsMenu)
        self._titleWidget.ui.titleButton.clicked.connect(self.clearSelection)

        self.layout().addWidget(self._titleWidget)
        self.layout().addWidget(self._treeWidget)

        self._treeWidget.installEventFilter(self)

    def _itemSelectionChanged(self, *args):
        self.itemSelectionChanged.emit()

    def eventFilter(self, obj, event):
        """Using an event filter to show the search widget on key press."""
        if event.type() == QtCore.QEvent.KeyPress:
            self._keyPressEvent(event)

        return super(SidebarWidget, self).eventFilter(obj, event)

    def _keyPressEvent(self, event):
        """
        Triggered from the tree widget key press event.

        :type event: QKeyEvent
        """
        text = event.text().strip()

        if not text.isalpha() and not text.isdigit():
            return

        if text and not self._titleWidget.ui.filterEdit.hasFocus():
                self._titleWidget.ui.filterEdit.setText(text)

        self.setFilterVisible(True)

        self._previousFilterText = text

    def _filterVisibleTrigger(self, visible):
        """
        Triggered by the filter visible action.

        :type visible: bool
        """
        self.setFilterVisible(visible)
        self._titleWidget.ui.filterEdit.selectAll()

    def createTitleWidget(self):
        """
        Create a new instance of the title bar widget.

        :rtype: QtWidgets.QFrame
        """

        class UI(object):
            """Proxy class for attaching ui widgets as properties."""
            pass

        titleWidget = QtWidgets.QFrame(self)
        titleWidget.setObjectName("titleWidget")
        titleWidget.ui = UI()

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

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

        vlayout.addLayout(hlayout)

        titleButton = QtWidgets.QPushButton(self)
        titleButton.setText("Folders")
        titleButton.setObjectName("titleButton")
        titleWidget.ui.titleButton = titleButton

        hlayout.addWidget(titleButton)

        menuButton = QtWidgets.QPushButton(self)
        menuButton.setText("...")
        menuButton.setObjectName("menuButton")
        titleWidget.ui.menuButton = menuButton

        hlayout.addWidget(menuButton)

        self._lineEdit = studiolibrary.widgets.LineEdit(self)
        self._lineEdit.hide()
        self._lineEdit.setObjectName("filterEdit")
        self._lineEdit.setText(self.treeWidget().filterText())
        self._lineEdit.textChanged.connect(self.searchChanged)
        titleWidget.ui.filterEdit = self._lineEdit

        vlayout.addWidget(self._lineEdit)

        titleWidget.setLayout(vlayout)

        return titleWidget

    def _dataChanged(self):
        pass

    def setDataset(self, dataset):
        """
        Set the dataset for the search widget:

        :type dataset: studioqt.Dataset
        """
        self._dataset = dataset
        self._dataset.dataChanged.connect(self._dataChanged)
        self._dataChanged()

    def dataset(self):
        """
        Get the dataset for the search widget.

        :rtype: studioqt.Dataset
        """
        return self._dataset

    def search(self):
        """Run the dataset search."""
        if self.dataset():
            self.dataset().addQuery(self.query())
            self.dataset().search()
        else:
            logger.info('No dataset found for the sidebar widget.')

    def query(self):
        """
        Get the query for the sidebar widget.

        :rtype: dict
        """
        filters = []

        for path in self.selectedPaths():
            if self.isRecursive():
                suffix = "" if path.endswith("/") else "/"

                filter_ = ('folder', 'startswith', path + suffix)
                filters.append(filter_)

            filter_ = ('folder', 'is', path)
            filters.append(filter_)

        uniqueName = 'sidebar_widget_' + str(id(self))
        return {'name': uniqueName, 'operator': 'or', 'filters': filters}

    def searchChanged(self, text):
        """
        Triggered when the search filter has changed.

        :type text: str
        """
        self.refreshFilter()
        if text:
            self.setFilterVisible(True)
        else:
            self.treeWidget().setFocus()
            self.setFilterVisible(False)

    def showSettingsMenu(self):
        """Create and show a new settings menu instance."""

        menu = studioqt.Menu(self)

        self.settingsMenuRequested.emit(menu)

        self.createSettingsMenu(menu)

        point = QtGui.QCursor.pos()
        point.setX(point.x() + 3)
        point.setY(point.y() + 3)
        action = menu.exec_(point)
        menu.close()

    def createSettingsMenu(self, menu):
        """
        Create a new settings menu instance.

        :rtype: QMenu
        """
        action = menu.addAction("Show Filter")
        action.setCheckable(True)
        action.setChecked(self.isFilterVisible())

        callback = functools.partial(self._filterVisibleTrigger, not self.isFilterVisible())
        action.triggered.connect(callback)

        action = menu.addAction("Show Icons")
        action.setCheckable(True)
        action.setChecked(self.iconsVisible())

        callback = functools.partial(self.setIconsVisible, not self.iconsVisible())
        action.triggered.connect(callback)

        action = menu.addAction("Show Root Folder")
        action.setCheckable(True)
        action.setChecked(self.isRootVisible())

        callback = functools.partial(self.setRootVisible, not self.isRootVisible())
        action.triggered.connect(callback)

        return menu

    def setFilterVisible(self, visible):
        """
        Set the filter widget visible

        :type visible: bool
        """
        self._titleWidget.ui.filterEdit.setVisible(visible)
        self._titleWidget.ui.filterEdit.setFocus()

        if not visible and bool(self.treeWidget().filterText()):
            self.treeWidget().setFilterText("")
        else:
            self.refreshFilter()

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

        :type settings: dict
        """
        self.treeWidget().setSettings(settings)

        value = settings.get("filterVisible")
        if value is not None:
            self.setFilterVisible(value)

        value = settings.get("filterText")
        if value is not None:
            self.setFilterText(value)

    def settings(self):
        """
        Get the settings for the widget.

        :rtype: dict
        """
        settings = self.treeWidget().settings()

        settings["filterText"]  = self.filterText()
        settings["filterVisible"]  = self.isFilterVisible()

        return settings

    # --------------------------------
    # convenience methods
    # --------------------------------

    def filterText(self):
        return self.treeWidget().filterText()

    def setFilterText(self, text):
        self._titleWidget.ui.filterEdit.setText(text)

    def refreshFilter(self):
        self.treeWidget().setFilterText(self._titleWidget.ui.filterEdit.text())

    def isFilterVisible(self):
        return bool(self.treeWidget().filterText()) or self._titleWidget.ui.filterEdit.isVisible()

    def setIconsVisible(self, visible):
        self.treeWidget().setIconsVisible(visible)

    def iconsVisible(self):
        return self.treeWidget().iconsVisible()

    def setRootVisible(self, visible):
        self.treeWidget().setRootVisible(visible)

    def isRootVisible(self):
        return self.treeWidget().isRootVisible()

    def treeWidget(self):
        return self._treeWidget

    def setDpi(self, dpi):
        self.treeWidget().setDpi(dpi)

    def setRecursive(self, enabled):
        self.treeWidget().setRecursive(enabled)

    def isRecursive(self):
        return self.treeWidget().isRecursive()

    def setData(self, *args, **kwargs):
        self.treeWidget().setData(*args, **kwargs)

    def setItemData(self, id, data):
        self.treeWidget().setPathSettings(id, data)

    def setLocked(self, locked):
        self.treeWidget().setLocked(locked)

    def selectedPath(self):
        return self.treeWidget().selectedPath()

    def selectPaths(self, paths):
        self.treeWidget().selectPaths(paths)

    def selectedPaths(self):
        return self.treeWidget().selectedPaths()

    def clearSelection(self):
        self.treeWidget().clearSelection()
Exemplo n.º 3
0
class WorkerSignals(QtCore.QObject):
    triggered = QtCore.Signal(object)
Exemplo n.º 4
0
class GlobalSignals(QtCore.QObject):
    """"""
    sliderChanged = QtCore.Signal(float)
Exemplo n.º 5
0
class SidebarWidget(QtWidgets.QWidget):

    itemDropped = QtCore.Signal(object)
    itemRenamed = QtCore.Signal(str, str)
    itemSelectionChanged = QtCore.Signal()
    settingsMenuRequested = QtCore.Signal(object)

    def __init__(self, *args):
        super(SidebarWidget, self).__init__(*args)

        self._previousFilterText = ""

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

        self.setLayout(layout)

        self._treeWidget = TreeWidget(self)
        self._treeWidget.itemDropped = self.itemDropped
        self._treeWidget.itemRenamed = self.itemRenamed
        self._treeWidget.itemSelectionChanged = self.itemSelectionChanged
        self._treeWidget.keyPressed.connect(self._keyPressTrigger)

        self._titleWidget = self.createTitleWidget()
        self._titleWidget.ui.menuButton.clicked.connect(self.showSettingsMenu)
        self._titleWidget.ui.titleButton.clicked.connect(self.clearSelection)

        self.layout().addWidget(self._titleWidget)
        self.layout().addWidget(self._treeWidget)

    def _keyPressTrigger(self, event):
        """
        Triggered from the tree widget key press event.

        :type event: QKeyEvent
        """
        text = event.text().strip()

        if event.key() == QtCore.Qt.Key_Backspace:
            return

        if text and not self._titleWidget.ui.filterEdit.hasFocus():
                self._titleWidget.ui.filterEdit.setText(text)

        self.setFilterVisible(True)

        self._previousFilterText = text

    def _filterVisibleTrigger(self, visible):
        """
        Triggered by the filter visible action.

        :type visible: bool
        """
        self.setFilterVisible(visible)
        self._titleWidget.ui.filterEdit.selectAll()

    def createTitleWidget(self):
        """
        Create a new instance of the title bar widget.

        :rtype: QtWidgets.QFrame
        """

        class UI(object):
            """Proxy class for attaching ui widgets as properties."""
            pass

        titleWidget = QtWidgets.QFrame(self)
        titleWidget.setObjectName("titleWidget")
        titleWidget.ui = UI()

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

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

        vlayout.addLayout(hlayout)

        titleButton = QtWidgets.QPushButton(self)
        titleButton.setText("Folders")
        titleButton.setObjectName("titleButton")
        titleWidget.ui.titleButton = titleButton

        hlayout.addWidget(titleButton)

        menuButton = QtWidgets.QPushButton(self)
        menuButton.setText("...")
        menuButton.setObjectName("menuButton")
        titleWidget.ui.menuButton = menuButton

        hlayout.addWidget(menuButton)

        lineEdit = QtWidgets.QLineEdit(self)
        lineEdit.hide()
        lineEdit.setObjectName("filterEdit")
        lineEdit.setText(self.treeWidget().filterText())
        lineEdit.textChanged.connect(self.searchChanged)
        titleWidget.ui.filterEdit = lineEdit

        vlayout.addWidget(lineEdit)

        titleWidget.setLayout(vlayout)

        return titleWidget

    def searchChanged(self, text):
        """
        Triggered when the search filter has changed.

        :type text: str
        """
        self.refreshFilter()
        if text:
            self.setFilterVisible(True)
        else:
            self.treeWidget().setFocus()
            self.setFilterVisible(False)

    def showSettingsMenu(self):
        """Create and show a new settings menu instance."""

        menu = studioqt.Menu(self)

        self.settingsMenuRequested.emit(menu)

        self.createSettingsMenu(menu)

        point = QtGui.QCursor.pos()
        point.setX(point.x() + 3)
        point.setY(point.y() + 3)
        action = menu.exec_(point)
        menu.close()

    def createSettingsMenu(self, menu):
        """
        Create a new settings menu instance.

        :rtype: QMenu
        """
        action = menu.addAction("Show Filter")
        action.setCheckable(True)
        action.setChecked(self.isFilterVisible())

        callback = functools.partial(self._filterVisibleTrigger, not self.isFilterVisible())
        action.triggered.connect(callback)

        action = menu.addAction("Show Icons")
        action.setCheckable(True)
        action.setChecked(self.iconsVisible())

        callback = functools.partial(self.setIconsVisible, not self.iconsVisible())
        action.triggered.connect(callback)

        action = menu.addAction("Show Root Folder")
        action.setCheckable(True)
        action.setChecked(self.isRootVisible())

        callback = functools.partial(self.setRootVisible, not self.isRootVisible())
        action.triggered.connect(callback)

        return menu

    def setFilterVisible(self, visible):
        """
        Set the filter widget visible

        :type visible: bool
        """
        self._titleWidget.ui.filterEdit.setVisible(visible)
        self._titleWidget.ui.filterEdit.setFocus()

        if not visible and bool(self.treeWidget().filterText()):
            self.treeWidget().setFilterText("")
        else:
            self.refreshFilter()

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

        :type settings: dict
        """
        self.treeWidget().setSettings(settings)

        value = settings.get("filterVisible")
        if value is not None:
            self.setFilterVisible(value)

        value = settings.get("filterText")
        if value is not None:
            self.setFilterText(value)

    def settings(self):
        """
        Get the settings for the widget.

        :rtype: dict
        """
        settings = self.treeWidget().settings()

        settings["filterText"]  = self.filterText()
        settings["filterVisible"]  = self.isFilterVisible()

        return settings

    # --------------------------------
    # convenience methods
    # --------------------------------

    def filterText(self):
        return self.treeWidget().filterText()

    def setFilterText(self, text):
        self._titleWidget.ui.filterEdit.setText(text)

    def refreshFilter(self):
        self.treeWidget().setFilterText(self._titleWidget.ui.filterEdit.text())

    def isFilterVisible(self):
        return bool(self.treeWidget().filterText()) or self._titleWidget.ui.filterEdit.isVisible()

    def setIconsVisible(self, visible):
        self.treeWidget().setIconsVisible(visible)

    def iconsVisible(self):
        return self.treeWidget().iconsVisible()

    def setRootVisible(self, visible):
        self.treeWidget().setRootVisible(visible)

    def isRootVisible(self):
        return self.treeWidget().isRootVisible()

    def treeWidget(self):
        return self._treeWidget

    def setDpi(self, dpi):
        self.treeWidget().setDpi(dpi)

    def setRecursive(self, enabled):
        self.treeWidget().setRecursive(enabled)

    def isRecursive(self):
        return self.treeWidget().isRecursive()

    def setItemData(self, id, data):
        self.treeWidget().setPathSettings(id, data)

    def setData(self, *args, **kwargs):
        self.treeWidget().setData(*args, **kwargs)

    def setLocked(self, locked):
        self.treeWidget().setLocked(locked)

    def setDataset(self, dataset):
        self.treeWidget().setDataset(dataset)

    def selectedPath(self):
        return self.treeWidget().selectedPath()

    def selectPaths(self, paths):
        self.treeWidget().selectPaths(paths)

    def selectedPaths(self):
        return self.treeWidget().selectedPaths()

    def clearSelection(self):
        self.treeWidget().clearSelection()
Exemplo n.º 6
0
class ListView(ItemViewMixin, QtWidgets.QListView):

    itemMoved = QtCore.Signal(object)
    itemDropped = QtCore.Signal(object)
    itemClicked = QtCore.Signal(object)
    itemDoubleClicked = QtCore.Signal(object)

    DEFAULT_DRAG_THRESHOLD = 10

    def __init__(self, *args):
        QtWidgets.QListView.__init__(self, *args)
        ItemViewMixin.__init__(self)

        self._treeWidget = None
        self._rubberBand = None
        self._rubberBandStartPos = None
        self._rubberBandColor = QtGui.QColor(QtCore.Qt.white)
        self._customSortOrder = []

        self._drag = None
        self._dragStartPos = None
        self._dragStartIndex = None
        self._dropEnabled = True

        self.setSpacing(5)

        self.setMouseTracking(True)
        self.setSelectionRectVisible(True)
        self.setViewMode(QtWidgets.QListView.IconMode)
        self.setResizeMode(QtWidgets.QListView.Adjust)
        self.setSelectionMode(QtWidgets.QListWidget.ExtendedSelection)
        self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)

        self.setAcceptDrops(True)
        self.setDragEnabled(True)
        self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop)

        self.clicked.connect(self._indexClicked)
        self.doubleClicked.connect(self._indexDoubleClicked)

    def _indexClicked(self, index):
        """
        Triggered when the user clicks on an index.

        :type index: QtCore.QModelIndex
        :rtype: None
        """
        item = self.itemFromIndex(index)
        item.clicked()
        self.setItemsSelected([item], True)
        self.itemClicked.emit(item)

    def _indexDoubleClicked(self, index):
        """
        Triggered when the user double clicks on an index.

        :type index: QtCore.QModelIndex
        :rtype: None
        """
        item = self.itemFromIndex(index)
        self.setItemsSelected([item], True)
        item.doubleClicked()
        self.itemDoubleClicked.emit(item)

    def treeWidget(self):
        """
        Return the tree widget that contains the item.

        :rtype: QtWidgets.QTreeWidget
        """
        return self._treeWidget

    def setTreeWidget(self, treeWidget):
        """
        Set the tree widget that contains the item.

        :type treeWidget: QtWidgets.QTreeWidget
        :rtype: None
        """
        self._treeWidget = treeWidget
        self.setModel(treeWidget.model())
        self.setSelectionModel(treeWidget.selectionModel())

    def scrollToItem(self, item, pos=None):
        """
        Ensures that the item is visible.

        :type item: QtWidgets.QTreeWidgetItem
        :type pos: QtCore.QPoint or None
        :rtype: None
        """
        index = self.indexFromItem(item)
        pos = pos or QtWidgets.QAbstractItemView.PositionAtCenter

        self.scrollTo(index, pos)

    def items(self):
        """
        Return all the items.

        :rtype: list[QtWidgets.QTreeWidgetItem]
        """
        return self.treeWidget().items()

    def itemAt(self, pos):
        """
        Return a pointer to the item at the coordinates p.

        The coordinates are relative to the tree widget's viewport().

        :type pos: QtCore.QPoint
        :rtype: QtWidgets.QTreeWidgetItem
        """
        index = self.indexAt(pos)
        return self.itemFromIndex(index)

    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 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 insertItem(self, row, item):
        """
        Inserts the item at row in the top level in the view.

        :type row: int
        :type item: QtWidgets.QTreeWidgetItem
        :rtype: None
        """
        self.treeWidget().insertTopLevelItem(row, item)

    def takeItems(self, items):
        """
        Removes and returns the items from the view

        :type items: list[QtWidgets.QTreeWidgetItem]
        :rtype: list[QtWidgets.QTreeWidgetItem]
        """
        for item in items:
            row = self.treeWidget().indexOfTopLevelItem(item)
            self.treeWidget().takeTopLevelItem(row)

        return items

    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 setIndexesSelected(self, indexes, value):
        """
        Set the selected state for the given indexes.

        :type indexes: list[QtCore.QModelIndex]
        :type value: bool
        :rtype: None
        """
        items = self.itemsFromIndexes(indexes)
        self.setItemsSelected(items, value)

    def setItemsSelected(self, items, value):
        """
        Set the selected state for the given items.

        :type items: list[studioqt.WidgetItem]
        :type value: bool
        :rtype: None
        """
        self.treeWidget().blockSignals(True)
        for item in items:
            self.treeWidget().setItemSelected(item, value)
        self.treeWidget().blockSignals(False)

    def moveItems(self, items, itemAt):
        """
        Move the given items to the position of the destination row.

        :type items: list[studioqt.Item]
        :type itemAt: studioqt.Item
        :rtype: None
        """
        scrollValue = self.verticalScrollBar().value()

        self.treeWidget().moveItems(items, itemAt)
        self.itemMoved.emit(items[-1])

        self.verticalScrollBar().setValue(scrollValue)

    # ---------------------------------------------------------------------
    # Support for a custom colored rubber band.
    # ---------------------------------------------------------------------

    def createRubberBand(self):
        """
        Create a new instance of the selection rubber band.

        :rtype: QtWidgets.QRubberBand
        """
        rubberBand = QtWidgets.QRubberBand(QtWidgets.QRubberBand.Rectangle,
                                           self)
        palette = QtGui.QPalette()
        color = self.rubberBandColor()
        palette.setBrush(QtGui.QPalette.Highlight, QtGui.QBrush(color))
        rubberBand.setPalette(palette)
        return rubberBand

    def setRubberBandColor(self, color):
        """
        Set the color for the rubber band.

        :type color: QtGui.QColor
        :rtype: None
        """
        self._rubberBand = None
        self._rubberBandColor = color

    def rubberBandColor(self):
        """
        Return the rubber band color for this widget.

        :rtype: QtGui.QColor
        """
        return self._rubberBandColor

    def rubberBand(self):
        """
        Return the selection rubber band for this widget.

        :rtype: QtWidgets.QRubberBand
        """
        if not self._rubberBand:
            self.setSelectionRectVisible(False)
            self._rubberBand = self.createRubberBand()

        return self._rubberBand

    # ---------------------------------------------------------------------
    # Events
    # ---------------------------------------------------------------------

    def validateDragEvent(self, event):
        """
        Validate the drag event.

        :type event: QtWidgets.QMouseEvent
        :rtype: bool
        """
        return QtCore.Qt.LeftButton == event.mouseButtons()

    def mousePressEvent(self, event):
        """
        Triggered when the user presses the mouse button for the viewport.

        :type event: QtWidgets.QMouseEvent
        :rtype: None
        """
        item = self.itemAt(event.pos())
        if not item:
            self.clearSelection()

        ItemViewMixin.mousePressEvent(self, event)
        if event.isAccepted():
            QtWidgets.QListView.mousePressEvent(self, event)
            self.itemsWidget().treeWidget().setItemSelected(item, True)

        self.endDrag()
        self._dragStartPos = event.pos()

        isLeftButton = self.mousePressButton() == QtCore.Qt.LeftButton
        isItemDraggable = item and item.dragEnabled()
        isSelectionEmpty = not self.selectedItems()

        if isLeftButton and (isSelectionEmpty or not isItemDraggable):
            self.rubberBandStartEvent(event)

    def mouseMoveEvent(self, event):
        """
        Triggered when the user moves the mouse over the current viewport.

        :type event: QtWidgets.QMouseEvent
        :rtype: None
        """
        if not self.isDraggingItems():

            isLeftButton = self.mousePressButton() == QtCore.Qt.LeftButton

            if isLeftButton and self.rubberBand().isHidden(
            ) and self.selectedItems():
                self.startDrag(event)
            else:
                ItemViewMixin.mouseMoveEvent(self, event)
                QtWidgets.QListView.mouseMoveEvent(self, event)

            if isLeftButton:
                self.rubberBandMoveEvent(event)

    def mouseReleaseEvent(self, event):
        """
        Triggered when the user releases the mouse button for this viewport.

        :type event: QtWidgets.QMouseEvent
        :rtype: None
        """
        item = self.itemAt(event.pos())
        items = self.selectedItems()

        ItemViewMixin.mouseReleaseEvent(self, event)

        if item not in items:
            if event.button() != QtCore.Qt.MidButton:
                QtWidgets.QListView.mouseReleaseEvent(self, event)
        elif not items:
            QtWidgets.QListView.mouseReleaseEvent(self, event)

        self.endDrag()
        self.rubberBand().hide()

    def rubberBandStartEvent(self, event):
        """
        Triggered when the user presses an empty area.

        :type event: QtWidgets.QMouseEvent
        :rtype: None
        """
        self._rubberBandStartPos = event.pos()
        rect = QtCore.QRect(self._rubberBandStartPos, QtCore.QSize())

        rubberBand = self.rubberBand()
        rubberBand.setGeometry(rect)
        rubberBand.show()

    def rubberBandMoveEvent(self, event):
        """
        Triggered when the user moves the mouse over the current viewport.

        :type event: QtWidgets.QMouseEvent
        :rtype: None
        """
        if self.rubberBand() and self._rubberBandStartPos:
            rect = QtCore.QRect(self._rubberBandStartPos, event.pos())
            rect = rect.normalized()
            self.rubberBand().setGeometry(rect)

    # -----------------------------------------------------------------------
    # Support for drag and drop
    # -----------------------------------------------------------------------

    def rowAt(self, pos):
        """
        Return the row for the given pos.

        :type pos: QtCore.QPoint
        :rtype: int
        """
        return self.treeWidget().rowAt(pos)

    def itemsFromUrls(self, urls):
        """
        Return items from the given url objects.

        :type urls: list[QtCore.QUrl]
        :rtype: list[studioqt.Item]
        """
        items = []
        for url in urls:
            item = self.itemFromUrl(url)
            if item:
                items.append(item)
        return items

    def itemFromUrl(self, url):
        """
        Return the item from the given url object.

        :type url: QtCore.QUrl
        :rtype: studioqt.Item
        """
        return self.itemFromPath(url.path())

    def itemsFromPaths(self, paths):
        """
        Return the items from the given paths.

        :type paths: list[str]
        :rtype: list[studioqt.Item]
        """
        items = []
        for path in paths:
            item = self.itemFromPath(path)
            if item:
                items.append(item)
        return items

    def itemFromPath(self, path):
        """
        Return the item from the given path.

        :type path: str
        :rtype: studioqt.Item
        """
        for item in self.items():
            path_ = item.url().path()
            if path_ and path_ == path:
                return item

    def setDropEnabled(self, value):
        """
        :type value: bool
        :rtype: None
        """
        self._dropEnabled = value

    def dropEnabled(self):
        """
        :rtype: bool
        """
        return self._dropEnabled

    def dragThreshold(self):
        """
        :rtype: int
        """
        return self.DEFAULT_DRAG_THRESHOLD

    def mimeData(self, items):
        """
        :type items: list[studioqt.Item]
        :rtype: QtCore.QMimeData
        """
        mimeData = QtCore.QMimeData()

        urls = [item.url() for item in items]
        text = "\n".join([item.mimeText() for item in items])

        mimeData.setUrls(urls)
        mimeData.setText(text)

        return mimeData

    def dropEvent(self, event):
        """
        This event handler is called when the drag is dropped on this widget.

        :type event: QtWidgets.QDropEvent
        :rtype: None
        """
        item = self.itemAt(event.pos())
        selectedItems = self.selectedItems()

        if selectedItems and item:
            if self.treeWidget().isSortByCustomOrder():
                self.moveItems(selectedItems, item)
            else:
                msg = "You can only re-order items when sorting by custom order."
                logger.info(msg)

        if item:
            item.dropEvent(event)

        self.itemDropped.emit(event)

    def dragMoveEvent(self, event):
        """
        This event handler is called if a drag is in progress.

        :type event: QtGui.QDragMoveEvent
        :rtype: None
        """
        mimeData = event.mimeData()

        if (mimeData.hasText() or mimeData.hasUrls()) and self.dropEnabled():
            event.accept()
        else:
            event.ignore()

    def dragEnterEvent(self, event):
        """
        This event handler is called when the mouse enters this widget
        while a drag is in pregress.

        :type event: QtGui.QDragEnterEvent
        :rtype: None
        """
        mimeData = event.mimeData()

        if (mimeData.hasText() or mimeData.hasUrls()) and self.dropEnabled():
            event.accept()
        else:
            event.ignore()

    def isDraggingItems(self):
        """
        Return true if the user is currently dragging items.

        :rtype: bool
        """
        return bool(self._drag)

    def startDrag(self, event):
        """
        Starts a drag using the given event.

        :type event: QtCore.QEvent
        :rtype: None
        """
        if not self.dragEnabled():
            return

        if self._dragStartPos and hasattr(event, "pos"):

            item = self.itemAt(event.pos())

            if item and item.dragEnabled():

                self._dragStartIndex = self.indexAt(event.pos())

                point = self._dragStartPos - event.pos()
                dt = self.dragThreshold()

                if point.x() > dt or point.y() > dt or point.x(
                ) < -dt or point.y() < -dt:

                    items = self.selectedItems()
                    mimeData = self.mimeData(items)

                    pixmap = self.dragPixmap(item, items)
                    hotSpot = QtCore.QPoint(pixmap.width() / 2,
                                            pixmap.height() / 2)

                    self._drag = QtGui.QDrag(self)
                    self._drag.setPixmap(pixmap)
                    self._drag.setHotSpot(hotSpot)
                    self._drag.setMimeData(mimeData)
                    self._drag.start(QtCore.Qt.MoveAction)

    def endDrag(self):
        """
        Ends the current drag.

        :rtype: None
        """
        logger.debug("End Drag")
        self._dragStartPos = None
        self._dragStartIndex = None
        if self._drag:
            del self._drag
            self._drag = None

    def dragPixmap(self, item, items):
        """
        Show the drag pixmap for the given item.

        :type item: studioqt.Item
        :type items: list[studioqt.Item]
        
        :rtype: QtGui.QPixmap
        """
        rect = self.visualRect(self.indexFromItem(item))

        pixmap = QtGui.QPixmap()
        pixmap = pixmap.grabWidget(self, rect)

        if len(items) > 1:
            cWidth = 35
            cPadding = 5
            cText = str(len(items))
            cX = pixmap.rect().center().x() - float(cWidth / 2)
            cY = pixmap.rect().top() + cPadding
            cRect = QtCore.QRect(cX, cY, cWidth, cWidth)

            painter = QtGui.QPainter(pixmap)
            painter.setRenderHint(QtGui.QPainter.Antialiasing)

            painter.setPen(QtCore.Qt.NoPen)
            painter.setBrush(self.itemsWidget().backgroundSelectedColor())
            painter.drawEllipse(cRect.center(), float(cWidth / 2),
                                float(cWidth / 2))

            font = QtGui.QFont('Serif', 12, QtGui.QFont.Light)
            painter.setFont(font)
            painter.setPen(self.itemsWidget().textSelectedColor())
            painter.drawText(cRect, QtCore.Qt.AlignCenter, str(cText))

        return pixmap
Exemplo n.º 7
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, formWidget=None):
        super(FieldWidget, self).__init__(parent)

        self._data = data or {}
        self._widget = None
        self._default = None
        self._required = None
        self._collapsed = False
        self._errorLabel = None
        self._menuButton = None
        self._actionResult = None
        self._formWidget = None

        if formWidget:
            self.setFormWidget(formWidget)

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

        widget = self.createWidget()
        if widget:
            self.setWidget(widget)

    def name(self):
        """
        Get the name of field widget.

        :rtype: str
        """
        return self.data()["name"]

    def defaultData(self):
        """
        This is the default data used by the schema.

        :rtype: dict
        """
        return {}

    def setFormWidget(self, formWidget):
        """
        Set the form widget which contains the field widget.

        :type formWidget: studiolibrary.widgets.formwidget.FormWidget
        """
        self._formWidget = formWidget

    def formWidget(self):
        """
        Get the form widget the contains the field widget.

        :return: studiolibrary.widgets.formwidget.FormWidget
        """
        return self._formWidget

    def setCollapsed(self, collapsed):
        """
        Set the field widget collapsed used by the GroupFieldWidget.

        :return: studiolibrary.widgets.formwidget.FormWidget
        """
        self._collapsed = collapsed

    def isCollapsed(self):
        """
        Get the collapsed state of the field widget.

        :return: studiolibrary.widgets.formwidget.FormWidget
        """
        return self._collapsed

    def createWidget(self):
        """
        Create the widget to be used by the field.

        :rtype: QtWidgets.Widget or None
        """
        return None

    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")
        if title is None:
            title = data.get("name", "")
            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.blockSignals(True)

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

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

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

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

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

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

        visible = data.get('visible')
        if visible is not None and not self.isCollapsed():
            self.setVisible(visible)

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

        error = data.get('error')
        if error is not None:
            self.setError(error)

        value = data.get('errorVisible')
        if value is not None:
            self.setErrorVisible(value)

        toolTip = data.get('toolTip')
        if toolTip is not None:
            self.setToolTip(toolTip)
            self.setStatusTip(toolTip)

        placeholder = data.get("placeholder")
        if placeholder is not None:
            self.setPlaceholder(placeholder)

        style = data.get("style")
        if style is not None:
            self.setStyleSheet(style)

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

        label = data.get('label')
        if label is not None:

            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)

        # Menu Items
        actions = data.get('actions')
        if actions is not None:
            self._menuButton.setVisible(True)

        # Menu Button
        menu = data.get('menu')
        if menu is not None:
            text = menu.get("name")
            if text is not None:
                self._menuButton.setText(text)

            visible = menu.get("visible", True)
            self._menuButton.setVisible(visible)

        self._data.update(data)

        self.refresh()

        self.blockSignals(False)

    def setPlaceholder(self, placeholder):
        """
        Set the placeholder text to be displayed for the widget.

        :type placeholder: str
        :raises: NotImplementedError
        """
        NotImplementedError(
            'The method "setPlaceholder" needs to be implemented')

    def hasError(self):
        """
        Check if the field contains any errors.

        :rtype: bool
        """
        return bool(self.data().get("error"))

    def setErrorVisible(self, visible):
        """
        Set the error message visibility.

        :type visible: bool
        """
        self._data["errorVisible"] = visible
        self.refreshError()

    def setError(self, message):
        """
        Set the error message to be displayed for the field widget.
        
        :type message: str
        """
        self._data["error"] = message
        self.refreshError()

    def refreshError(self):
        """Refresh the error message with the current data."""
        error = self.data().get("error")

        if self.hasError() and self.data().get("errorVisible", False):
            self._errorLabel.setText(error)
            self._errorLabel.setHidden(False)
            self.setToolTip(error)
        else:
            self._errorLabel.setText("")
            self._errorLabel.setHidden(True)
            self.setToolTip(self.data().get('toolTip'))

        self.refresh()

    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
        """
        widgetLayout = QtWidgets.QHBoxLayout(self)
        widgetLayout.setContentsMargins(0, 0, 0, 0)
        widgetLayout.setSpacing(0)

        self._widget = widget
        self._widget.setParent(self)
        self._widget.setObjectName('widget')
        self._widget.setSizePolicy(
            QtWidgets.QSizePolicy.Expanding,
            QtWidgets.QSizePolicy.Preferred,
        )

        self._menuButton = QtWidgets.QPushButton("...")
        self._menuButton.setHidden(True)
        self._menuButton.setObjectName("menuButton")
        self._menuButton.clicked.connect(self._menuCallback)
        self._menuButton.setSizePolicy(
            QtWidgets.QSizePolicy.Preferred,
            QtWidgets.QSizePolicy.Expanding,
        )

        widgetLayout.addWidget(self._widget)
        widgetLayout.addWidget(self._menuButton)

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

        self._errorLabel = QtWidgets.QLabel(self)
        self._errorLabel.setHidden(True)
        self._errorLabel.setObjectName("errorLabel")
        self._errorLabel.setSizePolicy(
            QtWidgets.QSizePolicy.Expanding,
            QtWidgets.QSizePolicy.Preferred,
        )

        layout.addLayout(widgetLayout)
        layout.addWidget(self._errorLabel)

        self._layout2.addLayout(layout)

    def _menuCallback(self):
        callback = self.data().get("menu", {}).get("callback", self.showMenu)
        callback()

    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")
            enabled = action.get("enabled", True)
            callback = action.get("callback")

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

            action = menu.addAction(name)
            action.setEnabled(enabled)
            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())

        if self.data().get("errorVisible", False):
            self.setProperty('error', self.hasError())

        self.setStyleSheet(self.styleSheet())
Exemplo n.º 8
0
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
Exemplo n.º 9
0
class ImageSequence(QtCore.QObject):

    DEFAULT_FPS = 24

    frameChanged = QtCore.Signal(int)

    def __init__(self, path, *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

        if path:
            self.setPath(path)

    def firstFrame(self):
        """
        Get the path to the first frame.

        :rtype: str
        """
        if self._frames:
            return self._frames[0]
        return ""

    def setPath(self, path):
        """
        Set a single frame or a directory to an image sequence.
        
        :type path: str
        """
        if os.path.isfile(path):
            self._frame = 0
            self._frames = [path]
        elif os.path.isdir(path):
            self.setDirname(path)

    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._timer.timeout.connect(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._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.jumpToFrame(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 frameCount(self):
        """
        Return the number of frames.

        :rtype: int
        """
        return len(self._frames)

    def currentIcon(self):
        """
        Returns the current frame as a QIcon.

        :rtype: QtGui.QIcon
        """
        return QtGui.QIcon(self.currentFilename())

    def currentPixmap(self):
        """
        Return the current frame as a QPixmap.

        :rtype: QtGui.QPixmap
        """
        return QtGui.QPixmap(self.currentFilename())

    def currentFilename(self):
        """
        Return the current file name.

        :rtype: str or None
        """
        try:
            return self._frames[self.currentFrameNumber()]
        except IndexError:
            pass

    def currentFrameNumber(self):
        """
        Return the current frame.

        :rtype: int or None
        """
        return self._frame

    def jumpToFrame(self, frame):
        """
        Set the current frame.

        :rtype: int or None
        """
        if frame >= self.frameCount():
            frame = 0
        self._frame = frame
        self.frameChanged.emit(frame)
Exemplo n.º 10
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")

        new = {}
        old = self.read()
        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 = old.get(path, {})
            itemData.update(item.createItemData())

            new[path] = itemData

        if progressCallback:
            progressCallback("Post Callbacks")

        self.postSync(new)

        if progressCallback:
            progressCallback("Saving Cache")

        self.save(new)

        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
Exemplo n.º 11
0
class GroupBoxWidget(QtWidgets.QFrame):

    toggled = QtCore.Signal(bool)

    def __init__(self, title, widget, persistent=False, *args, **kwargs):
        super(GroupBoxWidget, self).__init__(*args, **kwargs)

        self._widget = None
        self._persistent = None

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

        self.setLayout(layout)

        self._titleWidget = QtWidgets.QPushButton(self)
        self._titleWidget.setCheckable(True)
        self._titleWidget.setText(title)
        self._titleWidget.setObjectName("title")
        self._titleWidget.toggled.connect(self._toggled)

        on_path = studiolibrary.resource.get("icons", "caret-down.svg")
        off_path = studiolibrary.resource.get("icons", "caret-right.svg")
        icon = studioqt.Icon.fa(on_path, color="rgb(255,255,255,200)", off=off_path)
        self._titleWidget.setIcon(icon)

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

        self._widgetFrame = QtWidgets.QFrame(self)
        self._widgetFrame.setObjectName("frame")

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

        self._widgetFrame.setLayout(layout)

        self.layout().addWidget(self._widgetFrame)

        if widget:
            self.setWidget(widget)

        self.setPersistent(persistent)

    def setPersistent(self, persistent):
        """
        Save and load the state of the widget to disk.

        :type persistent: bool
        """
        self._persistent = persistent
        self.loadSettings()

    def title(self):
        """
        Get the title for the group box.

        :rtype: str
        """
        return self._titleWidget.text()

    def setWidget(self, widget):
        """
        Set the widget to hide when the user clicks the title.

        :type widget: QWidgets.QWidget
        """
        self._widget = widget
        # self._widget.setParent(self._widgetFrame)
        # self._widgetFrame.layout().addWidget(self._widget)

    def _toggled(self, visible):
        """
        Triggered when the user clicks the title.

        :type visible: bool
        """
        self.saveSettings()
        self.setChecked(visible)
        self.toggled.emit(visible)

    def isChecked(self):
        """
        Check the checked state for the group box.

        :rtype: bool
        """
        return self._titleWidget.isChecked()

    def setChecked(self, checked):
        """
        Overriding this method to hide the widget when the state changes.

        :type checked: bool
        """
        self._titleWidget.setChecked(checked)
        if self._widget:
            self._widget.setVisible(checked)

    def saveSettings(self):
        """Save the state to disc."""
        if self._persistent:

            if not self.objectName():
                raise NameError("No object name set for persistent widget.")

            data = {self.objectName(): {"checked": self.isChecked()}}
            settings.save(data)

    def loadSettings(self):
        """Load the state to disc."""
        if self._persistent:

            if not self.objectName():
                raise NameError("No object name set for persistent widget.")

            data = settings.read()
            data = data.get(self.objectName(), {})

            if isinstance(data, dict):
                checked = data.get("checked", True)
                self.setChecked(checked)
Exemplo n.º 12
0
class Library(QtCore.QObject):

    Fields = [
        {
            "name": "icon",
            "sortable": False,
            "groupable": False,
        },
        {
            "name": "name",
            "sortable": True,
            "groupable": False,
        },
        {
            "name": "path",
            "sortable": True,
            "groupable": False,
        },
        {
            "name": "type",
            "sortable": True,
            "groupable": True,
        },
        {
            "name": "folder",
            "sortable": True,
            "groupable": False,
        },
        {
            "name": "category",
            "sortable": True,
            "groupable": True,
        },
        {
            "name": "modified",
            "sortable": True,
            "groupable": False,
        },
        {
            "name": "Custom Order",
            "sortable": True,
            "groupable": False,
        },
    ]

    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._registeredItems = None
        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):
        """
        Get all the fields for the library.

        :rtype: list[dict]
        """
        return self.Fields

    def fieldNames(self):
        """
        Get all the field names for the library.

        :rtype: list[str]
        """
        return [field["name"] for field in 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._registeredItems = None
        self.dataChanged.emit()

    def registeredItems(self):
        """
        Get registered items for the library.

        :rtype: list[LibraryItem.__class__]
        """
        return studiolibrary.registeredItems()

    def isValidPath(self, path):
        """
        Check if the given item path should be ignored.

        :type path: str
        :rtype: bool
        """
        for ignore in studiolibrary.config.get('ignorePaths', []):
            if ignore in path:
                return False
        return True

    def walker(self, path):
        """
        Walk the given root path for valid items and return the item data.

        :type path: str

        :rtype: collections.Iterable[dict]
        """
        path = studiolibrary.normPath(path)
        maxDepth = self.recursiveDepth()
        startDepth = path.count(os.path.sep)

        for root, dirs, files in os.walk(path, followlinks=True):

            files.extend(dirs)

            for filename in files:

                # Normalise the path for consistent matching
                path = studiolibrary.normPath(os.path.join(root, filename))

                # Ignore any paths that have been specified in the config
                if not self.isValidPath(path):
                    continue

                # Match the path with a registered item
                item = self.itemFromPath(path)

                remove = False
                if item:

                    # Yield the item data that matches the current path
                    yield item.createItemData()

                    # Stop walking if the item doesn't support nested items
                    if not item.ENABLE_NESTED_ITEMS:
                        remove = True

                if remove and filename in dirs:
                    dirs.remove(filename)

            if maxDepth == 1:
                break

            # Stop walking the directory if the maximum depth has been reached
            currentDepth = root.count(os.path.sep)
            if (currentDepth - startDepth) >= maxDepth:
                del dirs[:]

    def sync(self, progressCallback=None):
        """
        Sync the file system with the database.

        :type progressCallback: None or func
        """
        if not self.path():
            logger.info('No path set for syncing data')
            return

        if progressCallback:
            progressCallback("Syncing")

        new = {}
        old = self.read()
        items = list(self.walker(self.path()))
        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.get("path")
            new[path] = old.get(path, {})
            new[path].update(item)

        if progressCallback:
            progressCallback("Post Callbacks")

        self.postSync(new)

        if progressCallback:
            progressCallback("Saving Cache")

        self.save(new)

        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 cache has changed since the last read call
        if self.isDirty():

            logger.debug("Creating items")

            self._items = []

            data = self.read()

            modules = []
            for itemData in data.values():
                if '__class__' in itemData:
                    modules.append(itemData.get("__class__"))
            modules = set(modules)

            classes = {}
            for module in modules:
                classes[module] = studiolibrary.resolveModule(module)

            for path in data.keys():
                module = data[path].get("__class__")
                cls = classes.get(module)
                if cls:
                    item = cls(path, library=self, libraryWindow=self._libraryWindow)
                    item.setItemData(data[path])
                    self._items.append(item)
                else:
                    # This is to support the older database data before v2.6.
                    # Will remove in a later version.
                    item = self.itemFromPath(path, library=self, libraryWindow=self._libraryWindow)
                    if item:
                        item.setItemData(data[path])
                        self._items.append(item)

        return self._items

    def itemFromPath(self, path, **kwargs):
        """
        Return a new item instance for the given path.

        :type path: str
        :rtype: studiolibrary.LibraryItem or None
        """
        path = studiolibrary.normPath(path)

        for cls in self.registeredItems():
            if cls.match(path):
                return cls(path, **kwargs)

    def itemsFromPaths(self, paths, **kwargs):
        """
        Return new item instances for the given paths.

        :type paths: list[str]:
        :rtype: collections.Iterable[studiolibrary.LibraryItem]
        """
        for path in paths:
            item = self.itemFromPath(path, **kwargs)
            if item:
                yield item

    def itemsFromUrls(self, urls, **kwargs):
        """
        Return new item instances for the given QUrl objects.

        :type urls: list[QtGui.QUrl]
        :rtype: list[studiolibrary.LibraryItem]
        """
        items = []
        for path in studiolibrary.pathsFromUrls(urls):

            item = self.itemFromPath(path, **kwargs)

            if item:
                data = item.createItemData()
                item.setItemData(data)

                items.append(item)
            else:
                msg = 'Cannot find the item for path "{0}"'
                msg = msg.format(path)
                logger.warning(msg)

        return 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 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 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 = six.text_type(data)
                else:
                    itemValue = data.get(key)

                if isinstance(value, six.string_types):
                    value = value.lower()

                if isinstance(itemValue, six.string_types):
                    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
Exemplo n.º 13
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 = 25
    DEFAULT_MIN_ICON_SIZE = 50

    LABEL_DISPLAY_OPTION = LabelDisplayOption.Under

    keyPressed = QtCore.Signal(object)

    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._itemSizeHint = QtCore.QSize(w, h)

        self._zoomAmount = self.DEFAULT_ZOOM_AMOUNT
        self._labelDisplayOption = self.LABEL_DISPLAY_OPTION

        self._dataset = None
        self._treeWidget = TreeWidget(self)

        self._listView = ListView(self)
        self._listView.setTreeWidget(self._treeWidget)
        self._listView.installEventFilter(self)

        self._delegate = ItemDelegate()
        self._delegate.setItemsWidget(self)

        self._listView.setItemDelegate(self._delegate)
        self._treeWidget.setItemDelegate(self._delegate)
        self._treeWidget.installEventFilter(self)

        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._itemBackgroundColor = 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 eventFilter(self, obj, event):
        if event.type() == QtCore.QEvent.KeyPress:
            self.keyPressed.emit(event)

        return super(ItemsWidget, self).eventFilter(obj, event)

    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.fieldNames())
        dataset.searchFinished.connect(self.updateItems)

    def dataset(self):
        return self._dataset

    def updateItems(self):
        """Sets the items to the widget."""
        selectedItems = self.selectedItems()

        self.treeWidget().blockSignals(True)

        try:
            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()

        finally:
            self.treeWidget().blockSignals(False)
            self.itemSelectionChanged.emit()

    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 setLabelDisplayOption(self, value):
        """
        Set the label display option.

        :type value: LabelDisplayOption
        """
        self._labelDisplayOption = value
        self.refreshSize()

    def labelDisplayOption(self):
        """
        Return the visibility of the item text.

        :rtype: LabelDisplayOption
        """
        if self.isIconView():
            return self._labelDisplayOption

    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["labelDisplayOption"] = self.labelDisplayOption()
        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)

        value = settings.get("labelDisplayOption", self.LABEL_DISPLAY_OPTION)
        self.setLabelDisplayOption(value)

        self.treeWidget().setSettings(settings)

        self.setToastEnabled(True)

        return settings

    def createCopyTextMenu(self):
        return self.treeWidget().createCopyTextMenu()

    def createSettingsMenu(self):

        menu = QtWidgets.QMenu("Item View", self)

        action = SeparatorAction("View Settings", menu)
        menu.addAction(action)

        action = 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 = 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 = 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 = SeparatorAction("Label Options", menu)
        menu.addAction(action)

        for option in LabelDisplayOption.values():
            action = QtWidgets.QAction(option.title(), menu)
            action.setCheckable(True)
            action.setChecked(option == self.labelDisplayOption())
            callback = functools.partial(self.setLabelDisplayOption, option)
            action.triggered.connect(callback)
            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 itemSizeHint(self, index=None):
        """
        Get the item size hint.
        
        :type index: QtWidgets.QModelIndex
        :rtype: QtCore.QSize
        """
        return self._itemSizeHint

    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

        if self.labelDisplayOption() == LabelDisplayOption.Under:
            w = size.width()
            h = size.width() + self.itemTextHeight()
            self._itemSizeHint = QtCore.QSize(w, h)
        elif size.height() < self.itemTextHeight():
            self._itemSizeHint = QtCore.QSize(size.width(), self.itemTextHeight())
        else:
            self._itemSizeHint = 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 setItemBackgroundColor(self, color):
        """
        Set the item background color.

        :type color: QtWidgets.QtColor
        """
        self._itemBackgroundColor = 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 itemBackgroundColor(self):
        """
        Return the item background color.

        :rtype: QtWidgets.QtColor
        """
        return self._itemBackgroundColor

    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
Exemplo n.º 14
0
class ThumbnailCaptureMenu(QtWidgets.QMenu):

    captured = QtCore.Signal(str)

    def __init__(self, path, force=False, parent=None):
        """
        Thumbnail capture menu.

        :type path: str
        :type force: bool
        :type parent: None or QtWidgets.QWidget
        """
        QtWidgets.QMenu.__init__(self, parent)

        self._path = path
        self._force = force

        changeImageAction = QtWidgets.QAction('Capture new image', self)
        changeImageAction.triggered.connect(self.capture)
        self.addAction(changeImageAction)

        changeImageAction = QtWidgets.QAction('Show Capture window', self)
        changeImageAction.triggered.connect(self.showCaptureWindow)
        self.addAction(changeImageAction)

        loadImageAction = QtWidgets.QAction('Load image from disk', self)
        loadImageAction.triggered.connect(self.showLoadImageDialog)
        self.addAction(loadImageAction)

    def path(self):
        """
        Return the thumbnail path on disc.
        
        :rtype: str
        """
        return self._path

    def showWarningDialog(self):
        """Show a warning dialog for overriding the previous thumbnail."""
        title = "Override Thumbnail"

        text = u"This action will delete the previous thumbnail. The " \
               u"previous image cannot be backed up. Do you want to " \
               u"confirm the action to take a new image and delete " \
               u"the previous one?"

        clickedButton = studiolibrary.widgets.MessageBox.warning(
            self.parent(),
            title=title,
            text=text,
            enableDontShowCheckBox=True,
        )

        if clickedButton != QtWidgets.QDialogButtonBox.StandardButton.Yes:
            raise Exception("Dialog was canceled!")

    def showCaptureWindow(self):
        """Show the capture window for framing."""
        self.capture(show=True)

    def capture(self, show=False):
        """
        Capture an image from the Maya viewport.
        
        :type show: bool
        """
        if not self._force and os.path.exists(self.path()):
            self.showWarningDialog()

        mutils.gui.thumbnailCapture(show=show,
                                    path=self.path(),
                                    captured=self.captured.emit)

    def showLoadImageDialog(self):
        """Show a file dialog for choosing an image from disc."""
        if not self._force and os.path.exists(self.path()):
            self.showWarningDialog()

        fileDialog = QtWidgets.QFileDialog(
            self,
            caption="Open Image",
            filter="Image Files (*.png *.jpg *.bmp)")

        fileDialog.fileSelected.connect(self._fileSelected)
        fileDialog.exec_()

    def _fileSelected(self, path):
        """
        Triggered when the file dialog is accepted.
        
        :type path: str
        """
        shutil.copy(path, self.path())
        self.captured.emit(self.path())
Exemplo n.º 15
0
class SearchWidget(QtWidgets.QLineEdit):

    SPACE_OPERATOR = "and"
    PLACEHOLDER_TEXT = "Search"

    searchChanged = QtCore.Signal()

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

        self._dataset = None
        self._spaceOperator = "and"
        self._iconButton = QtWidgets.QPushButton(self)
        self._iconButton.clicked.connect(self._iconClicked)

        icon = studiolibrary.resource.icon("search")
        self.setIcon(icon)

        self._clearButton = QtWidgets.QPushButton(self)
        self._clearButton.setCursor(QtCore.Qt.ArrowCursor)
        icon = studiolibrary.resource.icon("cross")
        self._clearButton.setIcon(icon)
        self._clearButton.setToolTip("Clear all search text")
        self._clearButton.clicked.connect(self._clearClicked)
        self._clearButton.setStyleSheet("background-color: transparent;")

        self.setPlaceholderText(self.PLACEHOLDER_TEXT)
        self.textChanged.connect(self._textChanged)

        self.update()

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

    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 _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.search()

    def search(self):
        """Run the search query on the data set."""
        if self.dataset():
            self.dataset().addQuery(self.query())
            self.dataset().search()
        else:
            logger.info("No dataset found the the search widget.")

        self.updateClearButton()
        self.searchChanged.emit()

    def query(self):
        """
        Get the query used for the data set.
        
        :rtype: dict 
        """
        text = str(self.text())

        filters = []
        for filter_ in text.split(' '):
            if filter_.split():
                filters.append(('*', 'contains', filter_))

        uniqueName = 'searchwidget' + str(id(self))

        return {
            'name': uniqueName,
            'operator': self.spaceOperator(),
            'filters': filters
        }

    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 spaceOperator(self):
        """
        Get the space operator for the search widget.

        :rtype: str
        """
        return self._spaceOperator

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

        :type operator: str
        """
        self._spaceOperator = operator
        self.search()

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

        :type parent: QGui.QMenu
        :rtype: QGui.QMenu
        """
        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, "or")
        action.triggered.connect(callback)

        if self.spaceOperator() == "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, "and")
        action.triggered.connect(callback)

        if self.spaceOperator() == "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 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 updateIconColor(self):
        """
        Update the icon colors to the current foregroundRole.

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

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

        :rtype: dict
        """
        settings = {
            "text": self.text(),
            "spaceOperator": self.spaceOperator(),
        }
        return settings

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

        :type settings: dict
        :rtype: None
        """
        text = settings.get("text", "")
        self.setText(text)

        spaceOperator = settings.get("spaceOperator")
        if spaceOperator:
            self.setSpaceOperator(spaceOperator)

    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())
Exemplo n.º 16
0
class IconPickerWidget(QtWidgets.QFrame):

    BUTTON_CLASS = IconButton

    iconChanged = QtCore.Signal(object)

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

        self._buttons = []
        self._currentIcon = None
        self._menuButton = None

        layout = QtWidgets.QGridLayout()
        layout.setSpacing(0)
        layout.setContentsMargins(0, 0, 0, 0)
        self.setLayout(layout)

    def enterEvent(self, event):
        """
        Overriding this method to fix a bug with custom actions.

        :type event: QtCore.QEvent
        """
        if self.parent():
            menu = self.parent().parent()
            if isinstance(menu, QtWidgets.QMenu):
                menu.setActiveAction(None)

    def _iconChanged(self, iconPath):
        """
        Triggered when the user clicks or browses for a color.

        :type iconPath: str
        :rtype: None
        """
        self.setCurrentIcon(iconPath)
        self.iconChanged.emit(iconPath)

    def menuButton(self):
        """
        Get the menu button used for browsing for custom colors.

        :rtype: QtGui.QWidget
        """
        return self._menuButton

    def deleteButtons(self):
        """
        Delete all the color buttons.

        :rtype: None
        """
        layout = self.layout()
        while layout.count():
            item = layout.takeAt(0)
            item.widget().deleteLater()

    def currentIcon(self):
        """
        Return the current color.

        :rtype: studioqt.Color
        """
        return self._currentIcon

    def setCurrentIcon(self, color):
        """
        Set the current color.

        :type color: studioqt.Color
        """
        self._currentIcon = color
        self.refresh()

    def refresh(self):
        """Update the current state of the selected color."""
        for button in self._buttons:
            button.setChecked(button.iconPath() == self.currentIcon())

    def setIcons(self, icons):
        """
        Set the colors for the color bar.

        :type icons: list[str] or list[studioqt.Icon]
        """
        self.deleteButtons()

        i = 0
        first = True
        last = False

        positions = [(i, j) for i in range(5) for j in range(5)]
        for position, iconPath in zip(positions, icons):
            i += 1

            if i == len(icons) - 1:
                last = True

            callback = partial(self._iconChanged, iconPath)

            button = self.BUTTON_CLASS(self)
            button.setIconPath(iconPath)
            button.setIconSize(QtCore.QSize(16, 16))

            button.setSizePolicy(QtWidgets.QSizePolicy.Expanding,
                                 QtWidgets.QSizePolicy.Preferred)

            button.setProperty("first", first)
            button.setProperty("last", last)

            button.clicked.connect(callback)

            self.layout().addWidget(button, *position)

            self._buttons.append(button)

            first = False

        self._menuButton = QtWidgets.QPushButton("...", self)
        self._menuButton.setObjectName('menuButton')
        self._menuButton.setSizePolicy(QtWidgets.QSizePolicy.Expanding,
                                       QtWidgets.QSizePolicy.Preferred)

        self._menuButton.clicked.connect(self.browseColor)
        self.layout().addWidget(self._menuButton)
        self.refresh()

    @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
        """
        pass
Exemplo n.º 17
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
        self._menuButton = None

        layout = QtWidgets.QHBoxLayout()
        layout.setSpacing(0)
        layout.setContentsMargins(0, 0, 0, 0)
        self.setLayout(layout)

    def enterEvent(self, event):
        """
        Overriding this method to fix a bug with custom actions.

        :type event: QtCore.QEvent
        """
        if self.parent():
            menu = self.parent().parent()
            if isinstance(menu, QtWidgets.QMenu):
                menu.setActiveAction(None)

    def _colorChanged(self, color):
        """
        Triggered when the user clicks or browses for a color.

        :type color: studioqt.Color
        :rtype: None
        """
        self.setCurrentColor(color)
        self.colorChanged.emit(color)

    def menuButton(self):
        """
        Get the menu button used for browsing for custom colors.

        :rtype: QtGui.QWidget
        """
        return self._menuButton

    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
        self.refresh()

    def refresh(self):
        """Update the current state of the selected color."""
        for button in self._buttons:
            button.setChecked(button.color() == self.currentColor())

    def setColors(self, colors):
        """
        Set the colors for the color bar.

        :type colors: list[str] or list[studioqt.Color]
        """
        self.deleteButtons()

        first = True
        last = False

        for i, color in enumerate(colors):

            if i == len(colors)-1:
                last = True

            if not isinstance(color, str):
                color = studioqt.Color(color)
                color = color.toString()

            callback = partial(self._colorChanged, color)

            button = self.COLOR_BUTTON_CLASS(self)

            button.setObjectName('colorButton')
            button.setColor(color)

            button.setSizePolicy(
                QtWidgets.QSizePolicy.Expanding,
                QtWidgets.QSizePolicy.Preferred
            )

            button.setProperty("first", first)
            button.setProperty("last", last)

            button.clicked.connect(callback)

            self.layout().addWidget(button)

            self._buttons.append(button)

            first = False

        self._menuButton = QtWidgets.QPushButton("...", self)
        self._menuButton.setObjectName('menuButton')
        self._menuButton.setSizePolicy(
            QtWidgets.QSizePolicy.Expanding,
            QtWidgets.QSizePolicy.Preferred
        )

        self._menuButton.clicked.connect(self.browseColor)
        self.layout().addWidget(self._menuButton)
        self.refresh()

    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()
        if color:
            color = studioqt.Color.fromString(color)

        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)

        if d.exec_():
            self._colorChanged(d.selectedColor())
        else:
            self._colorChanged(color)
Exemplo n.º 18
0
class SidebarWidget(QtWidgets.QTreeWidget):

    itemDropped = QtCore.Signal(object)
    itemRenamed = QtCore.Signal(str, str)
    itemSelectionChanged = QtCore.Signal()

    def __init__(self, *args):
        super(SidebarWidget, self).__init__(*args)

        self._dpi = 1
        self._items = []
        self._index = {}
        self._locked = False
        self._dataset = None
        self._recursive = True
        self._options = {
            'field': 'path',
            'separator': '/',
            'recursive': True,
            'autoRootPath': True,
            'rootText': 'FOLDERS',
            'sortBy': None,
            'queries': [{
                'filters': [('type', 'is', 'Folder')]
            }]
        }

        self.itemExpanded.connect(self.update)
        self.itemCollapsed.connect(self.update)

        self.setDpi(1)

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

    def clear(self):
        """Clear all the items from the tree widget."""
        self._items = []
        self._index = {}
        super(SidebarWidget, self).clear()

    def selectionChanged(self, *args):
        """Triggered the current selection has changed."""
        self.search()

    def setRecursive(self, enable):
        """
        Set the search query on the dataset to be recursive.
        
        :type enable: bool
        """
        self._recursive = enable
        self.search()

    def isRecursive(self):
        """
        Get the recursive query enable state.
        
        :rtype: bool 
        """
        return self._recursive

    def sortBy(self):
        """
        Get the sortby field.
        
        :rtype: str 
        """
        return self._options.get('sortBy', [self.field()])

    def field(self):
        """
        Get the field.
        
        :rtype: str 
        """
        return self._options.get('field', '')

    def rootText(self):
        """
        Get the root text.
        
        :rtype: str 
        """
        return self._options.get('rootText')

    def separator(self):
        """
        Get the separator used in the fields to separate level values.

        :rtype: str 
        """
        return self._options.get('separator', DEFAULT_SEPARATOR)

    def _dataChanged(self):
        """Triggered when the data set has changed."""
        pass
        # data = collections.OrderedDict()
        # queries = self._options.get("queries")
        #
        # items = self.dataset().findItems(queries)
        #
        # for item in items:
        #     itemData = item.itemData()
        #     value = itemData.get(self.field())
        #     data[value] = {'iconPath': itemData.get('iconPath')}
        #
        # if data:
        #     root = findRoot(data.keys(), separator=self.separator())
        #     self.setPaths(data, root=root)

    def setDataset(self, dataset):
        """
        Set the dataset for the search widget:
        
        :type dataset: studioqt.Dataset
        """
        self._dataset = dataset
        self._dataset.dataChanged.connect(self._dataChanged)
        self._dataChanged()

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

    def search(self):
        """Run the dataset search."""
        if self.dataset():
            self.dataset().addQuery(self.query())
            self.dataset().search()
        else:
            logger.info('No dataset found for the sidebar widget.')

    def query(self):
        """
        Get the query for the sidebar widget.
        
        :rtype: dict
        """
        filters = []

        for path in self.selectedPaths():
            if self.isRecursive():

                suffix = "" if path.endswith("/") else "/"

                filter_ = ('folder', 'startswith', path + suffix)
                filters.append(filter_)

            filter_ = ('folder', 'is', path)
            filters.append(filter_)

        uniqueName = 'sidebar_widget_' + str(id(self))
        return {'name': uniqueName, 'operator': 'or', 'filters': filters}

    def setLocked(self, locked):
        """
        Set the widget items to read only mode.
        
        :type locked: bool
        :rtype: None 
        """
        self._locked = locked

    def isLocked(self):
        """
        Return True if the items are in read only mode
        
        :rtype: bool 
        """
        return self._locked

    def itemAt(self, pos):
        """
        :type pos: QtGui.QPoint
        :rtype: None or Folder
        """
        index = self.indexAt(pos)
        if not index.isValid():
            return

        item = self.itemFromIndex(index)
        return item

    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 dragMoveEvent(self, event):
        """
        :type event: QtCore.QEvent
        :rtype: None
        """
        mimeData = event.mimeData()

        if mimeData.hasUrls():
            event.accept()
        else:
            event.ignore()

        item = self.itemAt(event.pos())
        if item:
            self.selectPaths([item.path()])

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

    def selectItem(self, item):
        """
        :type item: NavigationWidgetItem
        :rtype: None
        """
        self.selectPaths([item.path()])

    def dpi(self):
        """
        Return the dots per inch multiplier.

        :rtype: float
        """
        return self._dpi

    def setDpi(self, dpi):
        """
        Set the dots per inch multiplier.

        :type dpi: float
        :rtype: None
        """
        size = 24 * dpi
        self.setIndentation(14 * dpi)
        self.setMinimumWidth(22 * dpi)
        self.setIconSize(QtCore.QSize(size, size))
        self.setStyleSheet("height: {size}".format(size=size))

    def update(self, *args):
        """
        :rtype: None
        """
        for item in self.items():
            item.update()

    def items(self):
        """
        Return a list of all the items in the tree widget.

        :rtype: list[NavigationWidgetItem]
        """
        items = self.findItems(
            "*", QtCore.Qt.MatchWildcard | QtCore.Qt.MatchRecursive)

        return items

    def itemFromUrl(self, url):
        """
        Return the item for the given url.

        :type url: QtCore.QUrl
        :rtype: NavigationWidgetItem
        """
        for item in self.items():
            if url == item.url():
                return item

    def itemFromPath(self, path):
        """
        Return the item for the given path.

        :type path: str
        :rtype: NavigationWidgetItem
        """
        return self._index.get(path)

    def settings(self):
        """
        Return a dictionary of the settings for this widget.

        :rtype: dict
        """
        settings = {}

        scrollBar = self.verticalScrollBar()
        settings["verticalScrollBar"] = {"value": scrollBar.value()}

        scrollBar = self.horizontalScrollBar()
        settings["horizontalScrollBar"] = {"value": scrollBar.value()}

        for item in self.items():
            itemSettings = item.settings()
            if itemSettings:
                settings[item.path()] = item.settings()

        return settings

    def setSettings(self, settings):
        """
        Set the settings for this widget

        :type settings: dict
        """
        for path in sorted(settings.keys()):
            s = settings.get(path, None)
            self.setPathSettings(path, s)

        scrollBarSettings = settings.get("verticalScrollBar", {})
        value = scrollBarSettings.get("value", None)
        if value:
            self.verticalScrollBar().setValue(value)

        scrollBarSettings = settings.get("horizontalScrollBar", {})
        value = scrollBarSettings.get("value", None)
        if value:
            self.horizontalScrollBar().setValue(value)

        self.setDpi(self.dpi())

    def setPathSettings(self, path, settings):
        """
        Show the context menu at the given position.

        :type path: str
        :type settings: dict
        :rtype: None
        """
        item = self.itemFromPath(path)
        if item and settings:
            item.setSettings(settings)

    def showContextMenu(self, position):
        """
        Show the context menu at the given position.
        
        :type position: QtCore.QPoint
        :rtype: None
        """
        menu = self.createContextMenu()
        menu.exec_(self.viewport().mapToGlobal(position))

    def expandedItems(self):
        """
        Return all the expanded items.

        :rtype:  list[NavigationWidgetItem]
        """
        for item in self.items():
            if self.isItemExpanded(item):
                yield item

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

        :rtype:  list[NavigationWidgetItem]
        """
        for item in self.expandedItems():
            yield item.url()

    def setExpandedPaths(self, paths):
        """
        Set the given paths to expanded.

        :type paths: list[str]
        """
        for item in self.items():
            if item.url() in paths:
                item.setExpanded(True)

    def selectedItem(self):
        """
        Return the last selected item
        
        :rtype: SidebarWidgetItem 
        """
        path = self.selectedPath()
        return self.itemFromPath(path)

    def selectedPath(self):
        """
        Return the last selected path

        :rtype: str or None
        """
        paths = self.selectedPaths()
        if paths:
            return paths[-1]

    def selectedPaths(self):
        """
        Return the paths that are selected.

        :rtype: list[str]
        """
        paths = []
        items = self.selectedItems()
        for item in items:
            path = item.path()
            paths.append(path)
        return studiolibrary.normPaths(paths)

    def selectPath(self, path):
        """
        Select the given path

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

    def selectPaths(self, paths):
        """
        Select the items with the given paths.

        :type paths: list[str]
        :rtype: None
        """
        paths = studiolibrary.normPaths(paths)
        items = self.items()
        for item in items:
            if studiolibrary.normPath(item.path()) in paths:
                item.setSelected(True)
            else:
                item.setSelected(False)

    def selectUrl(self, url):
        """
        Select the item with the given url.

        :type url: str
        :rtype: None
        """
        items = self.items()

        for item in items:
            if item.url() == url:
                item.setSelected(True)
            else:
                item.setSelected(False)

    def selectedUrls(self):
        """
        Return the urls for the selected items.

        :rtype: list[str]
        """
        urls = []
        items = self.selectedItems()
        for item in items:
            urls.append(item.url())
        return urls

    def setPaths(self, *args, **kwargs):
        """
        This method has been deprecated.
        """
        logger.warning("This method has been deprecated!")
        self.setData(*args, **kwargs)

    def setData(self, data, root="", split=None):
        """
        Set the items to the given items.

        :type data: list[str]
        :type root: str
        :type split: str
        :rtype: None
        """
        settings = self.settings()

        self.blockSignals(True)

        self.clear()

        if not root:
            root = findRoot(data.keys(), self.separator())

        self.addPaths(data, root=root, split=split)

        self.setSettings(settings)

        self.blockSignals(False)

        self.search()

    def addPaths(self, paths, root="", split=None):
        """
        Set the given items as a flat list.

        :type paths: list[str]
        :type root: str or None
        :type split: str or None
        """
        data = pathsToDict(paths, root=root, separator=split)
        self.createItems(data, split=split)

        if isinstance(paths, dict):
            self.setSettings(paths)

    def createItems(self, data, split=None):
        """ 
        Create the items from the given data dict

        :type data: dict
        :type split: str or None

        :rtype: None
        """
        split = split or DEFAULT_SEPARATOR

        self._index = {}

        for key in data:

            path = split.join([key])

            item = SidebarWidgetItem(self)
            item.setText(0, unicode(key))
            item.setPath(path)
            self._index[path] = item

            if self.rootText():
                item.setText(0, self.rootText())
                item.setBold(True)
                item.setIconPath('none')
                item.setExpanded(True)

            def _recursive(parent, children, split=None):
                for text, val in sorted(children.iteritems()):

                    path = parent.path()
                    path = split.join([path, text])

                    child = SidebarWidgetItem()
                    child.setText(0, unicode(text))
                    child.setPath(path)

                    parent.addChild(child)
                    self._index[path] = child

                    _recursive(child, val, split=split)

            _recursive(item, data[key], split=split)

        self.update()
Exemplo n.º 19
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()
Exemplo n.º 20
0
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.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()
Exemplo 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._fieldsFrame = QtWidgets.QFrame(self)
        self._fieldsFrame.setObjectName("optionsFrame")

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

        self._fieldsFrame.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._fieldsFrame)

    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._fieldsFrame.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 savePersistentValues(self):
        """
        Triggered when the user changes the options.
        """
        data = {}

        for widget in self._widgets:
            name = widget.data().get("name")
            if name and widget.data().get("persistent"):

                key = self.objectName() or "FormWidget"
                key = widget.data().get("persistentKey", key)

                data.setdefault(key, {})
                data[key][name] = widget.value()

        for key in data:
            settings.set(key, data[key])

    def loadPersistentValues(self):
        """
        Get the options from the user settings.

        :rtype: dict
        """
        values = {}
        defaultValues = self.defaultValues()

        for field in self.schema():
            name = field.get("name")
            persistent = field.get("persistent")

            if persistent:
                key = self.objectName() or "FormWidget"
                key = field.get("persistentKey", key)
                value = settings.get(key, {}).get(name)
            else:
                value = defaultValues.get(name)

            if value:
                values[name] = value

        self.setValues(values)

    def schema(self):
        """
        Get the schema for the form.

        :rtype: dict
        """
        return self._schema

    def _sortSchema(self, schema):
        """
        Sort the schema depending on the group order.

        :type schema: list[dict]
        :rtype: list[dict]
        """
        order = 0

        for i, field in enumerate(schema):
            if field.get("type") == "group":
                order = field.get("order", order)
            field["order"] = order

        def _key(field):
            return field["order"]

        return sorted(schema, key=_key)

    def setSchema(self, schema, layout=None, errorsVisible=False):
        """
        Set the schema for the widget.
        
        :type schema: list[dict]
        :type layout: None or str
        :type errorsVisible: bool
        """
        self._schema = self._sortSchema(schema)

        for field in self._schema:

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

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

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

            errorVisible = field.get("errorVisible")
            if errorVisible is not None:
                field["errorVisible"] = errorVisible
            else:
                field["errorVisible"] = errorsVisible

            widget = cls(data=field, parent=self._fieldsFrame, formWidget=self)

            data_ = widget.defaultData()
            data_.update(field)

            widget.setData(data_)

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

            self._widgets.append(widget)

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

            self._fieldsFrame.layout().addWidget(widget)

        self.loadPersistentValues()

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

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

    def closeEvent(self, event):
        """Called when the widget is closed."""
        self.savePersistentValues()
        super(FormWidget, self).closeEvent(event)

    def errors(self):
        """
        Get all the errors.

        :rtype: list[str]
        """
        errors = []
        for widget in self._widgets:
            error = widget.data().get("error")
            if error:
                errors.append(error)
        return errors

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

        :rtype: bool
        """
        return bool(self.errors())

    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, widget=None):
        """Validate the current options using the validator."""

        if self._validator:

            logger.debug("Running validator: form.validate(widget=%s)", widget)

            values = {}

            for name, value in self.values().items():
                data = self.widget(name).data()
                if data.get("validate", True):
                    values[name] = value

            if widget:
                values["fieldChanged"] = widget.name()

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

            self.validated.emit()

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

    def setData(self, name, data):
        """
        Set the data for the given field name.

        :type name: str
        :type data: dict
        """
        widget = self.widget(name)
        widget.setData(data)

    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 fields(self):
        """
        Get all the field data for the form.

        :rtype: dict
        """
        fields = []
        for widget in self._widgets:
            fields.append(widget.data())
        return fields

    def fieldWidgets(self):
        """
        Get all the field widgets.

        :rtype: list[FieldWidget]
        """
        return self._widgets

    def setValues(self, values):
        """
        Set the field values for the current form.

        :type values: dict
        """
        state = []
        for name in values:
            state.append({"name": name, "value": values[name]})
        self._setState(state)

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

        :rtype: dict
        """
        values = {}
        for widget in self._widgets:
            name = widget.data().get("name")
            if name:
                values[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:
            name = widget.data().get("name")
            if name:
                values[name] = widget.default()
        return values

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

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

        state = {"fields": fields, "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)

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

        self.validate()

    def _setState(self, fields):
        """
        Set the state while blocking all signals.
        
        :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()
Exemplo n.º 22
0
class BaseItemSignals(QtCore.QObject):
    """"""
    loadValueChanged = QtCore.Signal(object, object)
Exemplo n.º 23
0
class GlobalSignals(QtCore.QObject):
    """"""
    blendChanged = QtCore.Signal(float)