class OptionsWidget(QtWidgets.QFrame): accepted = QtCore.Signal(object) OptionWidgetMap = {'label': LabelOption} def __init__(self, *args, **kwargs): super(OptionsWidget, self).__init__(*args, **kwargs) layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) self.setStyleSheet(STYLE) self.setLayout(layout) self._widgets = [] self._validator = None self._optionsFrame = QtWidgets.QFrame(self) self._optionsFrame.setObjectName('optionsFrame') layout = QtWidgets.QVBoxLayout(self._optionsFrame) self._optionsFrame.setLayout(layout) self.layout().addWidget(self._optionsFrame) def reset(self): """Reset all option widgets back to their default value.""" for widget in self._widgets: widget.reset() self.validate() def setOptions(self, options): """Set the options for the widget.""" self._options = options for option in options: cls = self.OptionWidgetMap.get(option.get('type', 'label')) widget = cls() widget.setOption(option) self._widgets.append(widget) callback = functools.partial(self._optionChanged, widget) widget.valueChanged.connect(callback) self._optionsFrame.layout().addWidget(widget) self._optionsFrame.layout().addStretch(0) def _optionChanged(self, widget): """ Triggered when the given option widget changes value. :type widget: OptionWidget """ self.validate() def accept(self): """Accept the current options""" self.emitAcceptedCallback() def closeEvent(self, event): super(OptionsWidget, self).closeEvent(event) def setValidator(self, validator): """ Set the validator for the options. :type validator: func """ self._validator = validator def validate(self): """Validate the current options using the validator.""" if self._validator: options = self.options() state = self._validator(**options) self._setState(state) else: logger.warning("No validator set.") 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: OptionWidget """ for widget in self._widgets: if widget.options().get('name') == name: return widget def options(self): """ Get all the option data. :rtype: dict """ options = {} for widget in self._widgets: options[widget.option().get('name')] = widget.value() return options def state(self): """ Get the current state. :rtype: dict """ state = {} for widget in self._widgets: name = widget.option().get('name') state.setdefault(name, {}) state[name]['value'] = widget.value() return state def setState(self, state): """ Set the current state. :type state: dict """ self._setState(state) self.validate() def _setState(self, state): for widget in self._widgets: widget.blockSignals(True) for widget in self._widgets: name = widget.option().get('name') widget.setState(state.get(name, {})) for widget in self._widgets: widget.blockSignals(False)
class BaseItemSignals(QtCore.QObject): """""" loadValueChanged = QtCore.Signal(object, object)
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())
def showMessageBox(parent, title, text, width=None, height=None, buttons=None, headerIcon=None, headerColor=None, enableDontShowCheckBox=False, force=False): """ Open a question message box with the given options. :type parent: QWidget :type title: str :type text: str :type buttons: list[QMessageBox.StandardButton] :type headerIcon: str :type headerColor: str :type enableDontShowCheckBox: bool :type force: bool :rtype: MessageBox """ settings = QtCore.QSettings(SETTINGS_PATH, QtCore.QSettings.IniFormat) key = 'MessageBox/{}/'.format(title.replace(" ", "_")) clickedButton = int(settings.value(key + "clickedButton") or -1) dontShowAgain = settings.value(key + "dontShowAgain") if isinstance(dontShowAgain, basestring): dontShowAgain = dontShowAgain == "true" # Force show the dialog if the user is holding the ctrl key down if studioqt.isControlModifier() or studioqt.isAltModifier(): force = True if force or not dontShowAgain or not enableDontShowCheckBox: mb = createMessageBox(parent, title, text, width=width, height=height, buttons=buttons, headerIcon=headerIcon, headerColor=headerColor, enableDontShowCheckBox=enableDontShowCheckBox) mb.exec_() mb.close() # Save the button that was clicked by the user clickedButton = mb.clickedStandardButton() settings.setValue(key + "clickedButton", clickedButton) # Save the dont show again checked state dontShowAgain = mb.isDontShowCheckboxChecked() settings.setValue(key + "dontShowAgain", dontShowAgain) settings.sync() return clickedButton
class CombinedWidget(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._treeWidget = CombinedTreeWidget(self) self._listView = CombinedListView(self) self._listView.setTreeWidget(self._treeWidget) self._delegate = CombinedItemDelegate() self._delegate.setCombinedWidget(self) self._listView.setItemDelegate(self._delegate) self._treeWidget.setItemDelegate(self._delegate) self._toastWidget = studioqt.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.CombinedWidgetItem :rtype: None """ if isinstance(item, studioqt.CombinedWidgetItemGroup): 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.CombinedWidgetItem :rtype: None """ self.itemDoubleClicked.emit(item) 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 sortOrder(self): """ Reimplemented for convenience. Calls self.treeWidget().sortOrder() """ return self.treeWidget().sortOrder() def sortColumn(self): """ Reimplemented for convenience. Calls self.treeWidget().sortColumn() """ return self.treeWidget().sortColumn() def sortByColumn(self, *args, **kwargs): """ Reimplemented for convenience. Calls self.treeWidget().sortByColumn(*args) """ self.treeWidget().sortByColumn(*args, **kwargs) def groupOrder(self): """ Reimplemented for convenience. Calls self.treeWidget().groupOrder() """ return self.treeWidget().groupOrder() def groupColumn(self): """ Reimplemented for convenience. Calls self.treeWidget().groupColumn() """ return self.treeWidget().groupColumn() def groupByColumn(self, *args): """ Reimplemented for convenience. Calls self.treeWidget().groupByColumn(*args) """ self.treeWidget().groupByColumn(*args) 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 scrollToItem(self, item): """ Ensures that the item is visible. :type item: QtWidgets.QTreeWidgetItem :rtype: None """ if self.isTableView(): self.treeWidget().scrollToItem( item, QtWidgets.QAbstractItemView.PositionAtCenter) elif self.isIconView(): self.listView().scrollToItem( item, QtWidgets.QAbstractItemView.PositionAtCenter) 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.CombinedWidgetItem """ 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.CombinedWidgetItem] :type itemAt: studioqt.CombinedWidgetItem :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.CombinedWidgetItem] :type itemAt: studioqt.CombinedWidgetItem :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 sorting and size of the items. :rtype: None """ self.refreshSortBy() 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 refreshSortBy(self): """ Refresh the sorting of the items. :rtype: None """ self.treeWidget().refreshSortBy() 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: CombinedItemDelegate """ 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 createSortByMenu(self): return self.treeWidget().createSortByMenu() def createGroupByMenu(self): return self.treeWidget().createGroupByMenu() 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() sortByMenu = self.treeWidget().createSortByMenu() menu.addMenu(sortByMenu) groupByMenu = self.treeWidget().createGroupByMenu() menu.addMenu(groupByMenu) 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, msg: logger.exception(msg) else:
class PoseItemSignals(QtCore.QObject): """Signals need to be attached to a QObject""" mirrorChanged = QtCore.Signal(bool)
class ListView(ItemViewMixin, QtWidgets.QListView): itemMoved = QtCore.Signal(object) itemDropped = QtCore.Signal(object) itemClicked = QtCore.Signal(object) itemDoubleClicked = QtCore.Signal(object) DEFAULT_DRAG_THRESHOLD = 10 def __init__(self, *args): QtWidgets.QListView.__init__(self, *args) ItemViewMixin.__init__(self) self._treeWidget = None self._rubberBand = None self._rubberBandStartPos = None self._rubberBandColor = QtGui.QColor(QtCore.Qt.white) self._customSortOrder = [] self._drag = None self._dragStartPos = None self._dragStartIndex = None self._dropEnabled = True self.setSpacing(5) self.setMouseTracking(True) self.setSelectionRectVisible(True) self.setViewMode(QtWidgets.QListView.IconMode) self.setResizeMode(QtWidgets.QListView.Adjust) self.setSelectionMode(QtWidgets.QListWidget.ExtendedSelection) self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.setAcceptDrops(True) self.setDragEnabled(True) self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop) self.clicked.connect(self._indexClicked) self.doubleClicked.connect(self._indexDoubleClicked) def _indexClicked(self, index): """ Triggered when the user clicks on an index. :type index: QtCore.QModelIndex :rtype: None """ item = self.itemFromIndex(index) item.clicked() self.setItemsSelected([item], True) self.itemClicked.emit(item) def _indexDoubleClicked(self, index): """ Triggered when the user double clicks on an index. :type index: QtCore.QModelIndex :rtype: None """ item = self.itemFromIndex(index) self.setItemsSelected([item], True) item.doubleClicked() self.itemDoubleClicked.emit(item) def treeWidget(self): """ Return the tree widget that contains the item. :rtype: QtWidgets.QTreeWidget """ return self._treeWidget def setTreeWidget(self, treeWidget): """ Set the tree widget that contains the item. :type treeWidget: QtWidgets.QTreeWidget :rtype: None """ self._treeWidget = treeWidget self.setModel(treeWidget.model()) self.setSelectionModel(treeWidget.selectionModel()) def scrollToItem(self, item, pos=None): """ Ensures that the item is visible. :type item: QtWidgets.QTreeWidgetItem :type pos: QtCore.QPoint or None :rtype: None """ index = self.indexFromItem(item) pos = pos or QtWidgets.QAbstractItemView.PositionAtCenter self.scrollTo(index, pos) def items(self): """ Return all the items. :rtype: list[QtWidgets.QTreeWidgetItem] """ return self.treeWidget().items() def itemAt(self, pos): """ Return a pointer to the item at the coordinates p. The coordinates are relative to the tree widget's viewport(). :type pos: QtCore.QPoint :rtype: QtWidgets.QTreeWidgetItem """ index = self.indexAt(pos) return self.itemFromIndex(index) def indexFromItem(self, item): """ Return the QModelIndex assocated with the given item. :type item: QtWidgets.QTreeWidgetItem. :rtype: QtCore.QModelIndex """ return self.treeWidget().indexFromItem(item) def itemFromIndex(self, index): """ Return a pointer to the QTreeWidgetItem assocated with the given index. :type index: QtCore.QModelIndex :rtype: QtWidgets.QTreeWidgetItem """ return self.treeWidget().itemFromIndex(index) def insertItem(self, row, item): """ Inserts the item at row in the top level in the view. :type row: int :type item: QtWidgets.QTreeWidgetItem :rtype: None """ self.treeWidget().insertTopLevelItem(row, item) def takeItems(self, items): """ Removes and returns the items from the view :type items: list[QtWidgets.QTreeWidgetItem] :rtype: list[QtWidgets.QTreeWidgetItem] """ for item in items: row = self.treeWidget().indexOfTopLevelItem(item) self.treeWidget().takeTopLevelItem(row) return items def selectedItem(self): """ Return the last selected non-hidden item. :rtype: QtWidgets.QTreeWidgetItem """ return self.treeWidget().selectedItem() def selectedItems(self): """ Return a list of all selected non-hidden items. :rtype: list[QtWidgets.QTreeWidgetItem] """ return self.treeWidget().selectedItems() def setIndexesSelected(self, indexes, value): """ Set the selected state for the given indexes. :type indexes: list[QtCore.QModelIndex] :type value: bool :rtype: None """ items = self.itemsFromIndexes(indexes) self.setItemsSelected(items, value) def setItemsSelected(self, items, value): """ Set the selected state for the given items. :type items: list[studioqt.WidgetItem] :type value: bool :rtype: None """ self.treeWidget().blockSignals(True) for item in items: self.treeWidget().setItemSelected(item, value) self.treeWidget().blockSignals(False) def moveItems(self, items, itemAt): """ Move the given items to the position of the destination row. :type items: list[studioqt.Item] :type itemAt: studioqt.Item :rtype: None """ scrollValue = self.verticalScrollBar().value() self.treeWidget().moveItems(items, itemAt) self.itemMoved.emit(items[-1]) self.verticalScrollBar().setValue(scrollValue) # --------------------------------------------------------------------- # Support for a custom colored rubber band. # --------------------------------------------------------------------- def createRubberBand(self): """ Create a new instance of the selection rubber band. :rtype: QtWidgets.QRubberBand """ rubberBand = QtWidgets.QRubberBand(QtWidgets.QRubberBand.Rectangle, self) palette = QtGui.QPalette() color = self.rubberBandColor() palette.setBrush(QtGui.QPalette.Highlight, QtGui.QBrush(color)) rubberBand.setPalette(palette) return rubberBand def setRubberBandColor(self, color): """ Set the color for the rubber band. :type color: QtGui.QColor :rtype: None """ self._rubberBand = None self._rubberBandColor = color def rubberBandColor(self): """ Return the rubber band color for this widget. :rtype: QtGui.QColor """ return self._rubberBandColor def rubberBand(self): """ Return the selection rubber band for this widget. :rtype: QtWidgets.QRubberBand """ if not self._rubberBand: self.setSelectionRectVisible(False) self._rubberBand = self.createRubberBand() return self._rubberBand # --------------------------------------------------------------------- # Events # --------------------------------------------------------------------- def validateDragEvent(self, event): """ Validate the drag event. :type event: QtWidgets.QMouseEvent :rtype: bool """ return QtCore.Qt.LeftButton == event.mouseButtons() def mousePressEvent(self, event): """ Triggered when the user presses the mouse button for the viewport. :type event: QtWidgets.QMouseEvent :rtype: None """ item = self.itemAt(event.pos()) if not item: self.clearSelection() ItemViewMixin.mousePressEvent(self, event) if event.isAccepted(): QtWidgets.QListView.mousePressEvent(self, event) self.itemsWidget().treeWidget().setItemSelected(item, True) self.endDrag() self._dragStartPos = event.pos() isLeftButton = self.mousePressButton() == QtCore.Qt.LeftButton isItemDraggable = item and item.dragEnabled() isSelectionEmpty = not self.selectedItems() if isLeftButton and (isSelectionEmpty or not isItemDraggable): self.rubberBandStartEvent(event) def mouseMoveEvent(self, event): """ Triggered when the user moves the mouse over the current viewport. :type event: QtWidgets.QMouseEvent :rtype: None """ if not self.isDraggingItems(): isLeftButton = self.mousePressButton() == QtCore.Qt.LeftButton if isLeftButton and self.rubberBand().isHidden( ) and self.selectedItems(): self.startDrag(event) else: ItemViewMixin.mouseMoveEvent(self, event) QtWidgets.QListView.mouseMoveEvent(self, event) if isLeftButton: self.rubberBandMoveEvent(event) def mouseReleaseEvent(self, event): """ Triggered when the user releases the mouse button for this viewport. :type event: QtWidgets.QMouseEvent :rtype: None """ item = self.itemAt(event.pos()) items = self.selectedItems() ItemViewMixin.mouseReleaseEvent(self, event) if item not in items: if event.button() != QtCore.Qt.MidButton: QtWidgets.QListView.mouseReleaseEvent(self, event) elif not items: QtWidgets.QListView.mouseReleaseEvent(self, event) self.endDrag() self.rubberBand().hide() def rubberBandStartEvent(self, event): """ Triggered when the user presses an empty area. :type event: QtWidgets.QMouseEvent :rtype: None """ self._rubberBandStartPos = event.pos() rect = QtCore.QRect(self._rubberBandStartPos, QtCore.QSize()) rubberBand = self.rubberBand() rubberBand.setGeometry(rect) rubberBand.show() def rubberBandMoveEvent(self, event): """ Triggered when the user moves the mouse over the current viewport. :type event: QtWidgets.QMouseEvent :rtype: None """ if self.rubberBand() and self._rubberBandStartPos: rect = QtCore.QRect(self._rubberBandStartPos, event.pos()) rect = rect.normalized() self.rubberBand().setGeometry(rect) # ----------------------------------------------------------------------- # Support for drag and drop # ----------------------------------------------------------------------- def rowAt(self, pos): """ Return the row for the given pos. :type pos: QtCore.QPoint :rtype: int """ return self.treeWidget().rowAt(pos) def itemsFromUrls(self, urls): """ Return items from the given url objects. :type urls: list[QtCore.QUrl] :rtype: list[studioqt.Item] """ items = [] for url in urls: item = self.itemFromUrl(url) if item: items.append(item) return items def itemFromUrl(self, url): """ Return the item from the given url object. :type url: QtCore.QUrl :rtype: studioqt.Item """ return self.itemFromPath(url.path()) def itemsFromPaths(self, paths): """ Return the items from the given paths. :type paths: list[str] :rtype: list[studioqt.Item] """ items = [] for path in paths: item = self.itemFromPath(path) if item: items.append(item) return items def itemFromPath(self, path): """ Return the item from the given path. :type path: str :rtype: studioqt.Item """ for item in self.items(): path_ = item.url().path() if path_ and path_ == path: return item def setDropEnabled(self, value): """ :type value: bool :rtype: None """ self._dropEnabled = value def dropEnabled(self): """ :rtype: bool """ return self._dropEnabled def dragThreshold(self): """ :rtype: int """ return self.DEFAULT_DRAG_THRESHOLD def mimeData(self, items): """ :type items: list[studioqt.Item] :rtype: QtCore.QMimeData """ mimeData = QtCore.QMimeData() urls = [item.url() for item in items] text = "\n".join([item.mimeText() for item in items]) mimeData.setUrls(urls) mimeData.setText(text) return mimeData def dropEvent(self, event): """ This event handler is called when the drag is dropped on this widget. :type event: QtWidgets.QDropEvent :rtype: None """ item = self.itemAt(event.pos()) selectedItems = self.selectedItems() if selectedItems and item: if self.treeWidget().isSortByCustomOrder(): self.moveItems(selectedItems, item) else: msg = "You can only re-order items when sorting by custom order." logger.info(msg) if item: item.dropEvent(event) self.itemDropped.emit(event) def dragMoveEvent(self, event): """ This event handler is called if a drag is in progress. :type event: QtGui.QDragMoveEvent :rtype: None """ mimeData = event.mimeData() if (mimeData.hasText() or mimeData.hasUrls()) and self.dropEnabled(): event.accept() else: event.ignore() def dragEnterEvent(self, event): """ This event handler is called when the mouse enters this widget while a drag is in pregress. :type event: QtGui.QDragEnterEvent :rtype: None """ mimeData = event.mimeData() if (mimeData.hasText() or mimeData.hasUrls()) and self.dropEnabled(): event.accept() else: event.ignore() def isDraggingItems(self): """ Return true if the user is currently dragging items. :rtype: bool """ return bool(self._drag) def startDrag(self, event): """ Starts a drag using the given event. :type event: QtCore.QEvent :rtype: None """ if not self.dragEnabled(): return if self._dragStartPos and hasattr(event, "pos"): item = self.itemAt(event.pos()) if item and item.dragEnabled(): self._dragStartIndex = self.indexAt(event.pos()) point = self._dragStartPos - event.pos() dt = self.dragThreshold() if point.x() > dt or point.y() > dt or point.x( ) < -dt or point.y() < -dt: items = self.selectedItems() mimeData = self.mimeData(items) pixmap = self.dragPixmap(item, items) hotSpot = QtCore.QPoint(pixmap.width() / 2, pixmap.height() / 2) self._drag = QtGui.QDrag(self) self._drag.setPixmap(pixmap) self._drag.setHotSpot(hotSpot) self._drag.setMimeData(mimeData) self._drag.start(QtCore.Qt.MoveAction) def endDrag(self): """ Ends the current drag. :rtype: None """ logger.debug("End Drag") self._dragStartPos = None self._dragStartIndex = None if self._drag: del self._drag self._drag = None def dragPixmap(self, item, items): """ Show the drag pixmap for the given item. :type item: studioqt.Item :type items: list[studioqt.Item] :rtype: QtGui.QPixmap """ rect = self.visualRect(self.indexFromItem(item)) pixmap = QtGui.QPixmap() pixmap = pixmap.grabWidget(self, rect) if len(items) > 1: cWidth = 35 cPadding = 5 cText = str(len(items)) cX = pixmap.rect().center().x() - float(cWidth / 2) cY = pixmap.rect().top() + cPadding cRect = QtCore.QRect(cX, cY, cWidth, cWidth) painter = QtGui.QPainter(pixmap) painter.setRenderHint(QtGui.QPainter.Antialiasing) painter.setPen(QtCore.Qt.NoPen) painter.setBrush(self.itemsWidget().backgroundSelectedColor()) painter.drawEllipse(cRect.center(), float(cWidth / 2), float(cWidth / 2)) font = QtGui.QFont('Serif', 12, QtGui.QFont.Light) painter.setFont(font) painter.setPen(self.itemsWidget().textSelectedColor()) painter.drawText(cRect, QtCore.Qt.AlignCenter, str(cText)) return pixmap
class Item(QtWidgets.QTreeWidgetItem): """The Item is used to hold rows of information for an item view.""" SortRole = "SortRole" DataRole = "DataRole" ThreadPool = QtCore.QThreadPool() MAX_ICON_SIZE = 256 DEFAULT_FONT_SIZE = 13 DEFAULT_PLAYHEAD_COLOR = QtGui.QColor(255, 255, 255, 220) THUMBNAIL_COLUMN = 0 ENABLE_THUMBNAIL_THREAD = True _globalSignals = GlobalSignals() blendChanged = _globalSignals.blendChanged def __init__(self, *args): QtWidgets.QTreeWidgetItem.__init__(self, *args) self._url = None self._path = None self._size = None self._rect = None self._textColumnOrder = [] self._data = {} self._itemData = {} self._icon = {} self._fonts = {} self._thread = None self._pixmap = {} self._pixmapRect = None self._pixmapScaled = None self._iconPath = "" self._thumbnailIcon = None self._underMouse = False self._searchText = None self._infoWidget = None self._groupItem = None self._groupColumn = 0 self._mimeText = None self._itemsWidget = None self._stretchToWidget = None self._dragEnabled = True self._imageSequence = None self._imageSequencePath = None self._blendValue = 0.0 self._blendPreviousValue = 0.0 self._blendPosition = None self._blendingEnabled = False self._worker = ImageWorker() self._worker.setAutoDelete(False) self._worker.signals.triggered.connect(self._thumbnailFromImage) self._workerStarted = False def __eq__(self, other): return id(other) == id(self) def __ne__(self, other): return id(other) != id(self) def __del__(self): """ Make sure the sequence is stopped when deleted. :rtype: None """ self.stop() def columnFromLabel(self, label): if self.treeWidget(): return self.treeWidget().columnFromLabel(label) else: return None def labelFromColumn(self, column): if self.treeWidget(): return self.treeWidget().labelFromColumn(column) else: return None def mimeText(self): """ Return the mime text for drag and drop. :rtype: str """ return self._mimeText or self.text(0) def setMimeText(self, text): """ Set the mime text for drag and drop. :type text: str :rtype: None """ self._mimeText = text def setHidden(self, value): """ Set the item hidden. :type value: bool :rtype: None """ QtWidgets.QTreeWidgetItem.setHidden(self, value) row = self.treeWidget().indexFromItem(self).row() self.itemsWidget().listView().setRowHidden(row, value) def setDragEnabled(self, value): """ Set True if the item can be dragged. :type value: bool :rtype: None """ self._dragEnabled = value def dragEnabled(self): """ Return True if the item can be dragged. :rtype: bool """ return self._dragEnabled def setIcon(self, column, icon, color=None): """ Set the icon to be displayed in the given column. :type column: int or str :type icon: QtGui.QIcon :type color: QtGui.QColor or None :rtype: None """ # Safe guard for when the class is being used without the gui. isAppRunning = bool(QtWidgets.QApplication.instance()) if not isAppRunning: return if isinstance(icon, basestring): if not os.path.exists(icon): color = color or studioqt.Color(255, 255, 255, 20) icon = studiolibrary.resource().icon("image", color=color) else: icon = QtGui.QIcon(icon) if isinstance(column, basestring): self._icon[column] = icon else: self._pixmap[column] = None QtWidgets.QTreeWidgetItem.setIcon(self, column, icon) self.updateIcon() def setItemData(self, data): """ Set the given dictionary as the data for the item. :type data: dict :rtype: None """ self._itemData = data def itemData(self): """ Return the item data for this item. :rtype: dict """ return self._itemData def setName(self, text): """ Set the name that is shown under the icon and in the Name column. :type text: str :rtype: None """ itemData = self.itemData() itemData['icon'] = text itemData['name'] = text def name(self): """ Return text for the Name column. :rtype: str """ return self.itemData().get("name") def displayText(self, label): """ Return the sort data for the given column. :type label: str :rtype: str """ return unicode(self.itemData().get(label, '')) def sortText(self, label): """ Return the sort data for the given column. :type label: str :rtype: str """ return unicode(self.itemData().get(label, '')) def update(self): """ Refresh the visual state of the icon. :rtype: None """ self.updateIcon() self.updateFrame() def updateIcon(self): """ Clear the pixmap cache for the item. :rtype: None """ self.clearCache() def clearCache(self): """Clear the thumbnail cache.""" self._pixmap = {} self._pixmapRect = None self._pixmapScaled = None self._thumbnailIcon = None def dpi(self): """ Used for high resolution devices. :rtype: int """ if self.itemsWidget(): return self.itemsWidget().dpi() else: return 1 def clicked(self): """ Triggered when an item is clicked. :rtype: None """ pass def takeFromTree(self): """ Takes this item from the tree. """ tree = self.treeWidget() parent = self.parent() if parent: parent.takeChild(parent.indexOfChild(self)) else: tree.takeTopLevelItem(tree.indexOfTopLevelItem(self)) def selectionChanged(self): """ Triggered when an item has been either selected or deselected. :rtype: None """ self.resetBlending() def doubleClicked(self): """ Triggered when an item is double clicked. :rtype: None """ pass def setGroupItem(self, groupItem): self._groupItem = groupItem def groupItem(self): return self._groupItem def itemsWidget(self): """ Returns the items widget that contains the items. :rtype: ItemsWidget """ itemsWidget = None if self.treeWidget(): itemsWidget = self.treeWidget().parent() return itemsWidget def url(self): """ Return the url object for the given item. :rtype: QtCore.QUrl or None """ if not self._url: self._url = QtCore.QUrl(self.text(0)) return self._url def setUrl(self, url): """ Set the url object for the item. :type: QtCore.QUrl or None :rtype: None """ self._url = url def searchText(self): """ Return the search string used for finding the item. :rtype: str """ if not self._searchText: self._searchText = unicode(self._data) return self._searchText def setStretchToWidget(self, widget): """ Set the width of the item to the width of the given widget. :type widget: QtWidgets.QWidget :rtype: None """ self._stretchToWidget = widget def stretchToWidget(self): """ Return the sretchToWidget. :rtype: QtWidgets.QWidget """ return self._stretchToWidget def setSize(self, size): """ Set the size for the item. :type size: QtCore.QSize :rtype: None """ self._size = size def sizeHint(self, column=0): """ Return the current size of the item. :type column: int :rtype: QtCore.QSize """ if self.stretchToWidget(): if self._size: size = self._size else: size = self.itemsWidget().iconSize() w = self.stretchToWidget().width() h = size.height() return QtCore.QSize(w - 20, h) if self._size: return self._size else: iconSize = self.itemsWidget().iconSize() if self.isTextVisible(): w = iconSize.width() h = iconSize.width() + self.textHeight() iconSize = QtCore.QSize(w, h) return iconSize def setPixmap(self, column, pixmap): """ Set the pixmap to be displayed in the given column. :type column: int :type pixmap: QtWidgets.QPixmap :rtype: None """ self._pixmap[column] = pixmap def thumbnailPath(self): """ Return the thumbnail path on disk. :rtype: None or str """ return "" def _thumbnailFromImage(self, image): """ Called after the given image object has finished loading. :type image: QtGui.QImage :rtype: None """ self.clearCache() pixmap = QtGui.QPixmap() pixmap.convertFromImage(image) icon = QtGui.QIcon(pixmap) self._thumbnailIcon = icon if self.itemsWidget(): self.itemsWidget().update() def thumbnailIcon(self): """ Return the thumbnail icon. :rtype: QtGui.QIcon """ thumbnailPath = self.thumbnailPath() if not os.path.exists(thumbnailPath): color = self.textColor() thumbnailPath = studiolibrary.resource().icon("thumbnail", color=color) if not self._thumbnailIcon: if self.ENABLE_THUMBNAIL_THREAD and not self._workerStarted: self._workerStarted = True self._worker.setPath(thumbnailPath) self.ThreadPool.start(self._worker) color = self.textColor() self._thumbnailIcon = studiolibrary.resource().icon( "thumbnail", color=color) else: self._thumbnailIcon = QtGui.QIcon(thumbnailPath) return self._thumbnailIcon def icon(self, column): """ Overriding the icon method to add support for the thumbnail icon. :type column: int :rtype: QtGui.QIcon """ icon = QtWidgets.QTreeWidgetItem.icon(self, column) if not icon and column == self.THUMBNAIL_COLUMN: icon = self.thumbnailIcon() return icon def pixmap(self, column): """ Return the pixmap for the given column. :type column: int :rtype: QtWidgets.QPixmap """ if not self._pixmap.get(column): icon = self.icon(column) if icon: size = QtCore.QSize(self.MAX_ICON_SIZE, self.MAX_ICON_SIZE) iconSize = icon.actualSize(size) self._pixmap[column] = icon.pixmap(iconSize) return self._pixmap.get(column) def padding(self): """ Return the padding/border size for the item. :rtype: int """ return self.itemsWidget().padding() def textHeight(self): """ Return the height of the text for the item. :rtype: int """ return self.itemsWidget().itemTextHeight() def isTextVisible(self): """ Return True if the text is visible. :rtype: bool """ return self.itemsWidget().isItemTextVisible() def textAlignment(self, column): """ Return the text alignment for the label in the given column. :type column: int :rtype: QtCore.Qt.AlignmentFlag """ if self.itemsWidget().isIconView(): return QtCore.Qt.AlignCenter else: return QtWidgets.QTreeWidgetItem.textAlignment(self, column) # ----------------------------------------------------------------------- # Support for mouse and key events # ----------------------------------------------------------------------- def underMouse(self): """Return True if the item is under the mouse cursor.""" return self._underMouse def contextMenu(self, menu): """ Return the context menu for the item. Reimplement in a subclass to return a custom context menu for the item. :rtype: QtWidgets.QMenu """ pass def dropEvent(self, event): """ Reimplement in a subclass to receive drop events for the item. :type event: QtWidgets.QDropEvent :rtype: None """ def mouseLeaveEvent(self, event): """ Reimplement in a subclass to receive mouse leave events for the item. :type event: QtWidgets.QMouseEvent :rtype: None """ self._underMouse = False self.stop() def mouseEnterEvent(self, event): """ Reimplement in a subclass to receive mouse enter events for the item. :type event: QtWidgets.QMouseEvent :rtype: None """ self._underMouse = True self.play() def mouseMoveEvent(self, event): """ Reimplement in a subclass to receive mouse move events for the item. :type event: QtWidgets.QMouseEvent :rtype: None """ self.blendingEvent(event) self.imageSequenceEvent(event) def mousePressEvent(self, event): """ Reimplement in a subclass to receive mouse press events for the item. :type event: QtWidgets.QMouseEvent :rtype: None """ if event.button() == QtCore.Qt.MidButton: self._blendPosition = event.pos() def mouseReleaseEvent(self, event): """ Reimplement in a subclass to receive mouse release events for the item. :type event: QtWidgets.QMouseEvent :rtype: None """ if self.isBlending(): self._blendPosition = None self._blendPreviousValue = self.blendValue() def keyPressEvent(self, event): """ Reimplement in a subclass to receive key press events for the item. :type event: QtWidgets.QKeyEvent :rtype: None """ pass def keyReleaseEvent(self, event): """ Reimplement in a subclass to receive key release events for the item. :type event: QtWidgets.QKeyEvent :rtype: None """ pass # ----------------------------------------------------------------------- # Support for custom painting # ----------------------------------------------------------------------- def textColor(self): """ Return the text color for the item. :rtype: QtWidgets.QtColor """ # This will be changed to use the palette soon. # Note: There were problems with older versions of Qt's palette (Maya 2014). # Eg: # return self.itemsWidget().palette().color(self.itemsWidget().foregroundRole()) return self.itemsWidget().textColor() def textSelectedColor(self): """ Return the selected text color for the item. :rtype: QtWidgets.QtColor """ return self.itemsWidget().textSelectedColor() def backgroundColor(self): """ Return the background color for the item. :rtype: QtWidgets.QtColor """ return self.itemsWidget().backgroundColor() def backgroundHoverColor(self): """ Return the background color when the mouse is over the item. :rtype: QtWidgets.QtColor """ return self.itemsWidget().backgroundHoverColor() def backgroundSelectedColor(self): """ Return the background color when the item is selected. :rtype: QtWidgets.QtColor """ return self.itemsWidget().backgroundSelectedColor() def rect(self): """ Return the rect for the current paint frame. :rtype: QtCore.QRect """ return self._rect def setRect(self, rect): """ Set the rect for the current paint frame. :type rect: QtCore.QRect :rtype: None """ self._rect = rect def visualRect(self, option): """ Return the visual rect for the item. :type option: QtWidgets.QStyleOptionViewItem :rtype: QtCore.QRect """ rect = QtCore.QRect(option.rect) return rect def paintRow(self, painter, option, index): """ Paint performs low-level painting for the item. :type painter: QtWidgets.QPainter :type option: QtWidgets.QStyleOptionViewItem :type index: QtCore.QModelIndex :rtype: None """ QtWidgets.QTreeWidget.drawRow(self.treeWidget(), painter, option, index) def paint(self, painter, option, index): """ Paint performs low-level painting for the item. :type painter: QtWidgets.QPainter :type option: QtWidgets.QStyleOptionViewItem :type index: QtCore.QModelIndex :rtype: None """ self.setRect(QtCore.QRect(option.rect)) painter.save() try: self.paintBackground(painter, option, index) if self.isTextVisible(): self.paintText(painter, option, index) self.paintIcon(painter, option, index) if index.column() == 0: if self.imageSequence(): self.paintPlayhead(painter, option) finally: painter.restore() def paintBackground(self, painter, option, index): """ Draw the background for the item. :type painter: QtWidgets.QPainter :type option: QtWidgets.QStyleOptionViewItem :type index: QtCore.QModelIndex :rtype: None """ isSelected = option.state & QtWidgets.QStyle.State_Selected isMouseOver = option.state & QtWidgets.QStyle.State_MouseOver painter.setPen(QtGui.QPen(QtCore.Qt.NoPen)) visualRect = self.visualRect(option) if isSelected: color = self.backgroundSelectedColor() painter.setBrush(QtGui.QBrush(color)) elif isMouseOver: color = self.backgroundHoverColor() painter.setBrush(QtGui.QBrush(color)) else: color = self.backgroundColor() painter.setBrush(QtGui.QBrush(color)) painter.drawRect(visualRect) def iconRect(self, option): """ Return the icon rect for the item. :type option: QtWidgets.QStyleOptionViewItem :rtype: QtCore.QRect """ padding = self.padding() rect = self.visualRect(option) width = rect.width() height = rect.height() if self.isTextVisible() and self.itemsWidget().isIconView(): height -= self.textHeight() width -= padding height -= padding rect.setWidth(width) rect.setHeight(height) x = 0 x += float(padding) / 2 x += float((width - rect.width())) / 2 y = float((height - rect.height())) / 2 y += float(padding) / 2 rect.translate(x, y) return rect def scalePixmap(self, pixmap, rect): """ Scale the given pixmap to the give rect size. This method will cache the scaled pixmap if called with the same size. :type pixmap: QtGui.QPixmap :type rect: QtCore.QRect :rtype: QtGui.QPixmap """ rectChanged = True if self._pixmapRect: widthChanged = self._pixmapRect.width() != rect.width() heightChanged = self._pixmapRect.height() != rect.height() rectChanged = widthChanged or heightChanged if not self._pixmapScaled or rectChanged: self._pixmapScaled = pixmap.scaled( rect.width(), rect.height(), QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation, ) self._pixmapRect = rect return self._pixmapScaled def paintIcon(self, painter, option, index, align=None): """ Draw the icon for the item. :type painter: QtWidgets.QPainter :type option: QtWidgets.QStyleOptionViewItem :rtype: None """ column = index.column() pixmap = self.pixmap(column) if not pixmap: return rect = self.iconRect(option) pixmap = self.scalePixmap(pixmap, rect) pixmapRect = QtCore.QRect(rect) pixmapRect.setWidth(pixmap.width()) pixmapRect.setHeight(pixmap.height()) align = QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter x, y = 0, 0 isAlignBottom = align == QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeft \ or align == QtCore.Qt.AlignBottom | QtCore.Qt.AlignHCenter \ or align == QtCore.Qt.AlignBottom | QtCore.Qt.AlignRight isAlignHCenter = align == QtCore.Qt.AlignHCenter \ or align == QtCore.Qt.AlignCenter \ or align == QtCore.Qt.AlignHCenter | QtCore.Qt.AlignBottom \ or align == QtCore.Qt.AlignHCenter | QtCore.Qt.AlignTop isAlignVCenter = align == QtCore.Qt.AlignVCenter \ or align == QtCore.Qt.AlignCenter \ or align == QtCore.Qt.AlignVCenter | QtCore.Qt.AlignLeft \ or align == QtCore.Qt.AlignVCenter | QtCore.Qt.AlignRight if isAlignHCenter: x += float(rect.width() - pixmap.width()) / 2 if isAlignVCenter: y += float(rect.height() - pixmap.height()) / 2 elif isAlignBottom: y += float(rect.height() - pixmap.height()) pixmapRect.translate(x, y) painter.drawPixmap(pixmapRect, pixmap) def drawIconBorder(self, painter, pixmapRect): """ Draw a border around the icon. :type painter: QtWidgets.QPainter :type pixmapRect: QtWidgets.QRect :rtype: None """ pixmapRect = QtCore.QRect(pixmapRect) pixmapRect.setX(pixmapRect.x() - 5) pixmapRect.setY(pixmapRect.y() - 5) pixmapRect.setWidth(pixmapRect.width() + 5) pixmapRect.setHeight(pixmapRect.height() + 5) color = QtGui.QColor(255, 255, 255, 10) painter.setPen(QtGui.QPen(QtCore.Qt.NoPen)) painter.setBrush(QtGui.QBrush(color)) painter.drawRect(pixmapRect) def fontSize(self): """ Return the font size for the item. :rtype: int """ return self.DEFAULT_FONT_SIZE def font(self, column): """ Return the font for the given column. :type column: int :rtype: QtWidgets.QFont """ default = QtWidgets.QTreeWidgetItem.font(self, column) font = self._fonts.get(column, default) font.setPixelSize(self.fontSize() * self.dpi()) return font def setFont(self, column, font): """ Set the font for the given column. :type column: int :type font: QtWidgets.QFont :rtype: Noen """ self._fonts[column] = font def paintText(self, painter, option, index): """ Draw the text for the item. :type painter: QtWidgets.QPainter :type option: QtWidgets.QStyleOptionViewItem :rtype: None """ column = index.column() if column == 0 and self.itemsWidget().isTableView(): return self._paintText(painter, option, column) def textWidth(self, column): text = self.text(column) font = self.font(column) metrics = QtGui.QFontMetricsF(font) textWidth = metrics.width(text) return textWidth def _paintText(self, painter, option, column): if self.itemsWidget().isIconView(): text = self.name() else: label = self.labelFromColumn(column) text = self.displayText(label) isSelected = option.state & QtWidgets.QStyle.State_Selected if isSelected: color = self.textSelectedColor() else: color = self.textColor() visualRect = self.visualRect(option) width = visualRect.width() height = visualRect.height() padding = self.padding() x = padding / 2 y = padding / 2 visualRect.translate(x, y) visualRect.setWidth(width - padding) visualRect.setHeight(height - padding) font = self.font(column) align = self.textAlignment(column) metrics = QtGui.QFontMetricsF(font) if text: textWidth = metrics.width(text) else: textWidth = 1 # # Check if the current text fits within the rect. if textWidth > visualRect.width() - padding: visualWidth = visualRect.width() text = metrics.elidedText(text, QtCore.Qt.ElideRight, visualWidth) align = QtCore.Qt.AlignLeft if self.itemsWidget().isIconView(): align = align | QtCore.Qt.AlignBottom else: align = align | QtCore.Qt.AlignVCenter pen = QtGui.QPen(color) painter.setPen(pen) painter.setFont(font) painter.drawText(visualRect, align, text) # ------------------------------------------------------------------------ # Support for middle mouse blending (slider) # ------------------------------------------------------------------------ def setBlendingEnabled(self, enabled): """ Set if middle mouse slider is enabled. :type enabled: bool :rtype: None """ self._blendingEnabled = enabled def isBlendingEnabled(self): """ Return true if middle mouse slider is enabled. :rtype: None """ return self._blendingEnabled def blendingEvent(self, event): """ Called when the mouse moves while the middle mouse button is held down. :param event: QtGui.QMouseEvent :rtype: None """ if self.isBlending(): value = (event.pos().x() - self.blendPosition().x()) / 1.5 value = math.ceil(value) + self.blendPreviousValue() try: self.setBlendValue(value) except Exception: self.stopBlending() def startBlendingEvent(self, event): """ Called when the middle mouse button is pressed. :param event: QtGui.QMouseEvent :rtype: None """ if self.isBlendingEnabled(): if event.button() == QtCore.Qt.MidButton: self._blendPosition = event.pos() def stopBlending(self): """ Called when the middle mouse button is released. :param event: QtGui.QMouseEvent :rtype: None """ self._blendPosition = None self._blendPreviousValue = self.blendValue() def resetBlending(self): """ Reset the blending value to zero. :rtype: None """ self._blendValue = 0.0 self._blendPreviousValue = 0.0 def isBlending(self): """ Return True if blending. :rtype: bool """ return self.blendPosition() is not None def setBlendValue(self, value): """ Set the blend value. :type value: float :rtype: bool """ if self.isBlendingEnabled(): self._blendValue = value self.blendChanged.emit(value) logger.debug("BLENDING:" + str(value)) def blendValue(self): """ Return the blend value. :rtype: float """ return self._blendValue def blendPreviousValue(self): """ :rtype: float """ return self._blendPreviousValue def blendPosition(self): """ :rtype: QtGui.QPoint """ return self._blendPosition # ------------------------------------------------------------------------ # Support animated image sequence # ------------------------------------------------------------------------ def imageSequenceEvent(self, event): """ :type event: QtCore.QEvent :rtype: None """ if self.imageSequence(): if studioqt.isControlModifier(): if self.rect(): x = event.pos().x() - self.rect().x() width = self.rect().width() percent = 1.0 - (float(width - x) / float(width)) frame = int(self.imageSequence().frameCount() * percent) self.imageSequence().jumpToFrame(frame) self.updateFrame() def resetImageSequence(self): self._imageSequence = None def imageSequence(self): """ :rtype: studioqt.ImageSequence """ return self._imageSequence def setImageSequence(self, value): """ :type value: studioqt.ImageSequence """ self._imageSequence = value def setImageSequencePath(self, path): """ :type path: str :rtype: None """ self._imageSequencePath = path def imageSequencePath(self): """ :rtype: str """ return self._imageSequencePath def stop(self): """ :rtype: None """ if self.imageSequence(): self.imageSequence().stop() def play(self): """ :rtype: None """ self.resetImageSequence() path = self.imageSequencePath() or self.thumbnailPath() movie = None if os.path.isfile(path) and path.lower().endswith(".gif"): movie = QtGui.QMovie(path) movie.setCacheMode(QtGui.QMovie.CacheAll) movie.frameChanged.connect(self._frameChanged) elif os.path.isdir(path): if not self.imageSequence(): movie = studioqt.ImageSequence(path) movie.frameChanged.connect(self._frameChanged) if movie: self.setImageSequence(movie) self.imageSequence().start() def _frameChanged(self, frame): """Triggered when the movie object updates to the given frame.""" if not studioqt.isControlModifier(): self.updateFrame() def updateFrame(self): """Triggered when the movie object updates the current frame.""" if self.imageSequence(): pixmap = self.imageSequence().currentPixmap() self.setIcon(0, pixmap) def playheadColor(self): """ Return the playhead color. :rtype: QtGui.Color """ return self.DEFAULT_PLAYHEAD_COLOR def paintPlayhead(self, painter, option): """ Paint the playhead if the item has an image sequence. :type painter: QtWidgets.QPainter :type option: QtWidgets.QStyleOptionViewItem :rtype: None """ imageSequence = self.imageSequence() if imageSequence and self.underMouse(): count = imageSequence.frameCount() current = imageSequence.currentFrameNumber() if count > 0: percent = float((count + current) + 1) / count - 1 else: percent = 0 r = self.iconRect(option) c = self.playheadColor() painter.setPen(QtCore.Qt.NoPen) painter.setBrush(QtGui.QBrush(c)) if percent <= 0: width = 0 elif percent >= 1: width = r.width() else: width = (percent * r.width()) - 1 height = 3 * self.dpi() y = r.y() + r.height() - (height - 1) painter.drawRect(r.x(), y, width, height)
class MayaDockWidgetMixin(object): DEFAULT_DOCK_AREA = "none" DEFAULT_DOCK_ALLOWED_AREAS = ["top", "bottom", "left", "right"] dockingChanged = QtCore.Signal() @staticmethod def generateUniqueObjectName(name, attempts=100): """ Generate a unique name for the dock widget. :type name: str :type attempts: int :rtype: str """ for i in range(1, attempts): uniqueName = name + str(i) controlExists = maya.cmds.control(uniqueName, exists=True) if not controlExists: return uniqueName msg = 'Cannot find unique window name for "{0}"' msg = msg.format(name) raise ValueError(msg) @staticmethod def dockAreaStrMap(): """ :rtype: dict """ return { 'none': QtCore.Qt.NoDockWidgetArea, 'top': QtCore.Qt.TopDockWidgetArea, 'left': QtCore.Qt.LeftDockWidgetArea, 'right': QtCore.Qt.RightDockWidgetArea, 'bottom': QtCore.Qt.BottomDockWidgetArea, 'all': QtCore.Qt.AllDockWidgetAreas } @staticmethod def dockAreaMap(): """ :rtype: dict """ return { QtCore.Qt.NoDockWidgetArea: 'none', QtCore.Qt.TopDockWidgetArea: 'top', QtCore.Qt.LeftDockWidgetArea: 'left', QtCore.Qt.RightDockWidgetArea: 'right', QtCore.Qt.BottomDockWidgetArea: 'bottom', QtCore.Qt.AllDockWidgetAreas: 'all' } def __init__(self, *args): self._dockWidgetName = None self._dockLayoutName = None def setObjectName(self, name): """ :type name: str """ try: name = self.generateUniqueObjectName(name) except NameError, msg: logger.exception(msg) QtWidgets.QWidget.setObjectName(self, name)
class SearchWidget(QtWidgets.QLineEdit): DEFAULT_PLACEHOLDER_TEXT = 'Search' searchChanged = QtCore.Signal() def __init__(self, *args): QtWidgets.QLineEdit.__init__(self, *args) self._iconPadding = 4 self._iconButton = QtWidgets.QPushButton(self) self._iconButton.clicked.connect(self._iconClicked) self._searchFilter = studioqt.SearchFilter('') icon = studioqt.icon('search') self.setIcon(icon) self.setPlaceholderText(self.DEFAULT_PLACEHOLDER_TEXT) self.textChanged.connect(self._textChanged) self.searchChanged = self.searchFilter().searchChanged self.update() def update(self): self.updateIconColor() def updateIconColor(self): color = self.palette().color(self.foregroundRole()) color = studioqt.Color.fromColor(color) self.setIconColor(color) def _textChanged(self, text): """ Triggered when the text changes. :type text: str :rtype: None """ self.searchFilter().setPattern(text) 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') 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) 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 _iconClicked(self): """ Triggered when the user clicks on the icon. :rtype: None """ if not self.hasFocus(): self.setFocus() 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) 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)
class WorkerSignals(QtCore.QObject): triggered = QtCore.Signal(object)
class FormWidget(QtWidgets.QFrame): accepted = QtCore.Signal(object) stateChanged = QtCore.Signal() validated = QtCore.Signal() def __init__(self, *args, **kwargs): super(FormWidget, self).__init__(*args, **kwargs) self._schema = [] self._widgets = [] self._validator = None layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) self.setLayout(layout) self._fieldsFrame = QtWidgets.QFrame(self) self._fieldsFrame.setObjectName("optionsFrame") layout = QtWidgets.QVBoxLayout(self._fieldsFrame) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) self._fieldsFrame.setLayout(layout) self._titleWidget = QtWidgets.QPushButton(self) self._titleWidget.setCheckable(True) self._titleWidget.setObjectName("titleWidget") self._titleWidget.toggled.connect(self._titleClicked) self._titleWidget.hide() self.layout().addWidget(self._titleWidget) self.layout().addWidget(self._fieldsFrame) def _titleClicked(self, toggle): """Triggered when the user clicks the title widget.""" self.setExpanded(toggle) self.stateChanged.emit() def titleWidget(self): """ Get the title widget. :rtype: QWidget """ return self._titleWidget def setTitle(self, title): """ Set the text for the title widget. :type title: str """ self.titleWidget().setText(title) def setExpanded(self, expand): """ Expands the options if expand is true, otherwise collapses the options. :type expand: bool """ self._titleWidget.blockSignals(True) try: self._titleWidget.setChecked(expand) self._fieldsFrame.setVisible(expand) finally: self._titleWidget.blockSignals(False) def isExpanded(self): """ Returns true if the item is expanded, otherwise returns false. :rtype: bool """ return self._titleWidget.isChecked() def setTitleVisible(self, visible): """ A convenience method for setting the title visible. :type visible: bool """ self.titleWidget().setVisible(visible) def reset(self): """Reset all option widgets back to their default value.""" for widget in self._widgets: widget.reset() self.validate() def readSettings(self): """ Return the local settings from the location of the SETTING_PATH. :rtype: dict """ path = studiolibrary.localPath("FormWidget.json") return studiolibrary.readJson(path) def saveSettings(self, data): """ Save the given dict to the local location of the SETTING_PATH. :type data: dict :rtype: None """ path = studiolibrary.localPath("FormWidget.json") studiolibrary.updateJson(path, data) def savePersistentValues(self): """ Triggered when the user changes the options. """ settings = self.readSettings() settings[self.__class__.__name__] = self.persistentValues() self.saveSettings(settings) def loadPersistentValues(self): """ Get the options from the user settings. :rtype: dict """ settings = self.readSettings() values = settings.get(self.__class__.__name__, {}) defaultValues = self.defaultValues() # Remove options from the user settings that are not persistent if values: for value in self.schema(): name = value.get("name") persistent = value.get("persistent") if not persistent and name in values: values[name] = defaultValues[name] self.setValues(values) def schema(self): """ Get the schema for the form. :rtype: dict """ return self._schema 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, formWidget=self) 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._fieldChanged, widget) widget.valueChanged.connect(callback) self._fieldsFrame.layout().addWidget(widget) self.loadPersistentValues() def _fieldChanged(self, widget): """ Triggered when the given option widget changes value. :type widget: FieldWidget """ self.validate() def accept(self): """Accept the current options""" self.emitAcceptedCallback() self.savePersistentValues() def closeEvent(self, event): """Called when the widget is closed.""" self.savePersistentValues() super(FormWidget, self).closeEvent(event) def 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 fields(self): """ Get all the field data for the form. :rtype: dict """ fields = [] for widget in self._widgets: fields.append(widget.data()) return fields def fieldWidgets(self): """ Get all the field widgets. :rtype: list[FieldWidget] """ return self._widgets def setValues(self, values): """ Set the field values for the current form. :type values: dict """ state = [] for name in values: state.append({"name": name, "value": values[name]}) self._setState(state) def values(self): """ Get the all the field values indexed by the field name. :rtype: dict """ values = {} for widget in self._widgets: name = widget.data().get("name") if name: values[name] = widget.value() return values def persistentValues(self): """ Get all the persistent field values for the current form. :rtype: dict """ values = {} for widget in self._widgets: name = widget.data().get("name") if name and widget.data().get("persistent"): values[name] = widget.value() return values def defaultValues(self): """ Get the all the default field values indexed by the field name. :rtype: dict """ values = {} for widget in self._widgets: name = widget.data().get("name") if name: values[name] = widget.default() return values def state(self): """ Get the current state. :rtype: dict """ fields = [] for widget in self._widgets: fields.append(widget.state()) state = {"fields": fields, "expanded": self.isExpanded()} return state def setState(self, state): """ Set the current state. :type state: dict """ expanded = state.get("expanded") if expanded is not None: self.setExpanded(expanded) fields = state.get("fields") if fields is not None: self._setState(fields) self.validate() def _setState(self, fields): """ Set the state while blocking all signals. :type fields: list[dict] """ for widget in self._widgets: widget.blockSignals(True) for widget in self._widgets: widget.setError("") for field in fields: if field.get("name") == widget.data().get("name"): widget.setData(field) for widget in self._widgets: widget.blockSignals(False) self.stateChanged.emit()
class SearchWidget(QtWidgets.QLineEdit): SPACE_OPERATOR = "and" PLACEHOLDER_TEXT = "Search" searchChanged = QtCore.Signal() def __init__(self, *args): QtWidgets.QLineEdit.__init__(self, *args) self._dataset = None self._spaceOperator = "and" self._iconPadding = 6 self._iconButton = QtWidgets.QPushButton(self) self._iconButton.clicked.connect(self._iconClicked) icon = studiolibrary.resource().icon("search") self.setIcon(icon) self._clearButton = QtWidgets.QPushButton(self) self._clearButton.setCursor(QtCore.Qt.ArrowCursor) icon = studiolibrary.resource().icon("cross") self._clearButton.setIcon(icon) self._clearButton.setToolTip("Clear all search text") self._clearButton.clicked.connect(self._clearClicked) self._clearButton.setStyleSheet("background-color: transparent;") self.setPlaceholderText(self.PLACEHOLDER_TEXT) self.textChanged.connect(self._textChanged) self.update() def update(self): self.updateIconColor() self.updateClearButton() def setDataset(self, dataset): """ Set the data set for the search widget: :type dataset: studiolibrary.Dataset """ self._dataset = dataset def dataset(self): """ Get the data set for the search widget. :rtype: studiolibrary.Dataset """ return self._dataset def _clearClicked(self): """ Triggered when the user clicks the cross icon. :rtype: None """ self.setText("") self.setFocus() def _iconClicked(self): """ Triggered when the user clicks on the icon. :rtype: None """ if not self.hasFocus(): self.setFocus() def _textChanged(self, text): """ Triggered when the text changes. :type text: str :rtype: None """ self.search() def search(self): """Run the search query on the data set.""" if self.dataset(): self.dataset().addQuery(self.query()) self.dataset().search() else: logger.info("No dataset found the the search widget.") self.updateClearButton() self.searchChanged.emit() def query(self): """ Get the query used for the data set. :rtype: dict """ text = str(self.text()) filters = [] for filter_ in text.split(' '): if filter_.split(): filters.append(('*', 'contains', filter_)) uniqueName = 'searchwidget' + str(id(self)) return { 'name': uniqueName, 'operator': self.spaceOperator(), 'filters': filters } def updateClearButton(self): """ Update the clear button depending on the current text. :rtype: None """ text = self.text() if text: self._clearButton.show() else: self._clearButton.hide() def contextMenuEvent(self, event): """ Triggered when the user right clicks on the search widget. :type event: QtCore.QEvent :rtype: None """ self.showContextMenu() def spaceOperator(self): """ Get the space operator for the search widget. :rtype: str """ return self._spaceOperator def setSpaceOperator(self, operator): """ Set the space operator for the search widget. :type operator: str """ self._spaceOperator = operator self.search() def createSpaceOperatorMenu(self, parent=None): """ Return the menu for changing the space operator. :type parent: QGui.QMenu :rtype: QGui.QMenu """ menu = QtWidgets.QMenu(parent) menu.setTitle("Space Operator") # Create the space operator for the OR operator action = QtWidgets.QAction(menu) action.setText("OR") action.setCheckable(True) callback = partial(self.setSpaceOperator, "or") action.triggered.connect(callback) if self.spaceOperator() == "or": action.setChecked(True) menu.addAction(action) # Create the space operator for the AND operator action = QtWidgets.QAction(menu) action.setText("AND") action.setCheckable(True) callback = partial(self.setSpaceOperator, "and") action.triggered.connect(callback) if self.spaceOperator() == "and": action.setChecked(True) menu.addAction(action) return menu def showContextMenu(self): """ Create and show the context menu for the search widget. :rtype QtWidgets.QAction """ menu = QtWidgets.QMenu(self) subMenu = self.createStandardContextMenu() subMenu.setTitle("Edit") menu.addMenu(subMenu) subMenu = self.createSpaceOperatorMenu(menu) menu.addMenu(subMenu) point = QtGui.QCursor.pos() action = menu.exec_(point) return action def setIcon(self, icon): """ Set the icon for the search widget. :type icon: QtWidgets.QIcon :rtype: None """ self._iconButton.setIcon(icon) def setIconColor(self, color): """ Set the icon color for the search widget icon. :type color: QtGui.QColor :rtype: None """ icon = self._iconButton.icon() icon = studioqt.Icon(icon) icon.setColor(color) self._iconButton.setIcon(icon) icon = self._clearButton.icon() icon = studioqt.Icon(icon) icon.setColor(color) self._clearButton.setIcon(icon) def updateIconColor(self): """ Update the icon colors to the current foregroundRole. :rtype: None """ color = self.palette().color(self.foregroundRole()) color = studioqt.Color.fromColor(color) self.setIconColor(color) def settings(self): """ Return a dictionary of the current widget state. :rtype: dict """ settings = { "text": self.text(), "spaceOperator": self.spaceOperator(), } return settings def setSettings(self, settings): """ Restore the widget state from a settings dictionary. :type settings: dict :rtype: None """ text = settings.get("text", "") self.setText(text) spaceOperator = settings.get("spaceOperator") if spaceOperator: self.setSpaceOperator(spaceOperator) def resizeEvent(self, event): """ Reimplemented so the icon maintains the same height as the widget. :type event: QtWidgets.QResizeEvent :rtype: None """ QtWidgets.QLineEdit.resizeEvent(self, event) self.setTextMargins(self.height(), 0, 0, 0) size = QtCore.QSize(self.height(), self.height()) self._iconButton.setIconSize(size) self._iconButton.setFixedSize(size) self._clearButton.setIconSize(size) x = self.width() - self.height() self._clearButton.setGeometry(x, 0, self.height(), self.height())
class SearchFilter(QtCore.QObject): searchChanged = QtCore.Signal() class Operator: OR = " or " AND = " and " def __init__(self, pattern, spaceOperator=Operator.AND): """ :type pattern: str :type spaceOperator: SearchFilter.Operator """ QtCore.QObject.__init__(self) self._matches = 0 self._pattern = None self._resolvedPattern = None self._spaceOperator = spaceOperator self.setPattern(pattern) def pattern(self): """ Return the pattern for the search filter. :rtype: str """ return self._pattern def setPattern(self, pattern): """ Set the pattern for the search filter. :type pattern: str """ self._pattern = pattern self._searchChanged() def _searchChanged(self): """ Triggered when the search filter changes. :rtype: None """ self.resolvePattern() self.searchChanged.emit() def resolvedPattern(self): """ Return the resolved pattern. :rtype: str """ return self._resolvedPattern def setResolvedPattern(self, resolvedPattern): """ Set the resolved pattern. :type resolvedPattern: str :rtype: None """ self._resolvedPattern = resolvedPattern def spaceOperator(self): """ Return the operator for all white spaces in the pattern. :rtype: SearchFilter.Operator """ return self._spaceOperator def setSpaceOperator(self, operator): """ Set the operator for all white spaces in the pattern. :type: SearchFilter.Operator """ self._spaceOperator = operator self._searchChanged() def settings(self): """ Return the state of the search filter as a dict object. :rtype: dict """ settings = {} settings["pattern"] = self.pattern() settings["spaceOperator"] = self.spaceOperator() return settings def setSettings(self, settings): """ Set the state of the search filter from a dict object. :type settings: dict :rtype: None """ pattern = settings.get("pattern", "") self.setPattern(pattern) spaceOperator = settings.get("spaceOperator", self.Operator.AND) self.setSpaceOperator(spaceOperator) def resolvePattern(self): """ Resolve the pattern to speed up the match method. :rtype: None """ pattern = self.pattern() spaceOperator = self.spaceOperator() pattern = pattern.strip() # Case-sensitive is not supported pattern = pattern.lower() # Remove all double spaces. pattern = re.sub(' +', ' ', pattern) # Replace all white spaces with the space operator pattern = pattern.replace(self.Operator.OR, "_OR_") pattern = pattern.replace(self.Operator.AND, "_AND_") pattern = pattern.replace(" ", spaceOperator) pattern = pattern.replace("_OR_", self.Operator.OR) pattern = pattern.replace("_AND_", self.Operator.AND) self.setResolvedPattern(pattern) def matches(self): """ Return the number of matches from the last match. :rtype: int """ return self._matches def match(self, text): """ Match the given text to the resolved pattern. :type text: str :rtype: bool """ match = False matches = 0 pattern = self.resolvedPattern() groups = pattern.split(self.Operator.OR) for group in groups: match = True labels = [ label.lower() for label in group.split(self.Operator.AND) ] for label in labels: if label not in text.lower(): matches += 1 match = False break matches += 1 if match: break matches += 1 if not match: matches = 0 self._matches = matches return match
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 = SeparatorAction("View Settings", menu) menu.addAction(action) action = SliderAction("Size", menu) action.slider().setMinimum(10) action.slider().setMaximum(200) action.slider().setValue(self.zoomAmount()) action.slider().valueChanged.connect(self.setZoomAmount) menu.addAction(action) action = SliderAction("Border", menu) action.slider().setMinimum(0) action.slider().setMaximum(20) action.slider().setValue(self.padding()) action.slider().valueChanged.connect(self.setPadding) menu.addAction(action) # action = SliderAction("Spacing", menu) action.slider().setMinimum(self.DEFAULT_MIN_SPACING) action.slider().setMaximum(self.DEFAULT_MAX_SPACING) action.slider().setValue(self.spacing()) action.slider().valueChanged.connect(self.setSpacing) menu.addAction(action) action = SeparatorAction("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 = SliderAction("Size", menu) action.slider().setMinimum(10) action.slider().setMaximum(200) action.slider().setValue(self.zoomAmount()) action.slider().valueChanged.connect(self.setZoomAmount) menu.addAction(action) action = SliderAction("Border", menu) action.slider().setMinimum(0) action.slider().setMaximum(20) action.slider().setValue(self.padding()) action.slider().valueChanged.connect(self.setPadding) menu.addAction(action) # action = SliderAction("Spacing", menu) action.slider().setMinimum(self.DEFAULT_MIN_SPACING) action.slider().setMaximum(self.DEFAULT_MAX_SPACING) action.slider().setValue(self.spacing()) action.slider().valueChanged.connect(self.setSpacing) menu.addAction(action) 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
class ImageSequence(QtCore.QObject): DEFAULT_FPS = 24 frameChanged = QtCore.Signal(int) def __init__(self, path, *args): QtCore.QObject.__init__(self, *args) self._fps = self.DEFAULT_FPS self._timer = None self._frame = 0 self._frames = [] self._dirname = None self._paused = False if path: self.setDirname(path) def setDirname(self, dirname): """ Set the location to the image sequence. :type dirname: str :rtype: None """ def naturalSortItems(items): """ Sort the given list in the way that humans expect. """ convert = lambda text: int(text) if text.isdigit() else text alphanum_key = lambda key: [ convert(c) for c in re.split('([0-9]+)', key) ] items.sort(key=alphanum_key) self._dirname = dirname if os.path.isdir(dirname): self._frames = [ dirname + "/" + filename for filename in os.listdir(dirname) ] naturalSortItems(self._frames) def dirname(self): """ Return the location to the image sequence. :rtype: str """ return self._dirname def reset(self): """ Stop and reset the current frame to 0. :rtype: None """ if not self._timer: self._timer = QtCore.QTimer(self.parent()) self._timer.setSingleShot(False) self.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._timer.stop() def start(self): """ Starts the movie. ImageSequence will enter Running state :rtype: None """ self.reset() if self._timer: self._timer.start(1000.0 / self._fps) def frames(self): """ Return all the filenames in the image sequence. :rtype: list[str] """ return self._frames def _frameChanged(self): """ Triggered when the current frame changes. :rtype: None """ if not self._frames: return frame = self._frame frame += 1 self.jumpToFrame(frame) def percent(self): """ Return the current frame position as a percentage. :rtype: None """ if len(self._frames) == self._frame + 1: _percent = 1 else: _percent = float( (len(self._frames) + self._frame)) / len(self._frames) - 1 return _percent def frameCount(self): """ Return the number of frames. :rtype: int """ return len(self._frames) def currentIcon(self): """ Returns the current frame as a QIcon. :rtype: QtGui.QIcon """ return QtGui.QIcon(self.currentFilename()) def currentPixmap(self): """ Return the current frame as a QPixmap. :rtype: QtGui.QPixmap """ return QtGui.QPixmap(self.currentFilename()) def currentFilename(self): """ Return the current file name. :rtype: str or None """ try: return self._frames[self.currentFrameNumber()] except IndexError: pass def currentFrameNumber(self): """ Return the current frame. :rtype: int or None """ return self._frame def jumpToFrame(self, frame): """ Set the current frame. :rtype: int or None """ if frame >= self.frameCount(): frame = 0 self._frame = frame self.frameChanged.emit(frame)
class LibraryItemSignals(QtCore.QObject): """""" saved = QtCore.Signal(object) saving = QtCore.Signal(object) loaded = QtCore.Signal(object)
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._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 [] for item in self.findItems(queries): value = item.itemData().get(field) results.setdefault(value, {'count': 0, 'name': value}) 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 sync(self): """Sync the file system with the database.""" if not self.path(): logger.info('No path set for syncing data') return 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, ) for item in items: path = item.path() itemData = data.get(path, {}) itemData.update(item.itemData()) data[path] = itemData self.postSync(data) 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, libraryWindow=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, libraryWindow=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 = [] logger.debug("Search queries:") for query in queries: logger.debug('Query: %s', query) items = self.createItems(libraryWindow=self._libraryWindow) 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 addQuery(self, query): """ Add the given query to the dataset. Examples: addQuery({ 'operator': 'or', 'filters': [ ('folder', 'is' '/library/proj/test'), ('folder', 'startswith', '/library/proj/test'), ] }) :type query: dict """ if query.get('name'): for i, query_ in enumerate(self._queries): if query_.get('name') == query.get('name'): self._queries[i] = query if query not in self._queries: self._queries.append(query) def removeQuery(self, name): """ Remove the query with the given name. :type name: str """ for query in self._queries: if query.get('name') == name: self._queries.remove(query) break def queryExists(self, name): """ Check if the given query name exists. :type name: str :rtype: bool """ for query in self._queries: if query.get('name') == name: return True return False 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 GroupBoxWidget(QtWidgets.QFrame): toggled = QtCore.Signal(bool) def __init__(self, title, widget, *args, **kwargs): super(GroupBoxWidget, self).__init__(*args, **kwargs) self._widget = None layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) self.setLayout(layout) self._titleWidget = QtWidgets.QPushButton(self) self._titleWidget.setCheckable(True) self._titleWidget.setText(title) self._titleWidget.setObjectName("title") self._titleWidget.toggled.connect(self._toggle) self.layout().addWidget(self._titleWidget) self._widgetFrame = QtWidgets.QFrame(self) self._widgetFrame.setObjectName("frame") layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) self._widgetFrame.setLayout(layout) self.layout().addWidget(self._widgetFrame) if widget: self.setWidget(widget) def setText(self, text): """ Set the text to be displayed for group box. :type text: str """ self._titleWidget.setText(text) def text(self): """ Get the title for the group box. :rtype: str """ return self._titleWidget.text() def setWidget(self, widget): """ Set the widget to hide when the user clicks the title. :type widget: QWidgets.QWidget """ self._widget = widget self._widget.setParent(self._widgetFrame) self._widgetFrame.layout().addWidget(self._widget) def _toggle(self, visible): """ Triggered when the user clicks the title. :type visible: bool """ self.toggled.emit(visible) def isChecked(self): """ Check the checked state for the group box. :rtype: bool """ return self._titleWidget.isChecked() def setChecked(self, checked): """ Overriding this method to hide the widget when the state changes. :type checked: bool """ self._titleWidget.setChecked(checked) if self._widget: self._widget.setVisible(checked)
class ThumbnailCaptureMenu(QtWidgets.QMenu): captured = QtCore.Signal(str) def __init__(self, path, force=False, parent=None): """ Thumbnail capture menu. :type path: str :type force: bool :type parent: None or QtWidgets.QWidget """ QtWidgets.QMenu.__init__(self, parent) self._path = path self._force = force changeImageAction = QtWidgets.QAction('Capture new image', self) changeImageAction.triggered.connect(self.capture) self.addAction(changeImageAction) changeImageAction = QtWidgets.QAction('Show Capture window', self) changeImageAction.triggered.connect(self.showCaptureWindow) self.addAction(changeImageAction) loadImageAction = QtWidgets.QAction('Load image from disk', self) loadImageAction.triggered.connect(self.showLoadImageDialog) self.addAction(loadImageAction) def path(self): """ Return the thumbnail path on disc. :rtype: str """ return self._path def showWarningDialog(self): """Show a warning dialog for overriding the previous thumbnail.""" title = "Override Thumbnail" text = u"This action will delete the previous thumbnail. The " \ u"previous image cannot be backed up. Do you want to " \ u"confirm the action to take a new image and delete " \ u"the previous one?" clickedButton = studiolibrary.widgets.MessageBox.warning( self.parent(), title=title, text=text, enableDontShowCheckBox=True, ) if clickedButton != QtWidgets.QDialogButtonBox.StandardButton.Yes: raise Exception("Dialog was canceled!") def showCaptureWindow(self): """Show the capture window for framing.""" self.capture(show=True) def capture(self, show=False): """ Capture an image from the Maya viewport. :type show: bool """ if not self._force and os.path.exists(self.path()): self.showWarningDialog() mutils.gui.thumbnailCapture(show=show, path=self.path(), captured=self.captured.emit) def showLoadImageDialog(self): """Show a file dialog for choosing an image from disc.""" if not self._force and os.path.exists(self.path()): self.showWarningDialog() fileDialog = QtWidgets.QFileDialog( self, caption="Open Image", filter="Image Files (*.png *.jpg *.bmp)") fileDialog.fileSelected.connect(self._fileSelected) fileDialog.exec_() def _fileSelected(self, path): """ Triggered when the file dialog is accepted. :type path: str """ shutil.copy(path, self.path()) self.captured.emit(self.path())
class SettingsDialog(QtWidgets.QDialog): DEFAULT_ACCENT_COLOR = QtGui.QColor(20, 175, 250) DEFAULT_BACKGROUND_COLOR = QtGui.QColor(70, 70, 80) DEFAULT_ACCENT_COLORS = [ QtGui.QColor(230, 75, 75), QtGui.QColor(235, 100, 70), QtGui.QColor(240, 125, 100), QtGui.QColor(240, 190, 40), QtGui.QColor(80, 200, 140), QtGui.QColor(20, 175, 250), QtGui.QColor(110, 110, 240), ] DEFAULT_BACKGROUND_COLORS = [ QtGui.QColor(70, 70, 80), QtGui.QColor(65, 65, 75), QtGui.QColor(55, 55, 65), QtGui.QColor(50, 50, 57), QtGui.QColor(40, 40, 47), ] accentColorChanged = QtCore.Signal(object) backgroundColorChanged = QtCore.Signal(object) def __init__(self, parent=None): """ :type parent: QtWidgets.QWidget :type library: studiolibrary.Library """ QtWidgets.QDialog.__init__(self, parent) studioqt.loadUi(self) self._validator = None self._accentColor = self.DEFAULT_ACCENT_COLOR self._backgroundColor = self.DEFAULT_BACKGROUND_COLOR resource = studiolibrary.resource() self.setWindowIcon(resource.icon("icon_black")) windowTitle = "Studio Library - {version}" windowTitle = windowTitle.format(version=studiolibrary.version()) self.setWindowTitle(windowTitle) self.createAccentColorBar() self.createBackgroundColorBar() self.ui.acceptButton.clicked.connect(self.accept) self.ui.rejectButton.clicked.connect(self.close) self.ui.browsePathButton.clicked.connect(self.browsePath) self.updateStyleSheet() self.center() def resizeEvent(self, event): """ Reimplemented to support the logo scaling on DPI screens. :type event: QtGui.QEvent :rtype: None """ scaleFactor = 1.4 height = self.ui.headerFrame.height() self.ui.logo.setFixedWidth(height / scaleFactor) self.ui.logo.setFixedHeight(height / scaleFactor) def _accentColorChanged(self, color): """ Triggered when the user clicks/changes the accent color. :type color: studioqt.Color :rtype: None """ self.setAccentColor(color) def _backgroundColorClicked(self, color): """ Triggered when the user clicks/changes the background color. :type color: studioqt.Color :rtype: None """ self.setBackgroundColor(color) def createAccentColorBar(self): """ Create and setup the accent color bar. :rtype: None """ browserColors_ = [ # Top row, Bottom row (230, 60, 60), (250, 80, 130), (255, 90, 40), (240, 100, 170), (255, 125, 100), (240, 200, 150), (250, 200, 0), (225, 200, 40), (80, 200, 140), (80, 225, 120), (50, 180, 240), (100, 200, 245), (130, 110, 240), (180, 160, 255), (180, 110, 240), (210, 110, 255), ] browserColors = [] for colorR, colorG, colorB in browserColors_: for i in range(0, 3): colorR = colorR if colorR > 0 else 0 colorG = colorG if colorG > 0 else 0 colorB = colorB if colorB > 0 else 0 color = QtGui.QColor(colorR, colorG, colorB).rgba() browserColors.append(color) colorR -= 20 colorB -= 20 colorG -= 20 hColorBar = studioqt.HColorBar() hColorBar.setColors(self.DEFAULT_ACCENT_COLORS) hColorBar.setCurrentColor(self.DEFAULT_ACCENT_COLOR) hColorBar.setBrowserColors(browserColors) hColorBar.colorChanged.connect(self._accentColorChanged) self.ui.accentColorBarFrame.layout().addWidget(hColorBar) def createBackgroundColorBar(self): """ Create and setup the background color bar. :rtype: None """ browserColors_ = [ (0, 0, 0), (20, 20, 30), (0, 30, 60), (0, 60, 60), (0, 60, 30), (60, 0, 10), (60, 0, 40), (40, 15, 5), ] browserColors = [] for colorR, colorG, colorB in browserColors_: for i in range(0, 6): color = QtGui.QColor(colorR, colorG, colorB).rgba() browserColors.append(color) colorR += 20 colorB += 20 colorG += 20 hColorBar = studioqt.HColorBar() hColorBar.setColors(self.DEFAULT_BACKGROUND_COLORS) hColorBar.setCurrentColor(self.DEFAULT_BACKGROUND_COLOR) hColorBar.setBrowserColors(browserColors) hColorBar.colorChanged.connect(self._backgroundColorClicked) self.ui.backgroundColorBarFrame.layout().addWidget(hColorBar) def accept(self): """ Hides the modal dialog and sets the result code to Accepted. :rtype: None """ self.validate() QtWidgets.QDialog.accept(self) def setValidator(self, validator): """ Set the validator for the dialog. :type validator: func :rtype: None """ self._validator = validator def validator(self): """ Return the validator for the dialog. :rtype: func """ return self._validator def validate(self): """ Run the validate. :rtype: None """ try: validator = self.validator() if validator: validator() except Exception, e: QtWidgets.QMessageBox.critical(self, "Validate Error", str(e)) raise
class PoseItemSignals(QtCore.QObject): """""" mirrorChanged = QtCore.Signal(bool)
class GlobalSignals(QtCore.QObject): """""" blendChanged = QtCore.Signal(float)
def run(self): if studiolibrary.package().isUpdateAvailable(): self.emit(QtCore.SIGNAL('updateAvailable()'))
class CaptureDialog(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(path, modelPanel, startFrame, endFrame, width, height, step=step) self.accept() self.captured.emit(self._capturedPath) return self._capturedPath
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.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 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()
def url(self): """Used by the mime data when dragging/dropping the item.""" return QtCore.QUrl("file:///" + self.path())
class FormWidget(QtWidgets.QFrame): accepted = QtCore.Signal(object) stateChanged = 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(not 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(not 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._optionsFrame.isVisible() 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.stateChanged.emit() self.validate() def accept(self): """Accept the current options""" self.emitAcceptedCallback() def closeEvent(self, event): super(FormWidget, self).closeEvent(event) 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: state = self._validator(**self.values()) if state is not None: self._setState(state) 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, state): """ Set the state. :type state: list[dict] """ for widget in self._widgets: widget.blockSignals(True) for widget in self._widgets: widget.setError("") for data in state: if data.get("name") == widget.data().get("name"): widget.setData(data) for widget in self._widgets: widget.blockSignals(False)
class SidebarWidget(QtWidgets.QTreeWidget): itemDropped = QtCore.Signal(object) itemRenamed = QtCore.Signal(str, str) itemSelectionChanged = QtCore.Signal() def __init__(self, *args): super(SidebarWidget, self).__init__(*args) self._dpi = 1 self._items = [] self._locked = False self._dataset = None self._recursive = True self.itemExpanded.connect(self.update) self.itemCollapsed.connect(self.update) self.setDpi(1) self.setAcceptDrops(True) self.setHeaderHidden(True) self.setFrameShape(QtWidgets.QFrame.NoFrame) self.setSelectionMode(QtWidgets.QTreeWidget.ExtendedSelection) self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) def selectionChanged(self, *args): """Triggered the current selection has changed.""" self.search() def setRecursive(self, enable): """ Set the search query on the dataset to be recursive. :type enable: bool """ self._recursive = enable self.search() def isRecursive(self): """ Get the recursive query enable state. :rtype: bool """ return self._recursive def _dataChanged(self): """Triggered when the data set has changed.""" pass # paths = collections.OrderedDict() # # for value in self.dataset().values(self.field(), self.sortBy()): # paths[value] = collections.OrderedDict() # # if paths: # root = findRoot(paths.keys(), separator=self.separator()) # self.setPaths(paths, root=root) def setDataset(self, dataset): """ Set the dataset for the search widget: :type dataset: studioqt.Dataset """ self._dataset = dataset self._dataset.dataChanged.connect(self._dataChanged) self._dataChanged() def dataset(self): """ Get the dataset for the search widget. :rtype: studioqt.Dataset """ return self._dataset def search(self): """Run the dataset search.""" if self.dataset(): self.dataset().addQuery(self.query()) self.dataset().search() else: logger.info('No dataset found for the sidebar widget.') def query(self): """ Get the query for the sidebar widget. :rtype: dict """ filters = [] if self.isRecursive(): condition = 'startswith' else: condition = 'is' for path in self.selectedPaths(): filter_ = ('folder', condition, path) filters.append(filter_) uniqueName = 'sidebarwidget_' + str(id(self)) return {'name': uniqueName, 'operator': 'or', 'filters': filters} def setLocked(self, locked): """ Set the widget items to read only mode. :type locked: bool :rtype: None """ self._locked = locked def isLocked(self): """ Return True if the items are in read only mode :rtype: bool """ return self._locked def itemAt(self, pos): """ :type pos: QtGui.QPoint :rtype: None or Folder """ index = self.indexAt(pos) if not index.isValid(): return item = self.itemFromIndex(index) return item def dropEvent(self, event): """ :type event: QtCore.QEvent :rtype: None """ if self.isLocked(): logger.debug("Folder is locked! Cannot accept drop!") return self.itemDropped.emit(event) def dragMoveEvent(self, event): """ :type event: QtCore.QEvent :rtype: None """ mimeData = event.mimeData() if mimeData.hasUrls(): event.accept() else: event.ignore() item = self.itemAt(event.pos()) if item: self.selectPaths([item.path()]) def dragEnterEvent(self, event): """ :type event: QtCore.QEvent :rtype: None """ event.accept() def selectItem(self, item): """ :type item: NavigationWidgetItem :rtype: None """ self.selectPaths([item.path()]) def dpi(self): """ Return the dots per inch multiplier. :rtype: float """ return self._dpi def setDpi(self, dpi): """ Set the dots per inch multiplier. :type dpi: float :rtype: None """ size = 24 * dpi self.setIndentation(15 * dpi) self.setMinimumWidth(35 * dpi) self.setIconSize(QtCore.QSize(size, size)) self.setStyleSheet("height: {size}".format(size=size)) def update(self, *args): """ :rtype: None """ for item in self.items(): item.update() def items(self): """ Return a list of all the items in the tree widget. :rtype: list[NavigationWidgetItem] """ items = self.findItems( "*", QtCore.Qt.MatchWildcard | QtCore.Qt.MatchRecursive) return items def itemFromUrl(self, url): """ Return the item for the given url. :type url: QtCore.QUrl :rtype: NavigationWidgetItem """ for item in self.items(): if url == item.url(): return item def itemFromPath(self, path): """ Return the item for the given path. :type path: str :rtype: NavigationWidgetItem """ for item in self.items(): if path == item.path(): return item def settings(self): """ Return a dictionary of the settings for this widget. :rtype: dict """ settings = {} scrollBar = self.verticalScrollBar() settings["verticalScrollBar"] = {"value": scrollBar.value()} scrollBar = self.horizontalScrollBar() settings["horizontalScrollBar"] = {"value": scrollBar.value()} for item in self.items(): itemSettings = item.settings() if itemSettings: settings[item.path()] = item.settings() return settings def setSettings(self, settings): """ Set the settings for this widget :type settings: dict """ for path in sorted(settings.keys()): s = settings.get(path, None) self.setPathSettings(path, s) scrollBarSettings = settings.get("verticalScrollBar", {}) value = scrollBarSettings.get("value", None) if value: self.verticalScrollBar().setValue(value) scrollBarSettings = settings.get("horizontalScrollBar", {}) value = scrollBarSettings.get("value", None) if value: self.horizontalScrollBar().setValue(value) self.setIndentation(18 * self.dpi()) def setPathSettings(self, path, settings): """ Show the context menu at the given position. :type path: str :type settings: dict :rtype: None """ item = self.itemFromPath(path) if item and settings: item.setSettings(settings) def showContextMenu(self, position): """ Show the context menu at the given position. :type position: QtCore.QPoint :rtype: None """ menu = self.createContextMenu() menu.exec_(self.viewport().mapToGlobal(position)) def expandedItems(self): """ Return all the expanded items. :rtype: list[NavigationWidgetItem] """ for item in self.items(): if self.isItemExpanded(item): yield item def expandedPaths(self): """ Return all the expanded paths. :rtype: list[NavigationWidgetItem] """ for item in self.expandedItems(): yield item.url() def setExpandedPaths(self, paths): """ Set the given paths to expanded. :type paths: list[str] """ for item in self.items(): if item.url() in paths: item.setExpanded(True) def selectedItem(self): """ Return the last selected item :rtype: SidebarWidgetItem """ path = self.selectedPath() return self.itemFromPath(path) def selectedPath(self): """ Return the last selected path :rtype: str or None """ paths = self.selectedPaths() if paths: return paths[-1] def selectedPaths(self): """ Return the paths that are selected. :rtype: list[str] """ paths = [] items = self.selectedItems() for item in items: path = item.path() paths.append(path) return paths def selectPath(self, path): """ Select the given path :type: str :rtype: None """ self.selectPaths([path]) def selectPaths(self, paths): """ Select the items with the given paths. :type paths: list[str] :rtype: None """ paths = self.normPaths(paths) items = self.items() for item in items: if item.path() in paths: item.setSelected(True) else: item.setSelected(False) def selectUrl(self, url): """ Select the item with the given url. :type url: str :rtype: None """ items = self.items() for item in items: if item.url() == url: item.setSelected(True) else: item.setSelected(False) def selectedUrls(self): """ Return the urls for the selected items. :rtype: list[str] """ urls = [] items = self.selectedItems() for item in items: urls.append(item.url()) return urls def normPaths(self, paths): return [path.replace("\\", "/") for path in paths] def setPaths(self, paths, root="", split=None): """ Set the items to the given items. :type paths: list[str] :type root: str :rtype: None """ settings = self.settings() self.blockSignals(True) self.clear() self.addPaths(paths, root=root, split=split) self.setSettings(settings) self.blockSignals(False) def addPaths(self, paths, root="", split=None): """ Set the given items as a flat list. :type paths: list[str] :type root: str or None :type split: str or None :rtype: None """ data = pathsToDict(paths, root=root, split=split) self.createItems(data, split=split) if isinstance(paths, dict): self.setSettings(paths) def createItems(self, data, split=None): """ Create the items from the given data dict :type data: dict :type split: str or None :rtype: None """ split = split or SPLIT_TOKEN for key in data: path = split.join([key]) item = SidebarWidgetItem(self) item.setText(0, unicode(key)) item.setPath(path) def _recursive(parent, children, split=None): for text, val in sorted(children.iteritems()): path = parent.path() path = split.join([path, text]) child = SidebarWidgetItem() child.setText(0, unicode(text)) child.setPath(path) parent.addChild(child) _recursive(child, val, split=split) _recursive(item, data[key], split=split) self.update()
class OptionWidget(QtWidgets.QFrame): """The base widget for all option widgets. Examples: option = { 'name': 'startFrame', 'type': 'int' 'value': 1, } optionWidget = OptionWidget(option) """ valueChanged = QtCore.Signal() def __init__(self, *args, **kwargs): super(OptionWidget, self).__init__(*args, **kwargs) self._option = {} self._widget = None self._default = None self._required = None layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) self.setLayout(layout) self.setContentsMargins(0, 0, 0, 0) self._label = QtWidgets.QLabel(self) self._label.setObjectName('label') self._label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) layout.addWidget(self._label) def label(self): """ Get the label widget. :rtype: QtWidgets.QLabel """ return self._label def option(self): """ Get the option data for the widget. :rtype: dict """ return self._option def setOption(self, state): """ Set the current state of the option widget using a dictionary. :type state: dict """ self._option.update(state) state = self._option self.blockSignals(True) items = state.get('items') if items is not None: self.setItems(items) value = state.get('value') default = state.get('default') or value # Must set the default before value if default is not None: self.setDefault(default) if value is not None: self.setValue(value) 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) label = state.get('label') if label is None: label = state.get('name') if label is not None: self.setText(label) self.refresh() self.blockSignals(False) def setError(self, message): """ Set the error message to be displayed for the options widget. :type message: str """ error = True if message else False if error: self.setToolTip(message) else: self.setToolTip(self.option().get('annotation')) self.setProperty('error', error) self.setStyleSheet(self.styleSheet()) def setText(self, text): """ Set the label text for the option. :type text: str """ if text: text = toTitle(text) if self.isRequired(): text += '*' self._label.setText(text) def setValue(self, value): """ Set the value of the option 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 option widget. :rtype: object """ raise NotImplementedError('The method "value" needs to be implemented') def setItems(self, items): """ Set the items for the options widget. :type items: list[str] """ raise NotImplementedError( 'The method "setItems" needs to be implemented') def reset(self): """Reset the option widget back to the defaults.""" self.setState(self._option) def setRequired(self, required): """ Set True if a value is required for this option. :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 option widget. :rtype: bool """ return bool(self._required) def setDefault(self, default): """ Set the default value for the option widget. :type default: object """ self._default = default def default(self): """ Get the default value for the option 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 option value. :type widget: QtWidgets.QWidget """ self._widget = widget self._widget.setObjectName('widget') self.layout().addWidget(self._widget) def widget(self, ): """ Get the widget used to set and get the option value. :rtype: QtWidgets.QWidget """ return self._widget def refresh(self): """Refresh the style properties.""" self.setProperty('default', self.isDefault()) self.setStyleSheet(self.styleSheet())