class RecordSignal(QtCore.QObject): """""" onSaved = QtCore.Signal(object) onSaving = QtCore.Signal(object) onLoaded = QtCore.Signal(object) onDeleted = QtCore.Signal(object) onDeleting = QtCore.Signal(object)
class SettingsDialogSignal(QtCore.QObject): """ """ onNameChanged = QtCore.Signal(object) onPathChanged = QtCore.Signal(object) onColorChanged = QtCore.Signal(object) onBackgroundColorChanged = QtCore.Signal(object)
class BaseWidget(QtWidgets.QWidget): """Base widget for creating and previewing transfer items.""" stateChanged = QtCore.Signal(object) def __init__(self, item, parent=None): """ Create a new widget for the given item. :type item: BaseItem :type parent: studiolibrary.LibraryWidget """ QtWidgets.QWidget.__init__(self, parent) self.setObjectName("studioLibraryItemWidget") studioqt.loadUi(self) self._item = None self._iconPath = "" self._scriptJob = None self.setItem(item) self.loadSettings() try: self.selectionChanged() self.enableScriptJob() except NameError, msg: logger.exception(msg) self.updateThumbnailSize()
class ThemesMenu(QtWidgets.QMenu): themeTriggered = QtCore.Signal(object) def __init__(self, parent=None, themes=None): """ :type themes: list[Theme] :rtype: QtWidgets.QMenu """ QtWidgets.QMenu.__init__(self, "Themes", parent) if not themes: themes = themePresets() for theme in themes: action = ThemeAction(theme, self) self.addAction(action) self.triggered.connect(self._themeTriggered) def _themeTriggered(self, action): """ Triggered when a theme has been clicked. :type action: Action :rtype: None """ if isinstance(action, ThemeAction): self.themeTriggered.emit(action.theme())
class BaseWidget(QtWidgets.QWidget): stateChanged = QtCore.Signal(object) def __init__(self, record, parent=None): """ :type record: Record :type parent: studiolibrary.LibraryWidget """ QtWidgets.QWidget.__init__(self, parent) self.setObjectName("studioLibraryPluginsWidget") studioqt.loadUi(self) self._record = None self._iconPath = "" self._scriptJob = None self.setRecord(record) self.loadSettings() try: self.selectionChanged() self.enableScriptJob() except NameError, msg: logger.exception(msg)
class BasePreviewWidget(QtWidgets.QWidget): """Base widget for creating and previewing transfer items.""" stateChanged = QtCore.Signal(object) def __init__(self, item, parent=None): """ :type parent: QtWidgets.QWidget """ QtWidgets.QWidget.__init__(self, parent) self.setObjectName("studioLibraryMayaPreviewWidget") self.setWindowTitle("Preview Item") studioqt.loadUi(self) self._item = None self._iconPath = "" self._scriptJob = None self.setItem(item) self.loadSettings() try: self.selectionChanged() self.setScriptJobEnabled(True) self.updateNamespaceEdit() except NameError, msg: logger.exception(msg) path = self.item().thumbnailPath() if os.path.exists(path): self.setIconPath(path) self.updateThumbnailSize() self.setupConnections()
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)
class IconThread(QtCore.QThread): """A convenience class for loading an icon in a thread.""" triggered = QtCore.Signal(object) def __init__(self, path, *args): QtCore.QThread.__init__(self, *args) self._path = path def run(self): """ The starting point for the thread. :rtype: None """ if self._path: image = QtGui.QImage(unicode(self._path)) self.triggered.emit(image)
class InvokeRepeatingThread(QtCore.QThread): """ A convenience class for invoking a method to the given repeat rate. """ triggered = QtCore.Signal() def __init__(self, repeatRate, *args): QtCore.QThread.__init__(self, *args) self._repeatRate = repeatRate def run(self): """ The starting point for the thread. :rtype: None """ while True: QtCore.QThread.sleep(self._repeatRate) self.triggered.emit()
class ThemesWidget(QtWidgets.QWidget): themeClicked = QtCore.Signal(object) def __init__(self, parent = None, themes = None): """ :type parent: QtWidgets.QWidget :type themes: list[Theme] """ QtWidgets.QWidget.__init__(self, parent) if not themes: themes = themePresets() layout = QtWidgets.QHBoxLayout(self) layout.setSpacing(0) layout.setContentsMargins(0, 0, 0, 0) self.setLayout(layout) for theme in themes: color = theme.accentColor().toString() themeWidget = QtWidgets.QPushButton(self) themeWidget.setStyleSheet('background-color: ' + color) callback = partial(self.themeClicked.emit, theme) themeWidget.clicked.connect(callback) layout.addWidget(themeWidget)
class ThumbnailCaptureDialog(QtWidgets.QDialog): DEFAULT_WIDTH = 250 DEFAULT_HEIGHT = 250 captured = QtCore.Signal(str) capturing = QtCore.Signal(str) def __init__(self, path="", parent=None, startFrame=None, endFrame=None, step=1): """ :type path: str :type parent: QtWidgets.QWidget :type startFrame: int :type endFrame: int :type step: int """ parent = parent or mutils.gui.mayaWindow() QtWidgets.QDialog.__init__(self, parent) self._path = path self._step = step self._endFrame = None self._startFrame = None self._capturedPath = None if endFrame is None: endFrame = int(maya.cmds.currentTime(query=True)) if startFrame is None: startFrame = int(maya.cmds.currentTime(query=True)) self.setEndFrame(endFrame) self.setStartFrame(startFrame) self.setObjectName("CaptureWindow") self.setWindowTitle("Capture Window") self._captureButton = QtWidgets.QPushButton("Capture") self._captureButton.clicked.connect(self.capture) self._modelPanelWidget = mutils.gui.modelpanelwidget.ModelPanelWidget( self) vbox = QtWidgets.QVBoxLayout(self) vbox.setObjectName(self.objectName() + "Layout") vbox.addWidget(self._modelPanelWidget) vbox.addWidget(self._captureButton) self.setLayout(vbox) width = (self.DEFAULT_WIDTH * 1.5) height = (self.DEFAULT_HEIGHT * 1.5) + 50 self.setWidthHeight(width, height) self.centerWindow() def path(self): """ Return the destination path. :rtype: str """ return self._path def setPath(self, path): """ Set the destination path. :type path: str """ self._path = path def endFrame(self): """ Return the end frame of the playblast. :rtype: int """ return self._endFrame def setEndFrame(self, endFrame): """ Specify the end frame of the playblast. :type endFrame: int """ self._endFrame = int(endFrame) def startFrame(self): """ Return the start frame of the playblast. :rtype: int """ return self._startFrame def setStartFrame(self, startFrame): """ Specify the start frame of the playblast. :type startFrame: int """ self._startFrame = int(startFrame) def step(self): """ Return the step amount of the playblast. Example: if step is set to 2 it will playblast every second frame. :rtype: int """ return self._step def setStep(self, step): """ Set the step amount of the playblast. :type step: int """ self._step = step def setWidthHeight(self, width, height): """ Set the width and height of the window. :type width: int :type height: int :rtype: None """ x = self.geometry().x() y = self.geometry().y() self.setGeometry(x, y, width, height) def centerWindow(self): """ Center the widget to the center of the desktop. :rtype: None """ geometry = self.frameGeometry() pos = QtWidgets.QApplication.desktop().cursor().pos() screen = QtWidgets.QApplication.desktop().screenNumber(pos) centerPoint = QtWidgets.QApplication.desktop().screenGeometry( screen).center() geometry.moveCenter(centerPoint) self.move(geometry.topLeft()) def capturedPath(self): """ Return the location of the captured playblast. :rtype: """ return self._capturedPath def capture(self): """ Capture a playblast and save it to the given path. :rtype: None """ path = self.path() self.capturing.emit(path) modelPanel = self._modelPanelWidget.name() startFrame = self.startFrame() endFrame = self.endFrame() step = self.step() width = self.DEFAULT_WIDTH height = self.DEFAULT_HEIGHT self._capturedPath = mutils.playblast.playblast( path, modelPanel, startFrame, endFrame, width, height, step=step, ) self.accept() self.captured.emit(self._capturedPath) return self._capturedPath
class PoseItemSignals(QtCore.QObject): """Signals need to be attached to a QObject""" mirrorChanged = QtCore.Signal(bool)
class PoseItemSignals(QtCore.QObject): """""" mirrorChanged = QtCore.Signal(bool)
class WorkerSignals(QtCore.QObject): triggered = QtCore.Signal(object)
class LibraryItemSignals(QtCore.QObject): """""" saved = QtCore.Signal(object) saving = QtCore.Signal(object) loaded = QtCore.Signal(object) renamed = QtCore.Signal(str, str)
class ControlWidget(QtWidgets.QWidget): controlChanged = QtCore.Signal(str) def __init__(self, *args): QtWidgets.QWidget.__init__(self, *args) layout = QtWidgets.QHBoxLayout(self) self.setLayout(layout) layout.setContentsMargins(0,0,0,0) layout.setSpacing(0) self._dataset = None self._iconPadding = 6 self._iconButton = QtWidgets.QPushButton(self) self._iconButton.clicked.connect(self._onClicked) layout.addWidget(self._iconButton) icon = studiolibrary.resource().icon("pokeball") self._iconButton.setIcon(icon) self._comboBox = QtWidgets.QComboBox(self) layout.addWidget(self._comboBox, 1) self._comboBox.addItem("Select a character", "") self._comboBox.installEventFilter(self) self._comboBox.activated.connect(self._onActivated) self._comboBox.setSizePolicy( QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum) self.setSizePolicy( QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum) self.update() def update(self): self.updateIconColor() def setDataset(self, dataset): """ Set the data set for the search widget: :type dataset: studiolibrary.Dataset """ self._dataset = dataset def dataset(self): """ Get the data set for the search widget. :rtype: studiolibrary.Dataset """ return self._dataset def updateIconColor(self): """ Update the icon colors to the current foregroundRole. :rtype: None """ color = self.palette().color(self._iconButton.foregroundRole()) color = studioqt.Color.fromColor(color) icon = self._iconButton.icon() icon = studioqt.Icon(icon) icon.setColor(color) self._iconButton.setIcon(icon) def resizeEvent(self, event): """ Reimplemented so the icon maintains the same height as the widget. :type event: QtWidgets.QResizeEvent :rtype: None """ QtWidgets.QWidget.resizeEvent(self, event) size = QtCore.QSize(self.height(), self.height()) self._iconButton.setIconSize(size) self._iconButton.setFixedSize(size) def eventFilter(self, source, event): if (event.type() == QtCore.QEvent.MouseButtonPress and source is self._comboBox): #populate the list self._comboBox.clear() self._comboBox.addItem("Select a character", "") for rig in self.dataset().findRigsNamespacesInScene(): self._comboBox.addItem(rig, rig) return super(ControlWidget, self).eventFilter(source, event) def _onClicked(self): self._comboBox.setFocus() def _onActivated(self, id=None): userdata = str( self._comboBox.itemData(id) ) self.dataset().setActiveCharacter(userdata) self.controlChanged.emit(userdata)
class GlobalSignals(QtCore.QObject): """""" blendChanged = QtCore.Signal(float)
class BasePreviewWidget(QtWidgets.QWidget): """Base widget for creating and previewing transfer items.""" stateChanged = QtCore.Signal(object) def __init__(self, item, parent=None): """ :type parent: QtWidgets.QWidget """ QtWidgets.QWidget.__init__(self, parent) self.setObjectName("studioLibraryMayaPreviewWidget") self.setWindowTitle("Preview Item") studioqt.loadUi(self) self._item = None self._iconPath = "" self._scriptJob = None self.setItem(item) self.loadSettings() try: self.selectionChanged() self.setScriptJobEnabled(True) self.updateNamespaceEdit() except NameError as error: logger.exception(error) path = self.item().thumbnailPath() if os.path.exists(path): self.setIconPath(path) self.updateThumbnailSize() self.setupConnections() def setupConnections(self): """ :rtype: None """ self.ui.acceptButton.clicked.connect(self.accept) self.ui.selectionSetButton.clicked.connect(self.showSelectionSetsMenu) self.ui.useFileNamespace.clicked.connect(self._namespaceOptionClicked) self.ui.useCustomNamespace.clicked.connect(self._useCustomNamespaceClicked) self.ui.useSelectionNamespace.clicked.connect(self._namespaceOptionClicked) self.ui.namespaceComboBox.activated[str].connect(self._namespaceEditChanged) self.ui.namespaceComboBox.editTextChanged[str].connect(self._namespaceEditChanged) self.ui.namespaceComboBox.currentIndexChanged[str].connect(self._namespaceEditChanged) self.ui.iconToggleBoxButton.clicked.connect(self.saveSettings) self.ui.infoToggleBoxButton.clicked.connect(self.saveSettings) self.ui.optionsToggleBoxButton.clicked.connect(self.saveSettings) self.ui.namespaceToggleBoxButton.clicked.connect(self.saveSettings) self.ui.iconToggleBoxButton.toggled[bool].connect(self.ui.iconToggleBoxFrame.setVisible) self.ui.infoToggleBoxButton.toggled[bool].connect(self.ui.infoToggleBoxFrame.setVisible) self.ui.optionsToggleBoxButton.toggled[bool].connect(self.ui.optionsToggleBoxFrame.setVisible) self.ui.namespaceToggleBoxButton.toggled[bool].connect(self.ui.namespaceToggleBoxFrame.setVisible) def isEditable(self): """ Return True if the user can edit the item. :rtype: bool """ item = self.item() editable = True if item and item.libraryWindow(): editable = not item.libraryWindow().isLocked() return editable def setCaptureMenuEnabled(self, enable): """ Enable the capture menu for editing the thumbnail. :rtype: None """ if enable: parent = self.item().libraryWindow() iconPath = self.iconPath() if iconPath == "": iconPath = self.item().thumbnailPath() menu = mutils.gui.ThumbnailCaptureMenu(iconPath, parent=parent) menu.captured.connect(self.setIconPath) self.ui.thumbnailButton.setMenu(menu) else: self.ui.thumbnailButton.setMenu(QtWidgets.QMenu(self)) def item(self): """ Return the library item to be created. :rtype: studiolibrarymaya.BaseItem """ return self._item def setItem(self, item): """ Set the item for the preview widget. :type item: BaseItem """ self._item = item self.ui.name.setText(item.name()) self.ui.owner.setText(item.owner()) self.ui.comment.setText(item.description()) self.updateContains() ctime = item.ctime() if ctime: self.ui.created.setText(studiolibrary.timeAgo(ctime)) def iconPath(self): """ Return the icon path to be used for the thumbnail. :rtype str """ return self._iconPath def setIconPath(self, path): """ Set the icon path to be used for the thumbnail. :type path: str :rtype: None """ self._iconPath = path icon = QtGui.QIcon(QtGui.QPixmap(path)) self.setIcon(icon) self.updateThumbnailSize() self.item().update() def setIcon(self, icon): """ Set the icon to be shown for the preview. :type icon: QtGui.QIcon """ self.ui.thumbnailButton.setIcon(icon) self.ui.thumbnailButton.setIconSize(QtCore.QSize(200, 200)) self.ui.thumbnailButton.setText("") def showSelectionSetsMenu(self): """ :rtype: None """ item = self.item() item.showSelectionSetsMenu() def resizeEvent(self, event): """ Overriding to adjust the image size when the widget changes size. :type event: QtCore.QSizeEvent """ self.updateThumbnailSize() def updateThumbnailSize(self): """ Update the thumbnail button to the size of the widget. :rtype: None """ if hasattr(self.ui, "thumbnailButton"): width = self.width() - 10 if width > 250: width = 250 size = QtCore.QSize(width, width) self.ui.thumbnailButton.setIconSize(size) self.ui.thumbnailButton.setMaximumSize(size) self.ui.thumbnailFrame.setMaximumSize(size) def close(self): """ Overriding the close method so that we can disable the script job. :rtype: None """ self.setScriptJobEnabled(False) QtWidgets.QWidget.close(self) def scriptJob(self): """ Return the script job object used when the users selection changes. :rtype: mutils.ScriptJob """ return self._scriptJob def setScriptJobEnabled(self, enable): """ Enable the script job used when the users selection changes. :rtype: None """ if enable: if not self._scriptJob: event = ['SelectionChanged', self.selectionChanged] self._scriptJob = mutils.ScriptJob(event=event) else: sj = self.scriptJob() if sj: sj.kill() self._scriptJob = None def objectCount(self): """ Return the number of controls contained in the item. :rtype: int """ return self.item().objectCount() def updateContains(self): """ Refresh the contains information. :rtype: None """ count = self.objectCount() plural = "s" if count > 1 else "" self.ui.contains.setText(str(count) + " Object" + plural) def _namespaceEditChanged(self, text): """ Triggered when the combox box has changed value. :type text: str :rtype: None """ self.ui.useCustomNamespace.setChecked(True) self.ui.namespaceComboBox.setEditText(text) self.saveSettings() def _namespaceOptionClicked(self): self.updateNamespaceEdit() self.saveSettings() def _useCustomNamespaceClicked(self): """ Triggered when the custom namespace radio button is clicked. :rtype: None """ self.ui.namespaceComboBox.setFocus() self.updateNamespaceEdit() self.saveSettings() def namespaces(self): """ Return the namespace names from the namespace edit widget. :rtype: list[str] """ namespaces = str(self.ui.namespaceComboBox.currentText()) namespaces = studiolibrary.stringToList(namespaces) return namespaces def setNamespaces(self, namespaces): """ Set the namespace names for the namespace edit. :type namespaces: list :rtype: None """ namespaces = studiolibrary.listToString(namespaces) self.ui.namespaceComboBox.setEditText(namespaces) def namespaceOption(self): """ :rtype: NamespaceOption """ if self.ui.useFileNamespace.isChecked(): namespaceOption = NamespaceOption.FromFile elif self.ui.useCustomNamespace.isChecked(): namespaceOption = NamespaceOption.FromCustom else: namespaceOption = NamespaceOption.FromSelection return namespaceOption def setNamespaceOption(self, namespaceOption): """ :type namespaceOption: NamespaceOption """ if namespaceOption == NamespaceOption.FromFile: self.ui.useFileNamespace.setChecked(True) elif namespaceOption == NamespaceOption.FromCustom: self.ui.useCustomNamespace.setChecked(True) else: self.ui.useSelectionNamespace.setChecked(True) def setSettings(self, settings): """ :type settings: dict """ namespaces = settings.get("namespaces", []) self.setNamespaces(namespaces) namespaceOption = settings.get("namespaceOption", NamespaceOption.FromFile) self.setNamespaceOption(namespaceOption) toggleBoxChecked = settings.get("iconToggleBoxChecked", True) self.ui.iconToggleBoxFrame.setVisible(toggleBoxChecked) self.ui.iconToggleBoxButton.setChecked(toggleBoxChecked) toggleBoxChecked = settings.get("infoToggleBoxChecked", True) self.ui.infoToggleBoxFrame.setVisible(toggleBoxChecked) self.ui.infoToggleBoxButton.setChecked(toggleBoxChecked) toggleBoxChecked = settings.get("optionsToggleBoxChecked", True) self.ui.optionsToggleBoxFrame.setVisible(toggleBoxChecked) self.ui.optionsToggleBoxButton.setChecked(toggleBoxChecked) toggleBoxChecked = settings.get("namespaceToggleBoxChecked", True) self.ui.namespaceToggleBoxFrame.setVisible(toggleBoxChecked) self.ui.namespaceToggleBoxButton.setChecked(toggleBoxChecked) def settings(self): """ :rtype: dict """ settings = {} settings["namespaces"] = self.namespaces() settings["namespaceOption"] = self.namespaceOption() settings["iconToggleBoxChecked"] = self.ui.iconToggleBoxButton.isChecked() settings["infoToggleBoxChecked"] = self.ui.infoToggleBoxButton.isChecked() settings["optionsToggleBoxChecked"] = self.ui.optionsToggleBoxButton.isChecked() settings["namespaceToggleBoxChecked"] = self.ui.namespaceToggleBoxButton.isChecked() return settings def loadSettings(self): """ :rtype: None """ data = studiolibrarymaya.settings() self.setSettings(data) def saveSettings(self): """ :rtype: None """ data = self.settings() studiolibrarymaya.saveSettings(data) def selectionChanged(self): """ :rtype: None """ self.updateNamespaceEdit() def updateNamespaceFromScene(self): """ Update the namespaces in the combobox with the ones in the scene. :rtype: None """ IGNORE_NAMESPACES = ['UI', 'shared'] if studiolibrary.isMaya(): namespaces = maya.cmds.namespaceInfo(listOnlyNamespaces=True) else: namespaces = [] namespaces = list(set(namespaces) - set(IGNORE_NAMESPACES)) namespaces = sorted(namespaces) text = self.ui.namespaceComboBox.currentText() if namespaces: self.ui.namespaceComboBox.setToolTip("") else: toolTip = "No namespaces found in scene." self.ui.namespaceComboBox.setToolTip(toolTip) self.ui.namespaceComboBox.clear() self.ui.namespaceComboBox.addItems(namespaces) self.ui.namespaceComboBox.setEditText(text) def updateNamespaceEdit(self): """ :rtype: None """ logger.debug('Updating namespace edit') self.ui.namespaceComboBox.blockSignals(True) self.updateNamespaceFromScene() namespaces = [] if self.ui.useSelectionNamespace.isChecked(): namespaces = mutils.namespace.getFromSelection() elif self.ui.useFileNamespace.isChecked(): namespaces = self.item().transferObject().namespaces() if not self.ui.useCustomNamespace.isChecked(): self.setNamespaces(namespaces) # Removes focus from the combobox self.ui.namespaceComboBox.setEnabled(False) self.ui.namespaceComboBox.setEnabled(True) self.ui.namespaceComboBox.blockSignals(False) def accept(self): """ :rtype: None """ try: self.item().load() except Exception as error: self.item().showErrorDialog("Error while loading", str(error)) raise
class BaseLoadWidget(QtWidgets.QWidget): """Base widget for creating and previewing transfer items.""" stateChanged = QtCore.Signal(object) def __init__(self, item, parent=None): """ :type parent: QtWidgets.QWidget """ QtWidgets.QWidget.__init__(self, parent) self.setObjectName("studioLibraryMayaPreviewWidget") self.setWindowTitle("Preview Item") studioqt.loadUi(self) self._item = None self._iconPath = "" self._scriptJob = None self._optionsWidget = None self.setItem(item) self.loadSettings() try: self.selectionChanged() self.setScriptJobEnabled(True) self.updateNamespaceEdit() except NameError as error: logger.exception(error) self.createSequenceWidget() self.updateThumbnailSize() self.setupConnections() def setupConnections(self): """Setup the connections for all the widgets.""" self.ui.acceptButton.clicked.connect(self.accept) self.ui.selectionSetButton.clicked.connect(self.showSelectionSetsMenu) self.ui.useFileNamespace.clicked.connect(self._namespaceOptionClicked) self.ui.useCustomNamespace.clicked.connect( self._useCustomNamespaceClicked) self.ui.useSelectionNamespace.clicked.connect( self._namespaceOptionClicked) self.ui.namespaceComboBox.activated[str].connect( self._namespaceEditChanged) self.ui.namespaceComboBox.editTextChanged[str].connect( self._namespaceEditChanged) self.ui.namespaceComboBox.currentIndexChanged[str].connect( self._namespaceEditChanged) self.ui.iconToggleBoxButton.clicked.connect(self.saveSettings) self.ui.infoToggleBoxButton.clicked.connect(self.saveSettings) self.ui.optionsToggleBoxButton.clicked.connect(self.saveSettings) self.ui.namespaceToggleBoxButton.clicked.connect(self.saveSettings) self.ui.iconToggleBoxButton.toggled[bool].connect( self.ui.iconToggleBoxFrame.setVisible) self.ui.infoToggleBoxButton.toggled[bool].connect( self.ui.infoToggleBoxFrame.setVisible) self.ui.optionsToggleBoxButton.toggled[bool].connect( self.ui.optionsToggleBoxFrame.setVisible) self.ui.namespaceToggleBoxButton.toggled[bool].connect( self.ui.namespaceToggleBoxFrame.setVisible) def createSequenceWidget(self): """ Create a sequence widget to replace the static thumbnail widget. :rtype: None """ self.ui.sequenceWidget = studiolibrary.widgets.ImageSequenceWidget( self) self.ui.sequenceWidget.setStyleSheet( self.ui.thumbnailButton.styleSheet()) self.ui.sequenceWidget.setToolTip(self.ui.thumbnailButton.toolTip()) self.ui.thumbnailFrame.layout().insertWidget(0, self.ui.sequenceWidget) self.ui.thumbnailButton.hide() self.ui.thumbnailButton = self.ui.sequenceWidget path = self.item().thumbnailPath() if os.path.exists(path): self.setIconPath(path) if self.item().imageSequencePath(): self.ui.sequenceWidget.setDirname(self.item().imageSequencePath()) def isEditable(self): """ Return True if the user can edit the item. :rtype: bool """ item = self.item() editable = True if item and item.libraryWindow(): editable = not item.libraryWindow().isLocked() return editable def setCaptureMenuEnabled(self, enable): """ Enable the capture menu for editing the thumbnail. :rtype: None """ if enable: parent = self.item().libraryWindow() iconPath = self.iconPath() if iconPath == "": iconPath = self.item().thumbnailPath() menu = mutils.gui.ThumbnailCaptureMenu(iconPath, parent=parent) menu.captured.connect(self.setIconPath) self.ui.thumbnailButton.setMenu(menu) else: self.ui.thumbnailButton.setMenu(QtWidgets.QMenu(self)) def item(self): """ Return the library item to be created. :rtype: studiolibrarymaya.BaseItem """ return self._item def _itemValueChanged(self, field, value): """ :type field: str :type value: object """ self._optionsWidget.setValue(field, value) def setItem(self, item): """ Set the item for the preview widget. :type item: BaseItem """ self._item = item if hasattr(self.ui, "titleLabel"): self.ui.titleLabel.setText(item.MenuName) if hasattr(self.ui, "iconLabel"): self.ui.iconLabel.setPixmap(QtGui.QPixmap(item.TypeIconPath)) if hasattr(self.ui, "infoFrame"): infoWidget = studiolibrary.widgets.FormWidget(self) infoWidget.setSchema(item.info()) self.ui.infoFrame.layout().addWidget(infoWidget) if hasattr(self.ui, "optionsFrame"): options = item.loadSchema() if options: item.loadValueChanged.connect(self._itemValueChanged) optionsWidget = studiolibrary.widgets.FormWidget(self) optionsWidget.setSchema(item.loadSchema()) optionsWidget.setValidator(item.loadValidator) optionsWidget.setStateFromOptions( self.item().optionsFromSettings()) optionsWidget.stateChanged.connect(self.optionsChanged) self.ui.optionsFrame.layout().addWidget(optionsWidget) self._optionsWidget = optionsWidget optionsWidget.validate() else: self.ui.optionsToggleBox.setVisible(False) def optionsChanged(self): self.item().optionsChanged(**self._optionsWidget.optionsToDict()) def iconPath(self): """ Return the icon path to be used for the thumbnail. :rtype str """ return self._iconPath def setIconPath(self, path): """ Set the icon path to be used for the thumbnail. :type path: str :rtype: None """ self._iconPath = path icon = QtGui.QIcon(QtGui.QPixmap(path)) self.setIcon(icon) self.updateThumbnailSize() self.item().update() def setIcon(self, icon): """ Set the icon to be shown for the preview. :type icon: QtGui.QIcon """ self.ui.thumbnailButton.setIcon(icon) self.ui.thumbnailButton.setIconSize(QtCore.QSize(200, 200)) self.ui.thumbnailButton.setText("") def showSelectionSetsMenu(self): """Show the selection sets menu.""" item = self.item() item.showSelectionSetsMenu() def resizeEvent(self, event): """ Overriding to adjust the image size when the widget changes size. :type event: QtCore.QSizeEvent """ self.updateThumbnailSize() def updateThumbnailSize(self): """ Update the thumbnail button to the size of the widget. :rtype: None """ if hasattr(self.ui, "thumbnailButton"): width = self.width() - 10 if width > 250: width = 250 size = QtCore.QSize(width, width) self.ui.thumbnailButton.setIconSize(size) self.ui.thumbnailButton.setMaximumSize(size) self.ui.thumbnailFrame.setMaximumSize(size) def close(self): """ Overriding the close method so that we can disable the script job. :rtype: None """ self.setScriptJobEnabled(False) QtWidgets.QWidget.close(self) def scriptJob(self): """ Get the script job object used when the users selection changes. :rtype: mutils.ScriptJob """ return self._scriptJob def setScriptJobEnabled(self, enable): """ Enable the script job used when the users selection changes. :rtype: None """ if enable: if not self._scriptJob: event = ['SelectionChanged', self.selectionChanged] self._scriptJob = mutils.ScriptJob(event=event) else: sj = self.scriptJob() if sj: sj.kill() self._scriptJob = None def objectCount(self): """ Return the number of controls contained in the item. :rtype: int """ return self.item().objectCount() def _namespaceEditChanged(self, text): """ Triggered when the combox box has changed value. :type text: str :rtype: None """ self.ui.useCustomNamespace.setChecked(True) self.ui.namespaceComboBox.setEditText(text) self.saveSettings() def _namespaceOptionClicked(self): self.updateNamespaceEdit() self.saveSettings() def _useCustomNamespaceClicked(self): """ Triggered when the custom namespace radio button is clicked. :rtype: None """ self.ui.namespaceComboBox.setFocus() self.updateNamespaceEdit() self.saveSettings() def namespaces(self): """ Return the namespace names from the namespace edit widget. :rtype: list[str] """ namespaces = str(self.ui.namespaceComboBox.currentText()) namespaces = studiolibrary.stringToList(namespaces) return namespaces def setNamespaces(self, namespaces): """ Set the namespace names for the namespace edit. :type namespaces: list :rtype: None """ namespaces = studiolibrary.listToString(namespaces) self.ui.namespaceComboBox.setEditText(namespaces) def namespaceOption(self): """ Get the current namespace option. :rtype: NamespaceOption """ if self.ui.useFileNamespace.isChecked(): namespaceOption = NamespaceOption.FromFile elif self.ui.useCustomNamespace.isChecked(): namespaceOption = NamespaceOption.FromCustom else: namespaceOption = NamespaceOption.FromSelection return namespaceOption def setNamespaceOption(self, namespaceOption): """ Set the current namespace option. :type namespaceOption: NamespaceOption """ if namespaceOption == NamespaceOption.FromFile: self.ui.useFileNamespace.setChecked(True) elif namespaceOption == NamespaceOption.FromCustom: self.ui.useCustomNamespace.setChecked(True) else: self.ui.useSelectionNamespace.setChecked(True) def setSettings(self, settings): """ Set the state of the widget. :type settings: dict """ namespaces = settings.get("namespaces", []) self.setNamespaces(namespaces) namespaceOption = settings.get("namespaceOption", NamespaceOption.FromFile) self.setNamespaceOption(namespaceOption) toggleBoxChecked = settings.get("iconToggleBoxChecked", True) self.ui.iconToggleBoxFrame.setVisible(toggleBoxChecked) self.ui.iconToggleBoxButton.setChecked(toggleBoxChecked) toggleBoxChecked = settings.get("infoToggleBoxChecked", True) self.ui.infoToggleBoxFrame.setVisible(toggleBoxChecked) self.ui.infoToggleBoxButton.setChecked(toggleBoxChecked) toggleBoxChecked = settings.get("optionsToggleBoxChecked", True) self.ui.optionsToggleBoxFrame.setVisible(toggleBoxChecked) self.ui.optionsToggleBoxButton.setChecked(toggleBoxChecked) toggleBoxChecked = settings.get("namespaceToggleBoxChecked", True) self.ui.namespaceToggleBoxFrame.setVisible(toggleBoxChecked) self.ui.namespaceToggleBoxButton.setChecked(toggleBoxChecked) def settings(self): """ Get the current state of the widget. :rtype: dict """ settings = {} settings["namespaces"] = self.namespaces() settings["namespaceOption"] = self.namespaceOption() settings[ "iconToggleBoxChecked"] = self.ui.iconToggleBoxButton.isChecked() settings[ "infoToggleBoxChecked"] = self.ui.infoToggleBoxButton.isChecked() settings[ "optionsToggleBoxChecked"] = self.ui.optionsToggleBoxButton.isChecked( ) settings[ "namespaceToggleBoxChecked"] = self.ui.namespaceToggleBoxButton.isChecked( ) return settings def loadSettings(self): """ Load the user settings from disc. :rtype: None """ data = studiolibrarymaya.settings() self.setSettings(data) def saveSettings(self): """ Save the user settings to disc. :rtype: None """ data = self.settings() studiolibrarymaya.saveSettings(data) def selectionChanged(self): """ Triggered when the users Maya selection has changed. :rtype: None """ self.updateNamespaceEdit() def updateNamespaceFromScene(self): """ Update the namespaces in the combobox with the ones in the scene. :rtype: None """ namespaces = mutils.namespace.getAll() text = self.ui.namespaceComboBox.currentText() if namespaces: self.ui.namespaceComboBox.setToolTip("") else: toolTip = "No namespaces found in scene." self.ui.namespaceComboBox.setToolTip(toolTip) self.ui.namespaceComboBox.clear() self.ui.namespaceComboBox.addItems(namespaces) self.ui.namespaceComboBox.setEditText(text) def updateNamespaceEdit(self): """ Update the namespace edit. :rtype: None """ logger.debug('Updating namespace edit') self.ui.namespaceComboBox.blockSignals(True) self.updateNamespaceFromScene() namespaces = [] if self.ui.useSelectionNamespace.isChecked(): namespaces = mutils.namespace.getFromSelection() elif self.ui.useFileNamespace.isChecked(): namespaces = self.item().transferObject().namespaces() if not self.ui.useCustomNamespace.isChecked(): self.setNamespaces(namespaces) # Removes focus from the combobox self.ui.namespaceComboBox.setEnabled(False) self.ui.namespaceComboBox.setEnabled(True) self.ui.namespaceComboBox.blockSignals(False) def accept(self): """ Called when the user clicks the apply button. :rtype: None """ self.item().loadFromCurrentOptions()
class Theme(QtCore.QObject): updated = QtCore.Signal() DEFAULT_DARK_COLOR = QtGui.QColor(60, 60, 60) DEFAULT_LIGHT_COLOR = QtGui.QColor(220, 220, 220) DEFAULT_ACCENT_COLOR = QtGui.QColor(0, 175, 255) DEFAULT_BACKGROUND_COLOR = QtGui.QColor(60, 60, 80) def __init__(self): QtCore.QObject.__init__(self) self._dpi = 1 self._name = "Default" self._accentColor = None self._backgroundColor = None self.setAccentColor(self.DEFAULT_ACCENT_COLOR) self.setBackgroundColor(self.DEFAULT_BACKGROUND_COLOR) def settings(self): """ Return a dictionary of settings for the current Theme. :rtype: dict """ settings = {} settings["name"] = self.name() accentColor = self.accentColor() settings["accentColor"] = accentColor.toString() backgroundColor = self.backgroundColor() settings["backgroundColor"] = backgroundColor.toString() return settings def setSettings(self, settings): """ Set a dictionary of settings for the current Theme. :type settings: dict :rtype: None """ name = settings.get("name") self.setName(name) color = settings.get("accentColor") if color: color = studioqt.Color.fromString(color) self.setAccentColor(color) color = settings.get("backgroundColor") if color: color = studioqt.Color.fromString(color) self.setBackgroundColor(color) def dpi(self): """ Return the dpi for the Theme :rtype: float """ return self._dpi def setDpi(self, dpi): """ Set the dpi for the Theme :type dpi: float :rtype: None """ self._dpi = dpi def name(self): """ Return the name for the Theme :rtype: str """ return self._name def setName(self, name): """ Set the name for the Theme :type name: str :rtype: None """ self._name = name def isDark(self): """ Return True if the current theme is dark. rtype: bool """ # The luminance for digital formats are (0.299, 0.587, 0.114) red = self.backgroundColor().redF() * 0.299 green = self.backgroundColor().greenF() * 0.587 blue = self.backgroundColor().blueF() * 0.114 darkness = red + green + blue if darkness < 0.6: return True return False def setDark(self): """ Set the current theme to the default dark color. :rtype: None """ self.setBackgroundColor(self.DEFAULT_DARK_COLOR) def setLight(self): """ Set the current theme to the default light color. :rtype: None """ self.setBackgroundColor(self.DEFAULT_LIGHT_COLOR) def iconColor(self): """ Return the icon color for the theme. :rtype: studioqt.Color """ return self.forgroundColor() def accentForgroundColor(self): """ Return the foreground color for the accent color. :rtype: studioqt.Color """ return studioqt.Color(255, 255, 255, 255) def forgroundColor(self): """ Return the foreground color for the theme. :rtype: studioqt.Color """ if self.isDark(): return studioqt.Color(250, 250, 250, 225) else: return studioqt.Color(0, 40, 80, 180) def itemBackgroundColor(self): """ Return the item background color. :rtype: studioqt.Color """ if self.isDark(): return studioqt.Color(255, 255, 255, 20) else: return studioqt.Color(255, 255, 255, 120) def itemBackgroundHoverColor(self): """ Return the item background color when the mouse hovers over the item. :rtype: studioqt.Color """ return studioqt.Color(255, 255, 255, 60) def accentColor(self): """ Return the accent color for the theme. :rtype: studioqt.Color or None """ return self._accentColor def backgroundColor(self): """ Return the background color for the theme. :rtype: studioqt.Color or None """ return self._backgroundColor def setAccentColor(self, color): """ Set the accent color for the theme. :type color: studioqt.Color | QtGui.QColor """ if isinstance(color, basestring): color = studioqt.Color.fromString(color) if isinstance(color, QtGui.QColor): color = studioqt.Color.fromColor(color) self._accentColor = color self.updated.emit() def setBackgroundColor(self, color): """ Set the background color for the theme. :type color: studioqt.Color | QtGui.QColor """ if isinstance(color, basestring): color = studioqt.Color.fromString(color) if isinstance(color, QtGui.QColor): color = studioqt.Color.fromColor(color) self._backgroundColor = color self.updated.emit() def createColorDialog( self, parent, standardColors=None, currentColor=None, ): """ Create a new instance of the color dialog. :type parent: QtWidgets.QWidget :type standardColors: list[int] :rtype: QtWidgets.QColorDialog """ dialog = QtWidgets.QColorDialog(parent) if standardColors: index = -1 for colorR, colorG, colorB in standardColors: index += 1 color = QtGui.QColor(colorR, colorG, colorB).rgba() try: # Support for new qt5 signature color = QtGui.QColor(color) dialog.setStandardColor(index, color) except: # Support for new qt4 signature color = QtGui.QColor(color).rgba() dialog.setStandardColor(index, color) # PySide2 doesn't support d.open(), so we need to pass a blank slot. dialog.open(self, QtCore.SLOT("blankSlot()")) if currentColor: dialog.setCurrentColor(currentColor) return dialog def browseAccentColor(self, parent=None): """ Show the color dialog for changing the accent color. :type parent: QtWidgets.QWidget :rtype: None """ standardColors = [ (230, 60, 60), (210, 40, 40), (190, 20, 20), (250, 80, 130), (230, 60, 110), (210, 40, 90), (255, 90, 40), (235, 70, 20), (215, 50, 0), (240, 100, 170), (220, 80, 150), (200, 60, 130), (255, 125, 100), (235, 105, 80), (215, 85, 60), (240, 200, 150), (220, 180, 130), (200, 160, 110), (250, 200, 0), (230, 180, 0), (210, 160, 0), (225, 200, 40), (205, 180, 20), (185, 160, 0), (80, 200, 140), (60, 180, 120), (40, 160, 100), (80, 225, 120), (60, 205, 100), (40, 185, 80), (50, 180, 240), (30, 160, 220), (10, 140, 200), (100, 200, 245), (80, 180, 225), (60, 160, 205), (130, 110, 240), (110, 90, 220), (90, 70, 200), (180, 160, 255), (160, 140, 235), (140, 120, 215), (180, 110, 240), (160, 90, 220), (140, 70, 200), (210, 110, 255), (190, 90, 235), (170, 70, 215) ] currentColor = self.accentColor() dialog = self.createColorDialog(parent, standardColors, currentColor) dialog.currentColorChanged.connect(self.setAccentColor) if dialog.exec_(): self.setAccentColor(dialog.selectedColor()) else: self.setAccentColor(currentColor) def browseBackgroundColor(self, parent=None): """ Show the color dialog for changing the background color. :type parent: QtWidgets.QWidget :rtype: None """ standardColors = [ (0, 0, 0), (20, 20, 20), (40, 40, 40), (60, 60, 60), (80, 80, 80), (100, 100, 100), (20, 20, 30), (40, 40, 50), (60, 60, 70), (80, 80, 90), (100, 100, 110), (120, 120, 130), (0, 30, 60), (20, 50, 80), (40, 70, 100), (60, 90, 120), (80, 110, 140), (100, 130, 160), (0, 60, 60), (20, 80, 80), (40, 100, 100), (60, 120, 120), (80, 140, 140), (100, 160, 160), (0, 60, 30), (20, 80, 50), (40, 100, 70), (60, 120, 90), (80, 140, 110), (100, 160, 130), (60, 0, 10), (80, 20, 30), (100, 40, 50), (120, 60, 70), (140, 80, 90), (160, 100, 110), (60, 0, 40), (80, 20, 60), (100, 40, 80), (120, 60, 100), (140, 80, 120), (160, 100, 140), (40, 15, 5), (60, 35, 25), (80, 55, 45), (100, 75, 65), (120, 95, 85), (140, 115, 105) ] currentColor = self.backgroundColor() dialog = self.createColorDialog(parent, standardColors, currentColor) dialog.currentColorChanged.connect(self.setBackgroundColor) if dialog.exec_(): self.setBackgroundColor(dialog.selectedColor()) else: self.setBackgroundColor(currentColor) def options(self): """ Return the variables used to customise the style sheet. :rtype: dict """ accentColor = self.accentColor() accentForegroundColor = self.accentForgroundColor() foregroundColor = self.forgroundColor() backgroundColor = self.backgroundColor() itemBackgroundColor = self.itemBackgroundColor() itemBackgroundHoverColor = self.itemBackgroundHoverColor() if self.isDark(): darkness = "white" else: darkness = "black" resourceDirname = studiolibrary.resource().dirname() resourceDirname = resourceDirname.replace("\\", "/") options = { "DARKNESS": darkness, "RESOURCE_DIRNAME": resourceDirname, "ACCENT_COLOR": accentColor.toString(), "ACCENT_COLOR_R": str(accentColor.red()), "ACCENT_COLOR_G": str(accentColor.green()), "ACCENT_COLOR_B": str(accentColor.blue()), "ACCENT_FOREGROUND_COLOR": accentForegroundColor.toString(), "FOREGROUND_COLOR": foregroundColor.toString(), "FOREGROUND_COLOR_R": str(foregroundColor.red()), "FOREGROUND_COLOR_G": str(foregroundColor.green()), "FOREGROUND_COLOR_B": str(foregroundColor.blue()), "BACKGROUND_COLOR": backgroundColor.toString(), "BACKGROUND_COLOR_R": str(backgroundColor.red()), "BACKGROUND_COLOR_G": str(backgroundColor.green()), "BACKGROUND_COLOR_B": str(backgroundColor.blue()), "ITEM_TEXT_COLOR": foregroundColor.toString(), "ITEM_TEXT_SELECTED_COLOR": accentForegroundColor.toString(), "ITEM_BACKGROUND_COLOR": itemBackgroundColor.toString(), "ITEM_BACKGROUND_HOVER_COLOR": itemBackgroundHoverColor.toString(), "ITEM_BACKGROUND_SELECTED_COLOR": accentColor.toString(), } return options def styleSheet(self): """ Return the style sheet for this theme. :rtype: str """ options = self.options() path = studiolibrary.resource().get("css", "default.css") styleSheet = studioqt.StyleSheet.fromPath(path, options=options, dpi=self.dpi()) return styleSheet.data()
class FormWidget(QtWidgets.QFrame): accepted = QtCore.Signal(object) stateChanged = QtCore.Signal() validated = QtCore.Signal() def __init__(self, *args, **kwargs): super(FormWidget, self).__init__(*args, **kwargs) self._schema = [] self._widgets = [] self._validator = None layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) self.setLayout(layout) self._optionsFrame = QtWidgets.QFrame(self) self._optionsFrame.setObjectName("optionsFrame") layout = QtWidgets.QVBoxLayout(self._optionsFrame) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) self._optionsFrame.setLayout(layout) self._titleWidget = QtWidgets.QPushButton(self) self._titleWidget.setCheckable(True) self._titleWidget.setObjectName("titleWidget") self._titleWidget.toggled.connect(self._titleClicked) self._titleWidget.hide() self.layout().addWidget(self._titleWidget) self.layout().addWidget(self._optionsFrame) def _titleClicked(self, toggle): """Triggered when the user clicks the title widget.""" self.setExpanded(toggle) self.stateChanged.emit() def titleWidget(self): """ Get the title widget. :rtype: QWidget """ return self._titleWidget def setTitle(self, title): """ Set the text for the title widget. :type title: str """ self.titleWidget().setText(title) def setExpanded(self, expand): """ Expands the options if expand is true, otherwise collapses the options. :type expand: bool """ self._titleWidget.blockSignals(True) try: self._titleWidget.setChecked(expand) self._optionsFrame.setVisible(expand) finally: self._titleWidget.blockSignals(False) def isExpanded(self): """ Returns true if the item is expanded, otherwise returns false. :rtype: bool """ return self._titleWidget.isChecked() def setTitleVisible(self, visible): """ A convenience method for setting the title visible. :type visible: bool """ self.titleWidget().setVisible(visible) def reset(self): """Reset all option widgets back to their default value.""" for widget in self._widgets: widget.reset() self.validate() def setSchema(self, schema, layout=None): """ Set the schema for the widget. :type schema: list[dict] """ self._schema = schema for data in schema: cls = FIELD_WIDGET_REGISTRY.get(data.get("type", "label")) if not cls: logger.warning("Cannot find widget for %s", data) continue if layout and not data.get("layout"): data["layout"] = layout widget = cls(data=data) widget.setData(data) value = data.get("value") default = data.get("default") if value is None and default is not None: widget.setValue(default) self._widgets.append(widget) callback = functools.partial(self._optionChanged, widget) widget.valueChanged.connect(callback) self._optionsFrame.layout().addWidget(widget) def _optionChanged(self, widget): """ Triggered when the given option widget changes value. :type widget: FieldWidget """ self.validate() def accept(self): """Accept the current options""" self.emitAcceptedCallback() def closeEvent(self, event): super(FormWidget, self).closeEvent(event) def hasErrors(self): """ Return True if the form contains any errors. :rtype: bool """ for widget in self._widgets: if widget.data().get("error"): return True return False def setValidator(self, validator): """ Set the validator for the options. :type validator: func """ self._validator = validator def validator(self): """ Return the validator for the form. :rtype: func """ return self._validator def validate(self): """Validate the current options using the validator.""" if self._validator: fields = self._validator(**self.values()) if fields is not None: self._setState(fields) self.validated.emit() else: logger.debug("No validator set.") def setValue(self, name, value): """ Set the value for the given field name and value :type name: str :type value: object """ widget = self.widget(name) widget.setValue(value) def value(self, name): """ Get the value for the given widget name. :type name: str :rtype: object """ widget = self.widget(name) return widget.value() def widget(self, name): """ Get the widget for the given widget name. :type name: str :rtype: FieldWidget """ for widget in self._widgets: if widget.data().get("name") == name: return widget def options(self): options = [] for widget in self._widgets: options.append(widget.data()) return options def optionsToDict(self): """ This method is deprecated. :rtype: dict """ return self.values() def values(self): """ Get the all the field values indexed by the field name. :rtype: dict """ values = {} for widget in self._widgets: values[widget.data().get("name")] = widget.value() return values def defaultValues(self): """ Get the all the default field values indexed by the field name. :rtype: dict """ values = {} for widget in self._widgets: values[widget.data().get("name")] = widget.default() return values def state(self): """ Get the current state. :rtype: dict """ options = [] for widget in self._widgets: options.append(widget.state()) state = { "options": options, "expanded": self.isExpanded() } return state def setState(self, state): """ Set the current state. :type state: dict """ expanded = state.get("expanded") if expanded is not None: self.setExpanded(expanded) options = state.get("options") if options is not None: self._setState(options) self.validate() def optionsState(self): state = {} values = self.values() options = self.options() for option in options: name = option.get("name") persistent = option.get("persistent") if name and persistent: state[name] = values[name] return state def setStateFromOptions(self, options): state = [] for option in options: state.append({"name": option, "value": options[option]}) self._setState(state) def _setState(self, fields): """ Set the state. :type fields: list[dict] """ for widget in self._widgets: widget.blockSignals(True) for widget in self._widgets: widget.setError("") for field in fields: if field.get("name") == widget.data().get("name"): widget.setData(field) for widget in self._widgets: widget.blockSignals(False) self.stateChanged.emit()
class FieldWidget(QtWidgets.QFrame): """The base widget for all field widgets. Examples: data = { 'name': 'startFrame', 'type': 'int' 'value': 1, } fieldWidget = FieldWidget(data) """ valueChanged = QtCore.Signal() DefaultLayout = "horizontal" def __init__(self, parent=None, data=None): super(FieldWidget, self).__init__(parent) self._data = data or {} self._widget = None self._default = None self._required = None self._menuButton = None self._actionResult = None self.setObjectName("fieldWidget") direction = self._data.get("layout", self.DefaultLayout) if direction == "vertical": layout = QtWidgets.QVBoxLayout(self) else: layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) self.setLayout(layout) self.setContentsMargins(0, 0, 0, 0) self._label = QtWidgets.QLabel(self) self._label.setObjectName('label') self._label.setSizePolicy( QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred, ) layout.addWidget(self._label) self._layout2 = QtWidgets.QHBoxLayout(self) layout.addLayout(self._layout2) if direction == "vertical": self._label.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) else: self._label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) def label(self): """ Get the label widget. :rtype: QtWidgets.QLabel """ return self._label def state(self): """ Get the current state of the data. :rtype: dict """ return {"name": self._data["name"], "value": self.value()} def data(self): """ Get the data for the widget. :rtype: dict """ return self._data def title(self): """ Get the title to be displayed for the field. :rtype: str """ data = self.data() title = data.get("title", "") or data.get("name", "") if title: title = toTitle(title) if self.isRequired(): title += '*' return title def setData(self, data): """ Set the current state of the field widget using a dictionary. :type data: dict """ self._data.update(data) state = self._data self.blockSignals(True) items = state.get('items') if items is not None: self.setItems(items) value = state.get('value') default = state.get('default') # Must set the default before value if default is not None: self.setDefault(default) else: self.setDefault(value) if value is not None: try: self.setValue(value) except TypeError as error: logger.exception(error) enabled = state.get('enabled') if enabled is not None: self.setEnabled(enabled) hidden = state.get('hidden') if hidden is not None: self.setHidden(hidden) required = state.get('required') if required is not None: self.setRequired(required) message = state.get('error', '') self.setError(message) annotation = state.get('annotation', '') self.setToolTip(annotation) toolTip = state.get('toolTip', '') self.setToolTip(toolTip) style = state.get("style") if style: self.setStyleSheet(style) title = self.title() or "" self.setText(title) label = state.get('label', {}) if label: text = label.get("name") if text is not None: self.setText(text) visible = label.get('visible') if visible is not None: self.label().setVisible(visible) text = state.get("menu", {}).get("name") if text is not None: self.setMenuText(text) self.refresh() self.blockSignals(False) def setError(self, message): """ Set the error message to be displayed for the field widget. :type message: str """ error = True if message else False if error: self.setToolTip(message) else: self.setToolTip(self.data().get('annotation')) self.setProperty('error', error) self.setStyleSheet(self.styleSheet()) def setText(self, text): """ Set the label text for the field widget. :type text: str """ self._label.setText(text) def setValue(self, value): """ Set the value of the field widget. Will emit valueChanged() if the new value is different from the old one. :type value: object """ self.emitValueChanged() def value(self): """ Get the value of the field widget. :rtype: object """ raise NotImplementedError('The method "value" needs to be implemented') def setItems(self, items): """ Set the items for the field widget. :type items: list[str] """ raise NotImplementedError( 'The method "setItems" needs to be implemented') def reset(self): """Reset the field widget back to the defaults.""" self.setState(self._data) def setRequired(self, required): """ Set True if a value is required for this field. :type required: bool """ self._required = required self.setProperty('required', required) self.setStyleSheet(self.styleSheet()) def isRequired(self): """ Check if a value is required for the field widget. :rtype: bool """ return bool(self._required) def setDefault(self, default): """ Set the default value for the field widget. :type default: object """ self._default = default def default(self): """ Get the default value for the field widget. :rtype: object """ return self._default def isDefault(self): """ Check if the current value is the same as the default value. :rtype: bool """ return self.value() == self.default() def emitValueChanged(self, *args): """ Emit the value changed signal. :type args: list """ self.valueChanged.emit() self.refresh() def setWidget(self, widget): """ Set the widget used to set and get the field value. :type widget: QtWidgets.QWidget """ self._widget = widget self._widget.setParent(self) self._widget.setObjectName('widget') self._widget.setSizePolicy( QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred, ) self._layout2.addWidget(self._widget) self.createMenuButton() def setMenuText(self, text): self._menuButton.setText(text) def createMenuButton(self): """Create the menu button to show the actions.""" menu = self.data().get("menu", {}) actions = self.data().get("actions", {}) if menu or actions: name = menu.get("name", "...") callback = menu.get("callback", self.showMenu) self._menuButton = QtWidgets.QPushButton(name) self._menuButton.setObjectName("menuButton") self._menuButton.clicked.connect(callback) self._layout2.addWidget(self._menuButton) def actionCallback(self, callback): """ Wrap schema callback to get the return value. :type callback: func """ self._actionResult = callback() def showMenu(self): """Show the menu using the actions from the data.""" menu = QtWidgets.QMenu(self) actions = self.data().get("actions", []) for action in actions: name = action.get("name", "No name found") callback = action.get("callback") func = functools.partial(self.actionCallback, callback) action = menu.addAction(name) action.triggered.connect(func) point = QtGui.QCursor.pos() point.setX(point.x() + 3) point.setY(point.y() + 3) # Reset the action results self._actionResult = None menu.exec_(point) if self._actionResult is not None: self.setValue(self._actionResult) def widget(self, ): """ Get the widget used to set and get the field value. :rtype: QtWidgets.QWidget """ return self._widget def refresh(self): """Refresh the style properties.""" direction = self._data.get("layout", self.DefaultLayout) self.setProperty("layout", direction) self.setProperty('default', self.isDefault()) self.setStyleSheet(self.styleSheet())
class Library(QtCore.QObject): ColumnLabels = [ "icon", "name", "path", "type", "category", "folder", # "modified" ] SortLabels = [ "name", "path", "type", "category", "folder", # "modified" ] GroupLabels = [ "type", "category", # "modified", ] DatabasePath = "{path}/.studiolibrary/database.json" dataChanged = QtCore.Signal() def __init__(self, path, *args): QtCore.QObject.__init__(self, *args) self._path = None self._mtime = None self._data = {} self._items = [] self._currentItems = [] self.setPath(path) self.setDirty(True) def currentItems(self): """ The items that are displayed in the view. :rtype: list[studiolibrary.LibraryItem] """ return self._currentItems def recursiveDepth(self): """ Return the recursive search depth. :rtype: int """ return studiolibrary.config().get('recursiveSearchDepth') def path(self): """ Return the disc location of the db. :rtype: str """ return self._path def setPath(self, path): """ Set the disc location of the db. :type path: str """ self._path = path def databasePath(self): """ Return the path to the database. :rtype: str """ return studiolibrary.formatPath(self.DatabasePath, path=self.path()) def mtime(self): """ Return when the database was last modified. :rtype: float or None """ path = self.databasePath() mtime = None if os.path.exists(path): mtime = os.path.getmtime(path) return mtime def setDirty(self, value): """ Update the model object with the current database timestamp. :type: bool """ if value: self._mtime = None else: self._mtime = self.mtime() def isDirty(self): """ Return True if the database has changed on disc. :rtype: bool """ return not self._items or self._mtime != self.mtime() def read(self): """ Read the database from disc and return a dict object. :rtype: dict """ if self.isDirty(): self._data = studiolibrary.readJson(self.databasePath()) self.setDirty(False) return self._data def save(self, data): """ Write the given dict object to the database on disc. :type data: dict :rtype: None """ studiolibrary.saveJson(self.databasePath(), data) def sync(self): """ Sync the file system with the database. :rtype: None """ data = self.read() isDirty = False for path in data.keys(): if not os.path.exists(path): isDirty = True del data[path] depth = self.recursiveDepth() items = studiolibrary.findItems( self.path(), depth=depth, ) for item in items: path = item.path() if item.path() not in data: isDirty = True data[path] = {} if isDirty: self.save(data) self.dataChanged.emit() def findItems(self, queries, libraryWidget=None): """ Get the items that match the given queries. Examples: queries = [ { 'operator': 'or', 'filters': [ ('folder', 'is' '/library/proj/test'), ('folder', 'startswith', '/library/proj/test'), ] }, { 'operator': 'and', 'filters': [ ('path', 'contains' 'test'), ('path', 'contains', 'run'), ] } ] print(library.find(queries)) :type queries: list[dict] :type libraryWidget: studiolibrary.LibraryWIdget or None :rtype: list[studiolibrary.LibraryItem] """ items = self.createItems(libraryWidget=libraryWidget) self._currentItems = [] for item in items: matches = [] for query in queries: filters = query.get('filters') operator = query.get('operator', 'and') if not filters: continue match = False for key, cond, text in filters: text = text.lower() itemText = item.text(key).lower() if cond == 'contains': match = text in itemText elif cond == 'is': match = text == itemText elif cond == 'startswith': match = itemText.startswith(text) if operator == 'or' and match: break if operator == 'and' and not match: break matches.append(match) if all(matches): self._currentItems.append(item) return self._currentItems def addItem(self, item, data=None): """ Add the given item to the database. :type item: studiolibrary.LibraryItem :type data: dict or None :rtype: None """ self.addItems([item], data) def addItems(self, items, data=None): """ Add the given items to the database. :type items: list[studiolibrary.LibraryItem] :type data: dict or None :rtype: None """ logger.info("Add items %s", items) paths = [item.path() for item in items] isDirty = self.isDirty() self.addPaths(paths, data) self.setDirty(isDirty) self._items.extend(items) self.dataChanged.emit() def createItems(self, libraryWidget=None): """ Create all the items for the model. :rtype: list[studiolibrary.LibraryItem] """ # Check if the database has changed since the last read call if self.isDirty(): paths = self.read().keys() items = studiolibrary.itemsFromPaths(paths, library=self, libraryWidget=libraryWidget) self._items = list(items) self.loadItemData(self._items) return self._items def saveItemData(self, items, columns=None): """ Save the item data to the database for the given items and columns. :type columns: list[str] :type items: list[studiolibrary.LibraryItem] """ data = {} columns = columns or ["Custom Order"] for item in items: path = item.path() data.setdefault(path, {}) for column in columns: value = item.text(column) data[path].setdefault(column, value) studiolibrary.updateJson(self.databasePath(), data) def loadItemData(self, items): """ Load the item data from the database to the given items. :type items: list[studiolibrary.LibraryItem] :rtype: None """ data = self.read() for item in items: key = item.id() if key in data: item.setItemData(data[key]) def addPaths(self, paths, data=None): """ Add the given path and the given data to the database. :type paths: list[str] :type data: dict or None :rtype: None """ data = data or {} self.updatePaths(paths, data) def updatePaths(self, paths, data): """ Update the given paths with the given data in the database. :type paths: list[str] :type data: dict :rtype: None """ data_ = self.read() paths = studiolibrary.normPaths(paths) for path in paths: if path in data_: data_[path].update(data) else: data_[path] = data self.save(data_) def copyPath(self, src, dst): """ Copy the given source path to the given destination path. :type src: str :type dst: str :rtype: str """ self.addPaths([dst]) return dst def renamePath(self, src, dst): """ Rename the source path to the given name. :type src: str :type dst: str :rtype: str """ studiolibrary.renamePathInFile(self.databasePath(), src, dst) return dst def removePath(self, path): """ Remove the given path from the database. :type path: str :rtype: None """ self.removePaths([path]) def removePaths(self, paths): """ Remove the given paths from the database. :type paths: list[str] :rtype: None """ data = self.read() paths = studiolibrary.normPaths(paths) for path in paths: if path in data: del data[path] self.save(data)
class SearchWidget(QtWidgets.QLineEdit): DEFAULT_PLACEHOLDER_TEXT = "Search" searchChanged = QtCore.Signal() def __init__(self, *args): QtWidgets.QLineEdit.__init__(self, *args) self._iconPadding = 6 self._iconButton = QtWidgets.QPushButton(self) self._iconButton.clicked.connect(self._iconClicked) self._searchFilter = studioqt.SearchFilter("") icon = studioqt.icon("search") self.setIcon(icon) self._clearButton = QtWidgets.QPushButton(self) self._clearButton.setCursor(QtCore.Qt.ArrowCursor) icon = studioqt.icon("cross") self._clearButton.setIcon(icon) self._clearButton.setToolTip("Clear all search text") self._clearButton.clicked.connect(self._clearClicked) self.setPlaceholderText(self.DEFAULT_PLACEHOLDER_TEXT) self.textChanged.connect(self._textChanged) self.searchChanged = self.searchFilter().searchChanged self.update() def update(self): self.updateIconColor() self.updateClearButton() def updateIconColor(self): """ Update the color of the icons from the current palette. :rtype: None """ color = self.palette().color(self.foregroundRole()) color = studioqt.Color.fromColor(color) self.setIconColor(color) def _clearClicked(self): """ Triggered when the user clicks the cross icon. :rtype: None """ self.setText("") self.setFocus() def _iconClicked(self): """ Triggered when the user clicks on the icon. :rtype: None """ if not self.hasFocus(): self.setFocus() def _textChanged(self, text): """ Triggered when the text changes. :type text: str :rtype: None """ self.searchFilter().setPattern(text) self.updateClearButton() def updateClearButton(self): """ Update the clear button depending on the current text. :rtype: None """ text = self.text() if text: self._clearButton.show() else: self._clearButton.hide() def contextMenuEvent(self, event): """ Triggered when the user right clicks on the search widget. :type event: QtCore.QEvent :rtype: None """ self.showContextMenu() def setSpaceOperator(self, operator): """ Set the space operator for the search filter. :type operator: studioqt.SearchFilter.Operator :rtype: None """ self._searchFilter.setSpaceOperator(operator) def createSpaceOperatorMenu(self, parent=None): """ Return the menu for changing the space operator. :type parent: QGui.QMenu :rtype: QGui.QMenu """ searchFilter = self.searchFilter() menu = QtWidgets.QMenu(parent) menu.setTitle("Space Operator") # Create the space operator for the OR operator action = QtWidgets.QAction(menu) action.setText("OR") action.setCheckable(True) callback = partial(self.setSpaceOperator, searchFilter.Operator.OR) action.triggered.connect(callback) if searchFilter.spaceOperator() == searchFilter.Operator.OR: action.setChecked(True) menu.addAction(action) # Create the space operator for the AND operator action = QtWidgets.QAction(menu) action.setText("AND") action.setCheckable(True) callback = partial(self.setSpaceOperator, searchFilter.Operator.AND) action.triggered.connect(callback) if searchFilter.spaceOperator() == searchFilter.Operator.AND: action.setChecked(True) menu.addAction(action) return menu def showContextMenu(self): """ Create and show the context menu for the search widget. :rtype QtWidgets.QAction """ menu = QtWidgets.QMenu(self) subMenu = self.createStandardContextMenu() subMenu.setTitle("Edit") menu.addMenu(subMenu) subMenu = self.createSpaceOperatorMenu(menu) menu.addMenu(subMenu) point = QtGui.QCursor.pos() action = menu.exec_(point) return action def searchFilter(self): """ Return the search filter for the widget. :rtype: studioqt.SearchFilter """ return self._searchFilter def setIcon(self, icon): """ Set the icon for the search widget. :type icon: QtWidgets.QIcon :rtype: None """ self._iconButton.setIcon(icon) def setIconColor(self, color): """ Set the icon color for the search widget icon. :type color: QtGui.QColor :rtype: None """ icon = self._iconButton.icon() icon = studioqt.Icon(icon) icon.setColor(color) self._iconButton.setIcon(icon) icon = self._clearButton.icon() icon = studioqt.Icon(icon) icon.setColor(color) self._clearButton.setIcon(icon) def settings(self): """ Return a dictionary of the current widget state. :rtype: dict """ settings = {"text": self.text()} settings["searchFilter"] = self.searchFilter().settings() return settings def setSettings(self, settings): """ Restore the widget state from a settings dictionary. :type settings: dict :rtype: None """ searchFilterSettings = settings.get("searchFilter", None) if searchFilterSettings is not None: self.searchFilter().setSettings(searchFilterSettings) text = settings.get("text", "") self.setText(text) def resizeEvent(self, event): """ Reimplemented so the icon maintains the same height as the widget. :type event: QtWidgets.QResizeEvent :rtype: None """ QtWidgets.QLineEdit.resizeEvent(self, event) self.setTextMargins(self.height(), 0, 0, 0) size = QtCore.QSize(self.height(), self.height()) self._iconButton.setIconSize(size) self._iconButton.setFixedSize(size) self._clearButton.setIconSize(size) x = self.width() - self.height() self._clearButton.setGeometry(x, 0, self.height(), self.height())
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()
class ImageSequence(QtCore.QObject): DEFAULT_FPS = 24 frameChanged = QtCore.Signal() def __init__(self, *args): QtCore.QObject.__init__(self, *args) self._fps = self.DEFAULT_FPS self._timer = None self._frame = 0 self._frames = [] self._dirname = None self._paused = False def setDirname(self, dirname): """ Set the location to the image sequence. :type dirname: str :rtype: None """ def naturalSortItems(items): """ Sort the given list in the way that humans expect. """ convert = lambda text: (int(text) if text.isdigit() else text) alphanum_key = lambda key: [ convert(c) for c in re.split('([0-9]+)', key) ] items.sort(key=alphanum_key) self._dirname = dirname if os.path.isdir(dirname): self._frames = [ dirname + '/' + filename for filename in os.listdir(dirname) ] naturalSortItems(self._frames) def dirname(self): """ Return the location to the image sequence. :rtype: str """ return self._dirname def reset(self): """ Stop and reset the current frame to 0. :rtype: None """ if not self._timer: self._timer = QtCore.QTimer(self.parent()) self._timer.setSingleShot(False) self.connect(self._timer, QtCore.SIGNAL('timeout()'), self._frameChanged) if not self._paused: self._frame = 0 self._timer.stop() def pause(self): """ ImageSequence will enter Paused state. :rtype: None """ self._paused = True self._timer.stop() def resume(self): """ ImageSequence will enter Playing state. :rtype: None """ if self._paused: self._paused = False self._timer.start() def stop(self): """ Stops the movie. ImageSequence enters NotRunning state. :rtype: None """ self._frame = 0 self._timer.stop() def start(self): """ Starts the movie. ImageSequence will enter Running state :rtype: None """ self.reset() if self._timer: self._timer.start(1000.0 / self._fps) def frames(self): """ Return all the filenames in the image sequence. :rtype: list[str] """ return self._frames def _frameChanged(self): """ Triggered when the current frame changes. :rtype: None """ if not self._frames: return frame = self._frame frame += 1 self.setCurrentFrame(frame) def percent(self): """ Return the current frame position as a percentage. :rtype: None """ if len(self._frames) == self._frame + 1: _percent = 1 else: _percent = float(len(self._frames) + self._frame) / len( self._frames) - 1 return _percent def duration(self): """ Return the number of frames. :rtype: int """ return len(self._frames) def currentFilename(self): """ Return the current file name. :rtype: str or None """ try: return self._frames[self.currentFrame()] except IndexError: pass def currentFrame(self): """ Return the current frame. :rtype: int or None """ return self._frame def setCurrentFrame(self, frame): """ Set the current frame. :rtype: int or None """ if frame >= self.duration(): frame = 0 self._frame = frame self.frameChanged.emit()
class Library(QtCore.QObject): Fields = [ "icon", "name", "path", "type", "folder", "category", # "modified" ] SortFields = [ "name", "path", "type", "folder", "category", "Custom Order", # legacy case # "modified" ] GroupFields = [ "type", "category", # "modified", ] dataChanged = QtCore.Signal() searchStarted = QtCore.Signal() searchFinished = QtCore.Signal() searchTimeFinished = QtCore.Signal() def __init__(self, path=None, libraryWindow=None, *args): QtCore.QObject.__init__(self, *args) self._path = path self._mtime = None self._data = {} self._items = [] self._fields = [] self._sortBy = [] self._groupBy = [] self._results = [] self._queries = {} self._globalQueries = {} self._groupedResults = {} self._searchTime = 0 self._searchEnabled = True self._libraryWindow = libraryWindow self.setPath(path) self.setDirty(True) def sortBy(self): """ Get the list of fields to sort by. :rtype: list[str] """ return self._sortBy def setSortBy(self, fields): """ Set the list of fields to group by. Example: library.setSortBy(["name:asc", "type:asc"]) :type fields: list[str] """ self._sortBy = fields def groupBy(self): """ Get the list of fields to group by. :rtype: list[str] """ return self._groupBy def setGroupBy(self, fields): """ Set the list of fields to group by. Example: library.setGroupBy(["name:asc", "type:asc"]) :type fields: list[str] """ self._groupBy = fields def settings(self): """ Get the settings for the dataset. :rtype: dict """ return {"sortBy": self.sortBy(), "groupBy": self.groupBy()} def setSettings(self, settings): """ Set the settings for the dataset object. :type settings: dict """ value = settings.get('sortBy') if value is not None: self.setSortBy(value) value = settings.get('groupBy') if value is not None: self.setGroupBy(value) def setSearchEnabled(self, enabled): """Enable or disable the search the for the library.""" self._searchEnabled = enabled def isSearchEnabled(self): """Check if search is enabled for the library.""" return self._searchEnabled def recursiveDepth(self): """ Return the recursive search depth. :rtype: int """ return studiolibrary.config().get('recursiveSearchDepth') def fields(self): """Return all the fields for the library.""" return self._fields def path(self): """ Return the disc location of the db. :rtype: str """ return self._path def setPath(self, path): """ Set the disc location of the db. :type path: str """ self._path = path def databasePath(self): """ Return the path to the database. :rtype: str """ formatString = studiolibrary.config().get('databasePath') return studiolibrary.formatPath(formatString, path=self.path()) def distinct(self, field, queries=None, sortBy="name"): """ Get all the values for the given field. :type field: str :type queries None or list[dict] :type sortBy: str :rtype: list """ results = {} queries = queries or [] queries.extend(self._globalQueries.values()) items = self.createItems() for item in items: value = item.itemData().get(field) if value: results.setdefault(value, {'count': 0, 'name': value}) match = self.match(item.itemData(), queries) if match: results[value]['count'] += 1 def sortKey(facet): return facet.get(sortBy) return sorted(results.values(), key=sortKey) def mtime(self): """ Return when the database was last modified. :rtype: float or None """ path = self.databasePath() mtime = None if os.path.exists(path): mtime = os.path.getmtime(path) return mtime def setDirty(self, value): """ Update the model object with the current database timestamp. :type: bool """ if value: self._mtime = None else: self._mtime = self.mtime() def isDirty(self): """ Return True if the database has changed on disc. :rtype: bool """ return not self._items or self._mtime != self.mtime() def read(self): """ Read the database from disc and return a dict object. :rtype: dict """ if self.path(): if self.isDirty(): self._data = studiolibrary.readJson(self.databasePath()) self.setDirty(False) else: logger.info('No path set for reading the data from disc.') return self._data def save(self, data): """ Write the given dict object to the database on disc. :type data: dict :rtype: None """ if self.path(): studiolibrary.saveJson(self.databasePath(), data) self.setDirty(True) else: logger.info('No path set for saving the data to disc.') def clear(self): """Clear all the item data.""" self._items = [] self._results = [] self._groupedResults = {} self.dataChanged.emit() def sync(self, progressCallback=None): """Sync the file system with the database.""" if not self.path(): logger.info('No path set for syncing data') return if progressCallback: progressCallback("Syncing") data = self.read() for path in data.keys(): if not os.path.exists(path): del data[path] depth = self.recursiveDepth() items = studiolibrary.findItems( self.path(), depth=depth, ) items = list(items) count = len(items) for i, item in enumerate(items): percent = (float(i + 1) / float(count)) if progressCallback: percent *= 100 label = "{0:.0f}%".format(percent) progressCallback(label, percent) path = item.path() itemData = data.get(path, {}) itemData.update(item.createItemData()) data[path] = itemData if progressCallback: progressCallback("Post Callbacks") self.postSync(data) if progressCallback: progressCallback("Saving Cache") self.save(data) self.dataChanged.emit() def postSync(self, data): """ Use this function to execute code on the data after sync, but before save and dataChanged.emit :type data: dict :rtype: None """ pass def createItems(self): """ Create all the items for the model. :rtype: list[studiolibrary.LibraryItem] """ # Check if the database has changed since the last read call if self.isDirty(): paths = self.read().keys() items = studiolibrary.itemsFromPaths( paths, library=self, libraryWindow=self._libraryWindow) self._items = list(items) self.loadItemData(self._items) return self._items def findItems(self, queries): """ Get the items that match the given queries. Examples: queries = [ { 'operator': 'or', 'filters': [ ('folder', 'is' '/library/proj/test'), ('folder', 'startswith', '/library/proj/test'), ] }, { 'operator': 'and', 'filters': [ ('path', 'contains' 'test'), ('path', 'contains', 'run'), ] } ] print(library.find(queries)) :type queries: list[dict] :rtype: list[studiolibrary.LibraryItem] """ fields = [] results = [] queries = copy.copy(queries) queries.extend(self._globalQueries.values()) logger.debug("Search queries:") for query in queries: logger.debug('Query: %s', query) items = self.createItems() for item in items: match = self.match(item.itemData(), queries) if match: results.append(item) fields.extend(item.itemData().keys()) self._fields = list(set(fields)) if self.sortBy(): results = self.sorted(results, self.sortBy()) return results def queries(self, exclude=None): """ Get all the queries for the dataset excluding the given ones. :type exclude: list[str] or None :rtype: list[dict] """ queries = [] exclude = exclude or [] for query in self._queries.values(): if query.get('name') not in exclude: queries.append(query) return queries def addGlobalQuery(self, query): """ Add a global query to library. :type query: dict """ self._globalQueries[query["name"]] = query def addQuery(self, query): """ Add a search query to the library. Examples: addQuery({ 'name': 'My Query', 'operator': 'or', 'filters': [ ('folder', 'is' '/library/proj/test'), ('folder', 'startswith', '/library/proj/test'), ] }) :type query: dict """ self._queries[query["name"]] = query def removeQuery(self, name): """ Remove the query with the given name. :type name: str """ if name in self._queries: del self._queries[name] def queryExists(self, name): """ Check if the given query name exists. :type name: str :rtype: bool """ return name in self._queries def search(self): """Run a search using the queries added to this dataset.""" if not self.isSearchEnabled(): logger.debug('Search is disabled') return t = time.time() logger.debug("Searching items") self.searchStarted.emit() self._results = self.findItems(self.queries()) self._groupedResults = self.groupItems(self._results, self.groupBy()) self.searchFinished.emit() self._searchTime = time.time() - t self.searchTimeFinished.emit() logger.debug('Search time: %s', self._searchTime) def results(self): """ Return the items found after a search is ran. :rtype: list[Item] """ return self._results def groupedResults(self): """ Get the results grouped after a search is ran. :rtype: dict """ return self._groupedResults def searchTime(self): """ Return the time taken to run a search. :rtype: float """ return self._searchTime def addItem(self, item): """ Add the given item to the database. :type item: studiolibrary.LibraryItem :rtype: None """ self.saveItemData([item]) def addItems(self, items): """ Add the given items to the database. :type items: list[studiolibrary.LibraryItem] """ self.saveItemData(items) def updateItem(self, item): """ Update the given item in the database. :type item: studiolibrary.LibraryItem :rtype: None """ self.saveItemData([item]) def saveItemData(self, items, emitDataChanged=True): """ Add the given items to the database. :type items: list[studiolibrary.LibraryItem] :type emitDataChanged: bool """ logger.debug("Save item data %s", items) data_ = self.read() for item in items: path = item.path() data = item.itemData() data_.setdefault(path, {}) data_[path].update(data) self.save(data_) if emitDataChanged: self.search() self.dataChanged.emit() def loadItemData(self, items): """ Load the item data from the database to the given items. :type items: list[studiolibrary.LibraryItem] """ logger.debug("Loading item data %s", items) data = self.read() for item in items: key = item.id() if key in data: item.setItemData(data[key]) def addPaths(self, paths, data=None): """ Add the given path and the given data to the database. :type paths: list[str] :type data: dict or None :rtype: None """ data = data or {} self.updatePaths(paths, data) def updatePaths(self, paths, data): """ Update the given paths with the given data in the database. :type paths: list[str] :type data: dict :rtype: None """ data_ = self.read() paths = studiolibrary.normPaths(paths) for path in paths: if path in data_: data_[path].update(data) else: data_[path] = data self.save(data_) def copyPath(self, src, dst): """ Copy the given source path to the given destination path. :type src: str :type dst: str :rtype: str """ self.addPaths([dst]) return dst def renamePath(self, src, dst): """ Rename the source path to the given name. :type src: str :type dst: str :rtype: str """ studiolibrary.renamePathInFile(self.databasePath(), src, dst) self.setDirty(True) return dst def removePath(self, path): """ Remove the given path from the database. :type path: str :rtype: None """ self.removePaths([path]) def removePaths(self, paths): """ Remove the given paths from the database. :type paths: list[str] :rtype: None """ data = self.read() paths = studiolibrary.normPaths(paths) for path in paths: if path in data: del data[path] self.save(data) @staticmethod def match(data, queries): """ Match the given data with the given queries. Examples: queries = [ { 'operator': 'or', 'filters': [ ('folder', 'is' '/library/proj/test'), ('folder', 'startswith', '/library/proj/test'), ] }, { 'operator': 'and', 'filters': [ ('path', 'contains' 'test'), ('path', 'contains', 'run'), ] } ] print(library.find(queries)) """ matches = [] for query in queries: filters = query.get('filters') operator = query.get('operator', 'and') if not filters: continue match = False for key, cond, value in filters: if key == '*': itemValue = unicode(data) else: itemValue = data.get(key) if isinstance(value, basestring): value = value.lower() if isinstance(itemValue, basestring): itemValue = itemValue.lower() if not itemValue: match = False elif cond == 'contains': match = value in itemValue elif cond == 'not_contains': match = value not in itemValue elif cond == 'is': match = value == itemValue elif cond == 'not': match = value != itemValue elif cond == 'startswith': match = itemValue.startswith(value) if operator == 'or' and match: break if operator == 'and' and not match: break matches.append(match) return all(matches) @staticmethod def sorted(items, sortBy): """ Return the given data sorted using the sortBy argument. Example: data = [ {'name':'red', 'index':1}, {'name':'green', 'index':2}, {'name':'blue', 'index':3}, ] sortBy = ['index:asc', 'name'] # sortBy = ['index:dsc', 'name'] print(sortedData(data, sortBy)) :type items: list[Item] :type sortBy: list[str] :rtype: list[Item] """ logger.debug('Sort by: %s', sortBy) t = time.time() for field in reversed(sortBy): tokens = field.split(':') reverse = False if len(tokens) > 1: field = tokens[0] reverse = tokens[1] != 'asc' def sortKey(item): default = False if reverse else '' return item.itemData().get(field, default) items = sorted(items, key=sortKey, reverse=reverse) logger.debug("Sort items took %s", time.time() - t) return items @staticmethod def groupItems(items, fields): """ Group the given items by the given field. :type items: list[Item] :type fields: list[str] :rtype: dict """ logger.debug('Group by: %s', fields) # Only support for top level grouping at the moment. if fields: field = fields[0] else: return {'None': items} t = time.time() results_ = {} tokens = field.split(':') reverse = False if len(tokens) > 1: field = tokens[0] reverse = tokens[1] != 'asc' for item in items: value = item.itemData().get(field) if value: results_.setdefault(value, []) results_[value].append(item) groups = sorted(results_.keys(), reverse=reverse) results = collections.OrderedDict() for group in groups: results[group] = results_[group] logger.debug("Group Items Took %s", time.time() - t) return results
class FoldersWidget(QtWidgets.QTreeView): itemDropped = QtCore.Signal(object) itemClicked = QtCore.Signal() itemSelectionChanged = QtCore.Signal() def __init__(self, parent=None): """ :type parent: QtWidgets.QWidget """ QtWidgets.QTreeView.__init__(self, parent) self._filter = [] self._folders = {} self._isLocked = False self._signalsEnabled = True self._enableFolderSettings = False self._sourceModel = FileSystemModel(self) self.setDpi(1) proxyModel = SortFilterProxyModel(self) proxyModel.setSourceModel(self._sourceModel) proxyModel.sort(0) self.setAcceptDrops(True) self.setModel(proxyModel) self.setHeaderHidden(True) self.setFrameShape(QtWidgets.QFrame.NoFrame) self.setSelectionMode(QtWidgets.QTreeWidget.ExtendedSelection) self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) signal = "selectionChanged(const QItemSelection&,const QItemSelection&)" self.connect( self.selectionModel(), QtCore.SIGNAL(signal), self._selectionChanged, ) def enableFolderSettings(self, value): self._enableFolderSettings = value def folders(self): return self._folders.values() def createEditMenu(self, parent=None): """ Return the edit menu for deleting, renaming folders. :rtype: QtWidgets.QMenu """ selectedFolders = self.selectedFolders() menu = QtWidgets.QMenu(parent) menu.setTitle("Edit") if len(selectedFolders) == 1: action = QtWidgets.QAction("Rename", menu) action.triggered.connect(self.showRenameDialog) menu.addAction(action) action = QtWidgets.QAction("Show in folder", menu) action.triggered.connect(self.openSelectedFolders) menu.addAction(action) separator = QtWidgets.QAction("Separator2", menu) separator.setSeparator(True) menu.addAction(separator) action = QtWidgets.QAction("Show icon", menu) action.setCheckable(True) action.setChecked(self.isFolderIconVisible()) action.triggered[bool].connect(self.setFolderIconVisible) menu.addAction(action) action = QtWidgets.QAction("Show bold", menu) action.setCheckable(True) action.setChecked(self.isFolderBold()) action.triggered[bool].connect(self.setFolderBold) menu.addAction(action) separator = QtWidgets.QAction("Separator2", menu) separator.setSeparator(True) menu.addAction(separator) action = QtWidgets.QAction("Change icon", menu) action.triggered.connect(self.browseFolderIcon) menu.addAction(action) action = QtWidgets.QAction("Change color", menu) action.triggered.connect(self.browseFolderColor) menu.addAction(action) separator = QtWidgets.QAction("Separator3", menu) separator.setSeparator(True) menu.addAction(separator) action = QtWidgets.QAction("Reset settings", menu) action.triggered.connect(self.resetFolderSettings) menu.addAction(action) return menu def setDpi(self, dpi): size = 24 * dpi self.setIndentation(15 * dpi) self.setMinimumWidth(35 * dpi) self.setIconSize(QtCore.QSize(size, size)) self.setStyleSheet("height: {size}".format(size=size)) def openSelectedFolders(self): folders = self.selectedFolders() for folder in folders: folder.openLocation() def reload(self): """ Force the root path and state to be reloaded. :rtype: None """ path = self.rootPath() settings = self.settings() ignoreFilter = self.ignoreFilter() self.setRootPath("") self.setRootPath(path) self.setSettings(settings) self.setIgnoreFilter(ignoreFilter) def setLocked(self, value): """ :rtype: bool """ self._isLocked = value def isLocked(self): """ :rtype: bool """ return self._isLocked def folderFromIndex(self, index): """ :type index: QtCore.QModelIndex :rtype: Folder """ path = self.pathFromIndex(index) return self.folderFromPath(path) def folderFromPath(self, path): """ :type path: str :rtype: Folder """ folders = self._folders if path not in folders: folders[path] = folderitem.FolderItem(path, self) return folders[path] def indexFromFolder(self, folder): """ :type path: FolderItem :rtype: QtCore.QModelIndex """ return self.indexFromPath(folder.path()) def indexFromPath(self, path): """ :type path: str :rtype: QtCore.QModelIndex """ index = self.model().sourceModel().index(path) return self.model().mapFromSource(index) def pathFromIndex(self, index): """ :type index: QtCore.QModelIndex :rtype: str """ index = self.model().mapToSource(index) return self.model().sourceModel().filePath(index) def _selectionChanged(self, selected, deselected): """ :type selected: list[Folder] :type deselected: list[Folder] :rtype: None """ if self._signalsEnabled: self.itemSelectionChanged.emit() def saveSettings(self, path): data = self.settings() studioqt.saveJson(path, data) def loadSettings(self, path): if os.path.exists(path): data = studioqt.readJson(path) self.setSettings(data) def folderSettings(self): settings = {} for folder in self.folders(): if folder.settings() and folder.exists(): settings[folder.path()] = folder.settings() return settings def setFolderSettings(self, settings): for path in settings: folder = self.folderFromPath(path) folder.setSettings(settings[path]) def settings(self): """ :rtype: dict """ settings = { "selectedPaths": self.selectedPaths(), # Saving the state of expanded folders is not supported yet! # "expandedPaths": self.expandedPaths(), # "folderSettings": self.folderSettings(), } if self._enableFolderSettings: settings["folderSettings"] = self.folderSettings() return settings def setSettings(self, settings): """ :rtype state: list """ # Saving the state of expanded folders is not supported yet! # expandedPaths = settings.get("expandedPaths", []) # self.expandPaths(expandedPaths) selectedPaths = settings.get("selectedPaths", []) self.selectPaths(selectedPaths) if self._enableFolderSettings: folderSettings = settings.get("folderSettings", {}) self.setFolderSettings(folderSettings) def setFolderOrderIndex(self, path, orderIndex): """ :type path: :type: position: :rtype: None """ folder = self.folderFromPath(path) folder.setOrderIndex(orderIndex) def setIgnoreFilter(self, ignoreFilter): """ :type ignoreFilter: list[str] """ self.model().sourceModel().setIgnoreFilter(ignoreFilter) def ignoreFilter(self): return self.model().sourceModel().ignoreFilter() def setRootPath(self, path): """ :type path: str """ self.model().sourceModel().setRootPath(path) index = self.indexFromPath(path) self.setRootIndex(index) def rootPath(self): """ :rtype: str """ return self.model().sourceModel().rootPath() def selectFolders(self, folders): """ :type folders: list[Folder] :rtype: None """ paths = [folder.path() for folder in folders] self.selectPaths(paths) def selectPaths(self, paths): """ :type paths: list[str] :rtype: None """ if not paths: return self._signalsEnabled = False for path in paths[:-1]: self.selectPath(path) self._signalsEnabled = True self.selectPath(paths[-1]) def selectFolder(self, folder, mode=QtCore.QItemSelectionModel.Select): """ :type folder: Folder :rtype: None """ self.selectPath(folder.path(), mode=mode) def expandedPaths(self): """ Return the expanded folder paths. :rtype: list[str] """ return [folder.path() for folder in self.expandedFolders()] def expandedFolders(self): """ Return the expanded folder paths. :rtype: list[studioqt.FolderItem] """ folders = [] for folder in self.folders(): index = self.indexFromPath(folder.path()) if self.isExpanded(index): folders.append(folder) return folders def expandPaths(self, paths): self._signalsEnabled = False for path in paths: self.expandParentsFromPath(path) self._signalsEnabled = True def expandParentsFromPath(self, path): """ :type path: str :rtype: None """ for i in range(0, 4): path = os.path.dirname(path) index = self.indexFromPath(path) if index and not self.isExpanded(index): self.setExpanded(index, True) def selectPath(self, path, mode=QtCore.QItemSelectionModel.Select): """ Select the given folders. :type path: str :rtype: Nones """ isSelected = path in self.selectedPaths() if not isSelected: self.expandParentsFromPath(path) index = self.indexFromPath(path) self.selectionModel().select(index, mode) def showCreateDialog(self, parent=None): """ :rtype: None """ name, accepted = QtWidgets.QInputDialog.getText( parent, "Create Folder", "Folder name", QtWidgets.QLineEdit.Normal) name = name.strip() if accepted and name: folders = self.selectedFolders() if len(folders) == 1: folder = folders[-1] path = folder.path() + "/" + name else: path = self.rootPath() + "/" + name if not os.path.exists(path): os.makedirs(path) folder = self.folderFromPath(path) self.reload() self.clearSelection() self.selectFolder(folder) def showRenameDialog(self, parent=None): """ :rtype: None """ parent = parent or self.parent() folder = self.selectedFolder() if folder: name, accept = QtWidgets.QInputDialog.getText( parent, "Rename Folder", "New Name", QtWidgets.QLineEdit.Normal, folder.name()) if accept: self.renameFolder(folder, str(name)) def renameFolder(self, folder, name): """ :type folder: Folder :type name: str """ oldPath = folder.path() folder.rename(str(name)) newPath = folder.path() del self._folders[oldPath] self._folders[newPath] = folder self.reload() self.selectPath(newPath) def selectedFolder(self): """ :rtype: None | Folder """ folders = self.selectedFolders() if folders: return folders[-1] return None def selectedPaths(self): return [folder.path() for folder in self.selectedFolders()] def selectedFolders(self): """ :rtype: list[Folder] """ folders = [] for index in self.selectionModel().selectedIndexes(): path = self.pathFromIndex(index) folder = self.folderFromPath(path) folders.append(folder) return folders def dragEnterEvent(self, event): """ :type event: QtCore.QEvent :rtype: None """ event.accept() def clearSelection(self): """ :rtype: None """ self.selectionModel().clearSelection() def dragMoveEvent(self, event): """ :type event: QtCore.QEvent :rtype: None """ mimeData = event.mimeData() if mimeData.hasUrls() and not self.isLocked(): event.accept() else: event.ignore() folder = self.folderAt(event.pos()) if folder: self.selectFolder(folder, QtCore.QItemSelectionModel.ClearAndSelect) def dropEvent(self, event): """ :type event: QtCore.QEvent :rtype: None """ if self.isLocked(): logger.debug("Folder is locked! Cannot accept drop!") return self.itemDropped.emit(event) def mouseMoveEvent(self, event): if studioqt.isControlModifier(): return folder = self.folderAt(event.pos()) selectedFolders = self.selectedFolders() isSelected = folder in selectedFolders if folder: self.clearSelection() self.selectFolder(folder) def mousePressEvent(self, event): """ :type event: QtCore.QEvent :rtype: None """ folder = self.folderAt(event.pos()) selectedFolders = self.selectedFolders() isSelected = folder in selectedFolders if event.button() == QtCore.Qt.RightButton: QtWidgets.QTreeView.mousePressEvent(self, event) else: QtWidgets.QTreeView.mousePressEvent(self, event) if not folder: self.clearSelection() elif event.button( ) == QtCore.Qt.LeftButton and studioqt.isControlModifier(): if folder and isSelected: self.clearSelection() selectedFolders.remove(folder) self.selectFolders(selectedFolders) if folder and not isSelected: selectedFolders.append(folder) self.selectFolders(selectedFolders) self.repaint() def mouseReleaseEvent(self, event): """ :type event: QtCore.QEvent :rtype: None """ if event.button() == QtCore.Qt.MidButton: event.ignore() else: QtWidgets.QTreeView.mouseReleaseEvent(self, event) def showContextMenu(self): """ :rtype: None """ menu = QtWidgets.QMenu(self) if self.isLocked(): self.lockedMenu(menu) else: self.createContextMenu(menu) action = menu.exec_(QtGui.QCursor.pos()) menu.close() return action def lockedMenu(self, menu): """ :type menu: QtWidgets.QMenu :rtype: None """ action = QtWidgets.QAction("Locked", menu) action.setEnabled(False) menu.addAction(action) def createContextMenu(self, menu): """ :type menu: QtWidgets.QMenu :rtype: None """ folders = self.selectedFolders() if not folders: action = menu.addAction("No folder selected") action.setEnabled(False) return editMenu = self.createEditMenu(menu) menu.addMenu(editMenu) return menu def folderAt(self, pos): """ :type pos: QtGui.QPoint :rtype: None or Folder """ index = self.indexAt(pos) if not index.isValid(): return path = self.pathFromIndex(index) folder = self.folderFromPath(path) return folder def setFolderIconVisible(self, value): """ :type value: Bool """ for folder in self.selectedFolders(): folder.setIconVisible(value) def isFolderIconVisible(self): """ :rtype: bool """ for folder in self.selectedFolders(): if not folder.isIconVisible(): return False return True def setFolderBold(self, value): """ :type value: Bool """ for folder in self.selectedFolders(): folder.setBold(value) def isFolderBold(self): """ :rtype: bool """ for folder in self.selectedFolders(): if not folder.isBold(): return False return True def setFolderColor(self, color): """ :type color: :return: """ for folder in self.selectedFolders(): folder.setColor(color) def resetFolderSettings(self): """ :rtype: """ for folder in self.selectedFolders(): folder.reset() def browseFolderIcon(self): """ :rtype: None """ path, ext = QtWidgets.QFileDialog.getOpenFileName( self.parent(), "Select an image", "", "*.png") path = str(path).replace("\\", "/") if path: for folder in self.selectedFolders(): folder.setIconPath(path) def browseFolderColor(self): """ :rtype: None """ dialog = QtWidgets.QColorDialog(self.parent()) dialog.currentColorChanged.connect(self.setFolderColor) # PySide2 doesn't support d.open(), so we need to pass a blank slot. dialog.open(self, QtCore.SLOT("blankSlot()")) if dialog.exec_(): self.setFolderColor(dialog.selectedColor()) @QtCore.Slot() def blankSlot(self): """ Blank slot to fix an issue with PySide2.QColorDialog.open() """ pass
class ColorPickerWidget(QtWidgets.QFrame): COLOR_BUTTON_CLASS = ColorButton colorChanged = QtCore.Signal(object) def __init__(self, *args): QtWidgets.QFrame.__init__(self, *args) self._buttons = [] self._currentColor = None self._browserColors = None layout = QtWidgets.QHBoxLayout() layout.setSpacing(0) layout.setContentsMargins(0, 0, 0, 0) self.setLayout(layout) def _colorChanged(self, color): """ Triggered when the user clicks or browses for a color. :type color: studioqt.Color :rtype: None """ self._currentColor = color self.colorChanged.emit(color) def deleteButtons(self): """ Delete all the color buttons. :rtype: None """ layout = self.layout() while layout.count(): item = layout.takeAt(0) item.widget().deleteLater() def currentColor(self): """ Return the current color. :rtype: studioqt.Color """ return self._currentColor def setCurrentColor(self, color): """ Set the current color. :type color: studioqt.Color """ self._currentColor = color def setColors(self, colors): """ Set the colors for the color bar. :type colors: list[str] or list[studioqt.Color] """ self.deleteButtons() self.layout().addStretch() for color in colors: if not isinstance(color, str): color = studioqt.Color(color) color = color.toString() callback = partial(self._colorChanged, color) css = "background-color: " + color button = self.COLOR_BUTTON_CLASS(self) button.setObjectName('colorButton') button.setStyleSheet(css) button.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) button.clicked.connect(callback) self.layout().addWidget(button) button = QtWidgets.QPushButton("...", self) button.setObjectName('browseColorButton') button.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) button.clicked.connect(self.browseColor) self.layout().addWidget(button) self.layout().addStretch() def setBrowserColors(self, colors): """ :type colors: list((int,int,int)) """ self._browserColors = colors def browserColors(self): """ Get the colors to be displayed in the browser :rtype: list[studioqt.Color] """ return self._browserColors @QtCore.Slot() def blankSlot(self): """Blank slot to fix an issue with PySide2.QColorDialog.open()""" pass def browseColor(self): """ Show the color dialog. :rtype: None """ color = self.currentColor() d = QtWidgets.QColorDialog(self) d.setCurrentColor(color) standardColors = self.browserColors() if standardColors: index = -1 for standardColor in standardColors: index += 1 try: # Support for new qt5 signature standardColor = QtGui.QColor(standardColor) d.setStandardColor(index, standardColor) except: # Support for new qt4 signature standardColor = QtGui.QColor(standardColor).rgba() d.setStandardColor(index, standardColor) d.currentColorChanged.connect(self._colorChanged) # PySide2 doesn't support d.open(), so we need to pass a blank slot. d.open(self, QtCore.SLOT("blankSlot()")) if d.exec_(): self._colorChanged(d.selectedColor()) else: self._colorChanged(color)
class ItemsWidget(QtWidgets.QWidget): IconMode = "icon" TableMode = "table" DEFAULT_PADDING = 5 DEFAULT_ZOOM_AMOUNT = 90 DEFAULT_TEXT_HEIGHT = 20 DEFAULT_WHEEL_SCROLL_STEP = 2 DEFAULT_MIN_SPACING = 0 DEFAULT_MAX_SPACING = 50 DEFAULT_MIN_LIST_SIZE = 15 DEFAULT_MIN_ICON_SIZE = 50 itemClicked = QtCore.Signal(object) itemDoubleClicked = QtCore.Signal(object) zoomChanged = QtCore.Signal(object) spacingChanged = QtCore.Signal(object) groupClicked = QtCore.Signal(object) def __init__(self, *args): QtWidgets.QWidget.__init__(self, *args) self._dpi = 1 self._padding = self.DEFAULT_PADDING w, h = self.DEFAULT_ZOOM_AMOUNT, self.DEFAULT_ZOOM_AMOUNT self._iconSize = QtCore.QSize(w, h) self._zoomAmount = self.DEFAULT_ZOOM_AMOUNT self._isItemTextVisible = True self._dataset = None self._treeWidget = TreeWidget(self) self._listView = ListView(self) self._listView.setTreeWidget(self._treeWidget) self._delegate = ItemDelegate() self._delegate.setItemsWidget(self) self._listView.setItemDelegate(self._delegate) self._treeWidget.setItemDelegate(self._delegate) self._toastWidget = ToastWidget(self) self._toastWidget.hide() self._toastEnabled = True self._textColor = QtGui.QColor(255, 255, 255, 200) self._textSelectedColor = QtGui.QColor(255, 255, 255, 200) self._backgroundColor = QtGui.QColor(255, 255, 255, 30) self._backgroundHoverColor = QtGui.QColor(255, 255, 255, 35) self._backgroundSelectedColor = QtGui.QColor(30, 150, 255) layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self._treeWidget) layout.addWidget(self._listView) header = self.treeWidget().header() header.sortIndicatorChanged.connect(self._sortIndicatorChanged) self.setLayout(layout) self.listView().itemClicked.connect(self._itemClicked) self.listView().itemDoubleClicked.connect(self._itemDoubleClicked) self.treeWidget().itemClicked.connect(self._itemClicked) self.treeWidget().itemDoubleClicked.connect(self._itemDoubleClicked) self.itemMoved = self._listView.itemMoved self.itemDropped = self._listView.itemDropped self.itemSelectionChanged = self._treeWidget.itemSelectionChanged def _sortIndicatorChanged(self): """ Triggered when the sort indicator changes. :rtype: None """ pass def _itemClicked(self, item): """ Triggered when the given item has been clicked. :type item: studioqt.ItemsWidget :rtype: None """ if isinstance(item, GroupItem): self.groupClicked.emit(item) else: self.itemClicked.emit(item) def _itemDoubleClicked(self, item): """ Triggered when the given item has been double clicked. :type item: studioqt.Item :rtype: None """ self.itemDoubleClicked.emit(item) def setDataset(self, dataset): self._dataset = dataset self.setColumnLabels(dataset.Fields) dataset.searchFinished.connect(self.updateItems) def dataset(self): return self._dataset def updateItems(self): """Sets the items to the widget.""" selectedItems = self.selectedItems() self.clearSelection() results = self.dataset().groupedResults() items = [] for group in results: if group != "None": groupItem = self.createGroupItem(group) items.append(groupItem) items.extend(results[group]) self.treeWidget().setItems(items) if selectedItems: self.selectItems(selectedItems) self.scrollToSelectedItem() def createGroupItem(self, text, children=None): """ Create a new group item for the given text and children. :type text: str :type children: list[studioqt.Item] :rtype: GroupItem """ groupItem = GroupItem() groupItem.setName(text) groupItem.setStretchToWidget(self) groupItem.setChildren(children) return groupItem def setToastEnabled(self, enabled): """ :type enabled: bool :rtype: None """ self._toastEnabled = enabled def toastEnabled(self): """ :rtype: bool """ return self._toastEnabled def showToastMessage(self, text, duration=500): """ Show a toast with the given text for the given duration. :type text: str :type duration: None or int :rtype: None """ if self.toastEnabled(): self._toastWidget.setDuration(duration) self._toastWidget.setText(text) self._toastWidget.show() def columnFromLabel(self, *args): """ Reimplemented for convenience. :return: int """ return self.treeWidget().columnFromLabel(*args) def setColumnHidden(self, column, hidden): """ Reimplemented for convenience. Calls self.treeWidget().setColumnHidden(column, hidden) """ self.treeWidget().setColumnHidden(column, hidden) def setLocked(self, value): """ Disables drag and drop. :Type value: bool :rtype: None """ self.listView().setDragEnabled(not value) self.listView().setDropEnabled(not value) def verticalScrollBar(self): """ Return the active vertical scroll bar. :rtype: QtWidget.QScrollBar """ if self.isTableView(): return self.treeWidget().verticalScrollBar() else: return self.listView().verticalScrollBar() def visualItemRect(self, item): """ Return the visual rect for the item. :type item: QtWidgets.QTreeWidgetItem :rtype: QtCore.QRect """ if self.isTableView(): visualRect = self.treeWidget().visualItemRect(item) else: index = self.treeWidget().indexFromItem(item) visualRect = self.listView().visualRect(index) return visualRect def isItemVisible(self, item): """ Return the visual rect for the item. :type item: QtWidgets.QTreeWidgetItem :rtype: bool """ height = self.height() itemRect = self.visualItemRect(item) scrollBarY = self.verticalScrollBar().value() y = (scrollBarY - itemRect.y()) + height return y > scrollBarY and y < scrollBarY + height def scrollToItem(self, item): """ Ensures that the item is visible. :type item: QtWidgets.QTreeWidgetItem :rtype: None """ position = QtWidgets.QAbstractItemView.PositionAtCenter if self.isTableView(): self.treeWidget().scrollToItem(item, position) elif self.isIconView(): self.listView().scrollToItem(item, position) def scrollToSelectedItem(self): """ Ensures that the item is visible. :rtype: None """ item = self.selectedItem() if item: self.scrollToItem(item) def dpi(self): """ return the zoom multiplier. Used for high resolution devices. :rtype: int """ return self._dpi def setDpi(self, dpi): """ Set the zoom multiplier. Used for high resolution devices. :type dpi: int """ self._dpi = dpi self.refreshSize() def itemAt(self, pos): """ Return the current item at the given pos. :type pos: QtWidgets.QPoint :rtype: studioqt.Item """ if self.isIconView(): return self.listView().itemAt(pos) else: return self.treeView().itemAt(pos) def insertItems(self, items, itemAt=None): """ Insert the given items at the given itemAt position. :type items: list[studioqt.Item] :type itemAt: studioqt.Item :rtype: Nones """ self.addItems(items) self.moveItems(items, itemAt=itemAt) self.treeWidget().setItemsSelected(items, True) def moveItems(self, items, itemAt=None): """ Move the given items to the given itemAt position. :type items: list[studioqt.Item] :type itemAt: studioqt.Item :rtype: None """ self.listView().moveItems(items, itemAt=itemAt) def listView(self): """ Return the list view that contains the items. :rtype: ListView """ return self._listView def treeWidget(self): """ Return the tree widget that contains the items. :rtype: TreeWidget """ return self._treeWidget def clear(self): """ Reimplemented for convenience. Calls self.treeWidget().clear() """ self.treeWidget().clear() def refresh(self): """Refresh the item size.""" self.refreshSize() def refreshSize(self): """ Refresh the size of the items. :rtype: None """ self.setZoomAmount(self.zoomAmount() + 1) self.setZoomAmount(self.zoomAmount() - 1) self.repaint() def itemFromIndex(self, index): """ Return a pointer to the QTreeWidgetItem assocated with the given index. :type index: QtCore.QModelIndex :rtype: QtWidgets.QTreeWidgetItem """ return self._treeWidget.itemFromIndex(index) def textFromItems(self, *args, **kwargs): """ Return all data for the given items and given column. :rtype: list[str] """ return self.treeWidget().textFromItems(*args, **kwargs) def textFromColumn(self, *args, **kwargs): """ Return all data for the given column. :rtype: list[str] """ return self.treeWidget().textFromColumn(*args, **kwargs) def toggleTextVisible(self): """ Toggle the item text visibility. :rtype: None """ if self.isItemTextVisible(): self.setItemTextVisible(False) else: self.setItemTextVisible(True) def setItemTextVisible(self, value): """ Set the visibility of the item text. :type value: bool :rtype: None """ self._isItemTextVisible = value self.refreshSize() def isItemTextVisible(self): """ Return the visibility of the item text. :rtype: bool """ if self.isIconView(): return self._isItemTextVisible else: return True def itemTextHeight(self): """ Return the height of the item text. :rtype: int """ return self.DEFAULT_TEXT_HEIGHT * self.dpi() def itemDelegate(self): """ Return the item delegate for the views. :rtype: ItemDelegate """ return self._delegate def settings(self): """ Return the current state of the widget. :rtype: dict """ settings = {} settings["columnLabels"] = self.columnLabels() settings["padding"] = self.padding() settings["spacing"] = self.spacing() settings["zoomAmount"] = self.zoomAmount() settings["selectedPaths"] = self.selectedPaths() settings["textVisible"] = self.isItemTextVisible() settings.update(self.treeWidget().settings()) return settings def setSettings(self, settings): """ Set the current state of the widget. :type settings: dict :rtype: None """ self.setToastEnabled(False) padding = settings.get("padding", 5) self.setPadding(padding) spacing = settings.get("spacing", 2) self.setSpacing(spacing) zoomAmount = settings.get("zoomAmount", 100) self.setZoomAmount(zoomAmount) selectedPaths = settings.get("selectedPaths", []) self.selectPaths(selectedPaths) itemTextVisible = settings.get("textVisible", True) self.setItemTextVisible(itemTextVisible) self.treeWidget().setSettings(settings) self.setToastEnabled(True) return settings def createCopyTextMenu(self): return self.treeWidget().createCopyTextMenu() def createItemSettingsMenu(self): menu = QtWidgets.QMenu("Item View", self) action = studioqt.SeparatorAction("View Settings", menu) menu.addAction(action) action = studioqt.SliderAction("Size", menu) action.slider().setMinimum(10) action.slider().setMaximum(200) action.slider().setValue(self.zoomAmount()) action.slider().valueChanged.connect(self.setZoomAmount) menu.addAction(action) action = studioqt.SliderAction("Border", menu) action.slider().setMinimum(0) action.slider().setMaximum(20) action.slider().setValue(self.padding()) action.slider().valueChanged.connect(self.setPadding) menu.addAction(action) # action = studioqt.SliderAction("Spacing", menu) action.slider().setMinimum(self.DEFAULT_MIN_SPACING) action.slider().setMaximum(self.DEFAULT_MAX_SPACING) action.slider().setValue(self.spacing()) action.slider().valueChanged.connect(self.setSpacing) menu.addAction(action) action = studioqt.SeparatorAction("Item Options", menu) menu.addAction(action) action = QtWidgets.QAction("Show labels", menu) action.setCheckable(True) action.setChecked(self.isItemTextVisible()) action.triggered[bool].connect(self.setItemTextVisible) menu.addAction(action) return menu def createSettingsMenu(self): """ Create and return the settings menu for the widget. :rtype: QtWidgets.QMenu """ menu = QtWidgets.QMenu("Item View", self) menu.addSeparator() action = QtWidgets.QAction("Show labels", menu) action.setCheckable(True) action.setChecked(self.isItemTextVisible()) action.triggered[bool].connect(self.setItemTextVisible) menu.addAction(action) menu.addSeparator() copyTextMenu = self.treeWidget().createCopyTextMenu() menu.addMenu(copyTextMenu) menu.addSeparator() action = studioqt.SliderAction("Size", menu) action.slider().setMinimum(10) action.slider().setMaximum(200) action.slider().setValue(self.zoomAmount()) action.slider().valueChanged.connect(self.setZoomAmount) menu.addAction(action) action = studioqt.SliderAction("Border", menu) action.slider().setMinimum(0) action.slider().setMaximum(20) action.slider().setValue(self.padding()) action.slider().valueChanged.connect(self.setPadding) menu.addAction(action) # action = studioqt.SliderAction("Spacing", menu) action.slider().setMinimum(self.DEFAULT_MIN_SPACING) action.slider().setMaximum(self.DEFAULT_MAX_SPACING) action.slider().setValue(self.spacing()) action.slider().valueChanged.connect(self.setSpacing) menu.addAction(action) return menu def createItemsMenu(self, items=None): """ Create the item menu for given item. :rtype: QtWidgets.QMenu """ item = items or self.selectedItem() menu = QtWidgets.QMenu(self) if item: try: item.contextMenu(menu) except Exception as error: logger.exception(error) else: action = QtWidgets.QAction(menu) action.setText("No Item selected") action.setDisabled(True) menu.addAction(action) return menu def createContextMenu(self): """ Create and return the context menu for the widget. :rtype: QtWidgets.QMenu """ menu = self.createItemsMenu() settingsMenu = self.createSettingsMenu() menu.addMenu(settingsMenu) return menu def contextMenuEvent(self, event): """ Show the context menu. :type event: QtCore.QEvent :rtype: None """ menu = self.createContextMenu() point = QtGui.QCursor.pos() return menu.exec_(point) # ------------------------------------------------------------------------ # Support for saving the current item order. # ------------------------------------------------------------------------ def itemData(self, columnLabels): """ Return all column data for the given column labels. :type columnLabels: list[str] :rtype: dict """ data = {} for item in self.items(): key = item.id() for columnLabel in columnLabels: column = self.treeWidget().columnFromLabel(columnLabel) value = item.data(column, QtCore.Qt.EditRole) data.setdefault(key, {}) data[key].setdefault(columnLabel, value) return data def setItemData(self, data): """ Set the item data for all the current items. :type data: dict :rtype: None """ for item in self.items(): key = item.id() if key in data: item.setItemData(data[key]) def updateColumns(self): """ Update the column labels with the current item data. :rtype: None """ self.treeWidget().updateHeaderLabels() def columnLabels(self): """ Set all the column labels. :rtype: list[str] """ return self.treeWidget().columnLabels() def _removeDuplicates(self, labels): """ Removes duplicates from a list in Python, whilst preserving order. :type labels: list[str] :rtype: list[str] """ s = set() sadd = s.add return [x for x in labels if x.strip() and not (x in s or sadd(x))] def setColumnLabels(self, labels): """ Set the columns for the widget. :type labels: list[str] :rtype: None """ labels = self._removeDuplicates(labels) if "Custom Order" not in labels: labels.append("Custom Order") # if "Search Order" not in labels: # labels.append("Search Order") self.treeWidget().setHeaderLabels(labels) self.setColumnHidden("Custom Order", True) # self.setColumnHidden("Search Order", True) def items(self): """ Return all the items in the widget. :rtype: list[studioqt.Item] """ return self._treeWidget.items() def addItems(self, items): """ Add the given items to the items widget. :type items: list[studioqt.Item] :rtype: None """ self._treeWidget.addTopLevelItems(items) def addItem(self, item): """ Add the item to the tree widget. :type item: Item :rtype: None """ self.addItems([item]) def columnLabelsFromItems(self): """ Return the column labels from all the items. :rtype: list[str] """ seq = [] for item in self.items(): seq.extend(item._textColumnOrder) seen = set() return [x for x in seq if x not in seen and not seen.add(x)] def refreshColumns(self): self.setColumnLabels(self.columnLabelsFromItems()) def padding(self): """ Return the item padding. :rtype: int """ return self._padding def setPadding(self, value): """ Set the item padding. :type: int :rtype: None """ if value % 2 == 0: self._padding = value else: self._padding = value + 1 self.repaint() self.showToastMessage("Border: " + str(value)) def spacing(self): """ Return the spacing between the items. :rtype: int """ return self._listView.spacing() def setSpacing(self, spacing): """ Set the spacing between the items. :type spacing: int :rtype: None """ self._listView.setSpacing(spacing) self.scrollToSelectedItem() self.showToastMessage("Spacing: " + str(spacing)) def iconSize(self): """ Return the icon size for the views. :rtype: QtCore.QSize """ return self._iconSize def setIconSize(self, size): """ Set the icon size for the views. :type size: QtCore.QSize :rtype: None """ self._iconSize = size self._listView.setIconSize(size) self._treeWidget.setIconSize(size) def clearSelection(self): """ Clear the user selection. :rtype: None """ self._treeWidget.clearSelection() def wheelScrollStep(self): """ Return the wheel scroll step amount. :rtype: int """ return self.DEFAULT_WHEEL_SCROLL_STEP def model(self): """ Return the model that this view is presenting. :rtype: QAbstractItemModel """ return self._treeWidget.model() def indexFromItem(self, item): """ Return the QModelIndex assocated with the given item. :type item: QtWidgets.QTreeWidgetItem. :rtype: QtCore.QModelIndex """ return self._treeWidget.indexFromItem(item) def selectionModel(self): """ Return the current selection model. :rtype: QtWidgets.QItemSelectionModel """ return self._treeWidget.selectionModel() def selectedItem(self): """ Return the last selected non-hidden item. :rtype: QtWidgets.QTreeWidgetItem """ return self._treeWidget.selectedItem() def selectedItems(self): """ Return a list of all selected non-hidden items. :rtype: list[QtWidgets.QTreeWidgetItem] """ return self._treeWidget.selectedItems() def setItemHidden(self, item, value): """ Set the visibility of given item. :type item: QtWidgets.QTreeWidgetItem :type value: bool :rtype: None """ item.setHidden(value) def setItemsHidden(self, items, value): """ Set the visibility of given items. :type items: list[QtWidgets.QTreeWidgetItem] :type value: bool :rtype: None """ for item in items: self.setItemHidden(item, value) def selectedPaths(self): """ Return the selected item paths. :rtype: list[str] """ paths = [] for item in self.selectedItems(): path = item.url().toLocalFile() paths.append(path) return paths def selectPaths(self, paths): """ Selected the items that have the given paths. :type paths: list[str] :rtype: None """ for item in self.items(): path = item.id() if path in paths: item.setSelected(True) def selectItems(self, items): """ Select the given items. :type items: list[studiolibrary.LibraryItem] :rtype: None """ paths = [item.id() for item in items] self.selectPaths(paths) def isIconView(self): """ Return True if widget is in Icon mode. :rtype: bool """ return not self._listView.isHidden() def isTableView(self): """ Return True if widget is in List mode. :rtype: bool """ return not self._treeWidget.isHidden() def setViewMode(self, mode): """ Set the view mode for this widget. :type mode: str :rtype: None """ if mode == self.IconMode: self.setZoomAmount(self.DEFAULT_MIN_ICON_SIZE) elif mode == self.TableMode: self.setZoomAmount(self.DEFAULT_MIN_ICON_SIZE) def _setViewMode(self, mode): """ Set the view mode for this widget. :type mode: str :rtype: None """ if mode == self.IconMode: self.setIconMode() elif mode == self.TableMode: self.setListMode() def setListMode(self): """ Set the tree widget visible. :rtype: None """ self._listView.hide() self._treeWidget.show() self._treeWidget.setFocus() def setIconMode(self): """ Set the list view visible. :rtype: None """ self._treeWidget.hide() self._listView.show() self._listView.setFocus() def zoomAmount(self): """ Return the zoom amount for the widget. :rtype: int """ return self._zoomAmount def setZoomAmount(self, value): """ Set the zoom amount for the widget. :type value: int :rtype: None """ if value < self.DEFAULT_MIN_LIST_SIZE: value = self.DEFAULT_MIN_LIST_SIZE self._zoomAmount = value size = QtCore.QSize(value * self.dpi(), value * self.dpi()) self.setIconSize(size) if value >= self.DEFAULT_MIN_ICON_SIZE: self._setViewMode(self.IconMode) else: self._setViewMode(self.TableMode) columnWidth = value * self.dpi() + self.itemTextHeight() self._treeWidget.setIndentation(0) self._treeWidget.setColumnWidth(0, columnWidth) self.scrollToSelectedItem() msg = "Size: {0}%".format(value) self.showToastMessage(msg) def wheelEvent(self, event): """ Triggered on any wheel events for the current viewport. :type event: QtWidgets.QWheelEvent :rtype: None """ modifier = QtWidgets.QApplication.keyboardModifiers() validModifiers = ( QtCore.Qt.AltModifier, QtCore.Qt.ControlModifier, ) if modifier in validModifiers: numDegrees = event.delta() / 8 numSteps = numDegrees / 15 delta = (numSteps * self.wheelScrollStep()) value = self.zoomAmount() + delta self.setZoomAmount(value) def setTextColor(self, color): """ Set the item text color. :type color: QtWidgets.QtColor """ self._textColor = color def setTextSelectedColor(self, color): """ Set the text color when an item is selected. :type color: QtWidgets.QtColor """ self._textSelectedColor = color def setBackgroundColor(self, color): """ Set the item background color. :type color: QtWidgets.QtColor """ self._backgroundColor = color def setBackgroundHoverColor(self, color): """ Set the background color when the mouse hovers over the item. :type color: QtWidgets.QtColor """ self._backgroundHoverColor = color def setBackgroundSelectedColor(self, color): """ Set the background color when an item is selected. :type color: QtWidgets.QtColor """ self._backgroundSelectedColor = color self._listView.setRubberBandColor(QtGui.QColor(200, 200, 200, 255)) def textColor(self): """ Return the item text color. :rtype: QtGui.QColor """ return self._textColor def textSelectedColor(self): """ Return the item text color when selected. :rtype: QtGui.QColor """ return self._textSelectedColor def backgroundColor(self): """ Return the item background color. :rtype: QtWidgets.QtColor """ return self._backgroundColor def backgroundHoverColor(self): """ Return the background color for when the mouse is over an item. :rtype: QtWidgets.QtColor """ return self._backgroundHoverColor def backgroundSelectedColor(self): """ Return the background color when an item is selected. :rtype: QtWidgets.QtColor """ return self._backgroundSelectedColor