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 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 createPixmap(self, path, color): """ Create a new Pixmap from the given path. :type path: str :type color: str or QtCore.QColor :rtype: QtCore.QPixmap """ dpi = self.treeWidget().dpi() key = path + color + "DPI-" + str(dpi) pixmap = self.PIXMAP_CACHE.get(key) if not pixmap: width = 20 * dpi height = 18 * dpi if "/" not in path and "\\" not in path: path = studiolibrary.resource.get("icons", path) if not os.path.exists(path): path = self.defaultIconPath() pixmap2 = studioqt.Pixmap(path) pixmap2.setColor(color) pixmap2 = pixmap2.scaled(16 * dpi, 16 * dpi, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation) x = (width - pixmap2.width()) / 2 y = (height - pixmap2.height()) / 2 pixmap = QtGui.QPixmap(QtCore.QSize(width, height)) pixmap.fill(QtCore.Qt.transparent) painter = QtGui.QPainter(pixmap) painter.drawPixmap(x, y, pixmap2) painter.end() self.PIXMAP_CACHE[key] = pixmap return pixmap
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 """ self.setRect(QtCore.QRect(option.rect)) painter.save() try: self.paintBackground(painter, option, index) if self.isTextVisible(): self._paintText(painter, option, 1) finally: painter.restore()
def createColorDialog( self, parent, standardColors=None, currentColor=None, ): """ Create a new instance of the color dialog. :type parent: QtWidgets.QWidget :type standardColors: list[int] :rtype: QtWidgets.QColorDialog """ dialog = QtWidgets.QColorDialog(parent) if standardColors: index = -1 for colorR, colorG, colorB in standardColors: index += 1 color = QtGui.QColor(colorR, colorG, colorB).rgba() try: # Support for new qt5 signature color = QtGui.QColor(color) dialog.setStandardColor(index, color) except: # Support for new qt4 signature color = QtGui.QColor(color).rgba() dialog.setStandardColor(index, color) # PySide2 doesn't support d.open(), so we need to pass a blank slot. dialog.open(self, QtCore.SLOT("blankSlot()")) if currentColor: dialog.setCurrentColor(currentColor) return dialog
def fadeOut(widget, duration=200, onFinished=None): """ Fade out the given widget using the opacity effect. :type widget: QtWidget.QWidgets :type duration: int :type onFinished: func :rtype: QtCore.QPropertyAnimation """ widget._fadeOutEffect_ = QtWidgets.QGraphicsOpacityEffect() widget.setGraphicsEffect(widget._fadeOutEffect_) animation = QtCore.QPropertyAnimation(widget._fadeOutEffect_, b"opacity") animation.setDuration(duration) animation.setStartValue(1.0) animation.setEndValue(0.0) animation.setEasingCurve(QtCore.QEasingCurve.InOutCubic) animation.start() if onFinished: animation.finished.connect(onFinished) widget._fadeOut_ = animation return animation
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 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 ThumbnailCaptureDialog(QtWidgets.QDialog): DEFAULT_WIDTH = 250 DEFAULT_HEIGHT = 250 captured = QtCore.Signal(str) capturing = QtCore.Signal(str) def __init__(self, path="", parent=None, startFrame=None, endFrame=None, step=1): """ :type path: str :type parent: QtWidgets.QWidget :type startFrame: int :type endFrame: int :type step: int """ parent = parent or mutils.gui.mayaWindow() QtWidgets.QDialog.__init__(self, parent) self._path = path self._step = step self._endFrame = None self._startFrame = None self._capturedPath = None if endFrame is None: endFrame = int(maya.cmds.currentTime(query=True)) if startFrame is None: startFrame = int(maya.cmds.currentTime(query=True)) self.setEndFrame(endFrame) self.setStartFrame(startFrame) self.setObjectName("CaptureWindow") self.setWindowTitle("Capture Window") self._captureButton = QtWidgets.QPushButton("Capture") self._captureButton.clicked.connect(self.capture) self._modelPanelWidget = mutils.gui.modelpanelwidget.ModelPanelWidget( self) vbox = QtWidgets.QVBoxLayout(self) vbox.setObjectName(self.objectName() + "Layout") vbox.addWidget(self._modelPanelWidget) vbox.addWidget(self._captureButton) self.setLayout(vbox) width = (self.DEFAULT_WIDTH * 1.5) height = (self.DEFAULT_HEIGHT * 1.5) + 50 self.setWidthHeight(width, height) self.centerWindow() def path(self): """ Return the destination path. :rtype: str """ return self._path def setPath(self, path): """ Set the destination path. :type path: str """ self._path = path def endFrame(self): """ Return the end frame of the playblast. :rtype: int """ return self._endFrame def setEndFrame(self, endFrame): """ Specify the end frame of the playblast. :type endFrame: int """ self._endFrame = int(endFrame) def startFrame(self): """ Return the start frame of the playblast. :rtype: int """ return self._startFrame def setStartFrame(self, startFrame): """ Specify the start frame of the playblast. :type startFrame: int """ self._startFrame = int(startFrame) def step(self): """ Return the step amount of the playblast. Example: if step is set to 2 it will playblast every second frame. :rtype: int """ return self._step def setStep(self, step): """ Set the step amount of the playblast. :type step: int """ self._step = step def setWidthHeight(self, width, height): """ Set the width and height of the window. :type width: int :type height: int :rtype: None """ x = self.geometry().x() y = self.geometry().y() self.setGeometry(x, y, width, height) def centerWindow(self): """ Center the widget to the center of the desktop. :rtype: None """ geometry = self.frameGeometry() pos = QtWidgets.QApplication.desktop().cursor().pos() screen = QtWidgets.QApplication.desktop().screenNumber(pos) centerPoint = QtWidgets.QApplication.desktop().screenGeometry( screen).center() geometry.moveCenter(centerPoint) self.move(geometry.topLeft()) def capturedPath(self): """ Return the location of the captured playblast. :rtype: """ return self._capturedPath def capture(self): """ Capture a playblast and save it to the given path. :rtype: None """ path = self.path() self.capturing.emit(path) modelPanel = self._modelPanelWidget.name() startFrame = self.startFrame() endFrame = self.endFrame() step = self.step() width = self.DEFAULT_WIDTH height = self.DEFAULT_HEIGHT self._capturedPath = mutils.playblast.playblast( path, modelPanel, startFrame, endFrame, width, height, step=step, ) self.accept() self.captured.emit(self._capturedPath) return self._capturedPath
class IconPickerWidget(QtWidgets.QFrame): BUTTON_CLASS = IconButton iconChanged = QtCore.Signal(object) def __init__(self, *args): QtWidgets.QFrame.__init__(self, *args) self._buttons = [] self._currentIcon = None self._menuButton = None layout = QtWidgets.QGridLayout() layout.setSpacing(0) layout.setContentsMargins(0, 0, 0, 0) self.setLayout(layout) def enterEvent(self, event): """ Overriding this method to fix a bug with custom actions. :type event: QtCore.QEvent """ if self.parent(): menu = self.parent().parent() if isinstance(menu, QtWidgets.QMenu): menu.setActiveAction(None) def _iconChanged(self, iconPath): """ Triggered when the user clicks or browses for a color. :type iconPath: str :rtype: None """ self.setCurrentIcon(iconPath) self.iconChanged.emit(iconPath) def menuButton(self): """ Get the menu button used for browsing for custom colors. :rtype: QtGui.QWidget """ return self._menuButton def deleteButtons(self): """ Delete all the color buttons. :rtype: None """ layout = self.layout() while layout.count(): item = layout.takeAt(0) item.widget().deleteLater() def currentIcon(self): """ Return the current color. :rtype: studioqt.Color """ return self._currentIcon def setCurrentIcon(self, color): """ Set the current color. :type color: studioqt.Color """ self._currentIcon = color self.refresh() def refresh(self): """Update the current state of the selected color.""" for button in self._buttons: button.setChecked(button.iconPath() == self.currentIcon()) def setIcons(self, icons): """ Set the colors for the color bar. :type icons: list[str] or list[studioqt.Icon] """ self.deleteButtons() i = 0 first = True last = False positions = [(i, j) for i in range(5) for j in range(5)] for position, iconPath in zip(positions, icons): i += 1 if i == len(icons) - 1: last = True callback = partial(self._iconChanged, iconPath) button = self.BUTTON_CLASS(self) button.setIconPath(iconPath) button.setIconSize(QtCore.QSize(16, 16)) button.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) button.setProperty("first", first) button.setProperty("last", last) button.clicked.connect(callback) self.layout().addWidget(button, *position) self._buttons.append(button) first = False self._menuButton = QtWidgets.QPushButton("...", self) self._menuButton.setObjectName('menuButton') self._menuButton.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) self._menuButton.clicked.connect(self.browseColor) self.layout().addWidget(self._menuButton) self.refresh() @QtCore.Slot() def blankSlot(self): """Blank slot to fix an issue with PySide2.QColorDialog.open()""" pass def browseColor(self): """ Show the color dialog. :rtype: None """ pass
class GroupBoxWidget(QtWidgets.QFrame): toggled = QtCore.Signal(bool) def __init__(self, title, widget, persistent=False, *args, **kwargs): super(GroupBoxWidget, self).__init__(*args, **kwargs) self._widget = None self._persistent = None layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) self.setLayout(layout) self._titleWidget = QtWidgets.QPushButton(self) self._titleWidget.setCheckable(True) self._titleWidget.setText(title) self._titleWidget.setObjectName("title") self._titleWidget.toggled.connect(self._toggled) on_path = studiolibrary.resource.get("icons", "caret-down.svg") off_path = studiolibrary.resource.get("icons", "caret-right.svg") icon = studioqt.Icon.fa(on_path, color="rgb(255,255,255,200)", off=off_path) self._titleWidget.setIcon(icon) self.layout().addWidget(self._titleWidget) self._widgetFrame = QtWidgets.QFrame(self) self._widgetFrame.setObjectName("frame") layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) self._widgetFrame.setLayout(layout) self.layout().addWidget(self._widgetFrame) if widget: self.setWidget(widget) self.setPersistent(persistent) def setPersistent(self, persistent): """ Save and load the state of the widget to disk. :type persistent: bool """ self._persistent = persistent self.loadSettings() def title(self): """ Get the title for the group box. :rtype: str """ return self._titleWidget.text() def setWidget(self, widget): """ Set the widget to hide when the user clicks the title. :type widget: QWidgets.QWidget """ self._widget = widget # self._widget.setParent(self._widgetFrame) # self._widgetFrame.layout().addWidget(self._widget) def _toggled(self, visible): """ Triggered when the user clicks the title. :type visible: bool """ self.saveSettings() self.setChecked(visible) self.toggled.emit(visible) def isChecked(self): """ Check the checked state for the group box. :rtype: bool """ return self._titleWidget.isChecked() def setChecked(self, checked): """ Overriding this method to hide the widget when the state changes. :type checked: bool """ self._titleWidget.setChecked(checked) if self._widget: self._widget.setVisible(checked) def saveSettings(self): """Save the state to disc.""" if self._persistent: if not self.objectName(): raise NameError("No object name set for persistent widget.") data = {self.objectName(): {"checked": self.isChecked()}} settings.save(data) def loadSettings(self): """Load the state to disc.""" if self._persistent: if not self.objectName(): raise NameError("No object name set for persistent widget.") data = settings.read() data = data.get(self.objectName(), {}) if isinstance(data, dict): checked = data.get("checked", True) self.setChecked(checked)
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 SearchWidget(QtWidgets.QLineEdit): SPACE_OPERATOR = "and" PLACEHOLDER_TEXT = "Search" searchChanged = QtCore.Signal() def __init__(self, *args): QtWidgets.QLineEdit.__init__(self, *args) self._dataset = None self._spaceOperator = "and" self._iconButton = QtWidgets.QPushButton(self) self._iconButton.clicked.connect(self._iconClicked) icon = studiolibrary.resource.icon("search") self.setIcon(icon) self._clearButton = QtWidgets.QPushButton(self) self._clearButton.setCursor(QtCore.Qt.ArrowCursor) icon = studiolibrary.resource.icon("cross") self._clearButton.setIcon(icon) self._clearButton.setToolTip("Clear all search text") self._clearButton.clicked.connect(self._clearClicked) self._clearButton.setStyleSheet("background-color: transparent;") self.setPlaceholderText(self.PLACEHOLDER_TEXT) self.textChanged.connect(self._textChanged) self.update() def update(self): self.updateIconColor() self.updateClearButton() def setDataset(self, dataset): """ Set the data set for the search widget: :type dataset: studiolibrary.Dataset """ self._dataset = dataset def dataset(self): """ Get the data set for the search widget. :rtype: studiolibrary.Dataset """ return self._dataset def _clearClicked(self): """ Triggered when the user clicks the cross icon. :rtype: None """ self.setText("") self.setFocus() def _iconClicked(self): """ Triggered when the user clicks on the icon. :rtype: None """ if not self.hasFocus(): self.setFocus() def _textChanged(self, text): """ Triggered when the text changes. :type text: str :rtype: None """ self.search() def search(self): """Run the search query on the data set.""" if self.dataset(): self.dataset().addQuery(self.query()) self.dataset().search() else: logger.info("No dataset found the the search widget.") self.updateClearButton() self.searchChanged.emit() def query(self): """ Get the query used for the data set. :rtype: dict """ text = str(self.text()) filters = [] for filter_ in text.split(' '): if filter_.split(): filters.append(('*', 'contains', filter_)) uniqueName = 'searchwidget' + str(id(self)) return { 'name': uniqueName, 'operator': self.spaceOperator(), 'filters': filters } def updateClearButton(self): """ Update the clear button depending on the current text. :rtype: None """ text = self.text() if text: self._clearButton.show() else: self._clearButton.hide() def contextMenuEvent(self, event): """ Triggered when the user right clicks on the search widget. :type event: QtCore.QEvent :rtype: None """ self.showContextMenu() def spaceOperator(self): """ Get the space operator for the search widget. :rtype: str """ return self._spaceOperator def setSpaceOperator(self, operator): """ Set the space operator for the search widget. :type operator: str """ self._spaceOperator = operator self.search() def createSpaceOperatorMenu(self, parent=None): """ Return the menu for changing the space operator. :type parent: QGui.QMenu :rtype: QGui.QMenu """ menu = QtWidgets.QMenu(parent) menu.setTitle("Space Operator") # Create the space operator for the OR operator action = QtWidgets.QAction(menu) action.setText("OR") action.setCheckable(True) callback = partial(self.setSpaceOperator, "or") action.triggered.connect(callback) if self.spaceOperator() == "or": action.setChecked(True) menu.addAction(action) # Create the space operator for the AND operator action = QtWidgets.QAction(menu) action.setText("AND") action.setCheckable(True) callback = partial(self.setSpaceOperator, "and") action.triggered.connect(callback) if self.spaceOperator() == "and": action.setChecked(True) menu.addAction(action) return menu def showContextMenu(self): """ Create and show the context menu for the search widget. :rtype QtWidgets.QAction """ menu = QtWidgets.QMenu(self) subMenu = self.createStandardContextMenu() subMenu.setTitle("Edit") menu.addMenu(subMenu) subMenu = self.createSpaceOperatorMenu(menu) menu.addMenu(subMenu) point = QtGui.QCursor.pos() action = menu.exec_(point) return action def setIcon(self, icon): """ Set the icon for the search widget. :type icon: QtWidgets.QIcon :rtype: None """ self._iconButton.setIcon(icon) def setIconColor(self, color): """ Set the icon color for the search widget icon. :type color: QtGui.QColor :rtype: None """ icon = self._iconButton.icon() icon = studioqt.Icon(icon) icon.setColor(color) self._iconButton.setIcon(icon) icon = self._clearButton.icon() icon = studioqt.Icon(icon) icon.setColor(color) self._clearButton.setIcon(icon) def updateIconColor(self): """ Update the icon colors to the current foregroundRole. :rtype: None """ color = self.palette().color(self.foregroundRole()) color = studioqt.Color.fromColor(color) self.setIconColor(color) def settings(self): """ Return a dictionary of the current widget state. :rtype: dict """ settings = { "text": self.text(), "spaceOperator": self.spaceOperator(), } return settings def setSettings(self, settings): """ Restore the widget state from a settings dictionary. :type settings: dict :rtype: None """ text = settings.get("text", "") self.setText(text) spaceOperator = settings.get("spaceOperator") if spaceOperator: self.setSpaceOperator(spaceOperator) def resizeEvent(self, event): """ Reimplemented so the icon maintains the same height as the widget. :type event: QtWidgets.QResizeEvent :rtype: None """ QtWidgets.QLineEdit.resizeEvent(self, event) self.setTextMargins(self.height(), 0, 0, 0) size = QtCore.QSize(self.height(), self.height()) self._iconButton.setIconSize(size) self._iconButton.setFixedSize(size) self._clearButton.setIconSize(size) x = self.width() - self.height() self._clearButton.setGeometry(x, 0, self.height(), self.height())
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 Theme(QtCore.QObject): updated = QtCore.Signal() DEFAULT_DARK_COLOR = QtGui.QColor(60, 60, 60) DEFAULT_LIGHT_COLOR = QtGui.QColor(220, 220, 220) DEFAULT_ACCENT_COLOR = QtGui.QColor(0, 175, 255) DEFAULT_BACKGROUND_COLOR = QtGui.QColor(60, 60, 80) def __init__(self): QtCore.QObject.__init__(self) self._dpi = 1 self._name = "Default" self._accentColor = None self._backgroundColor = None self.setAccentColor(self.DEFAULT_ACCENT_COLOR) self.setBackgroundColor(self.DEFAULT_BACKGROUND_COLOR) def settings(self): """ Return a dictionary of settings for the current Theme. :rtype: dict """ settings = {} settings["name"] = self.name() accentColor = self.accentColor() settings["accentColor"] = accentColor.toString() backgroundColor = self.backgroundColor() settings["backgroundColor"] = backgroundColor.toString() return settings def setSettings(self, settings): """ Set a dictionary of settings for the current Theme. :type settings: dict :rtype: None """ name = settings.get("name") self.setName(name) color = settings.get("accentColor") if color: color = studioqt.Color.fromString(color) self.setAccentColor(color) color = settings.get("backgroundColor") if color: color = studioqt.Color.fromString(color) self.setBackgroundColor(color) def dpi(self): """ Return the dpi for the Theme :rtype: float """ return self._dpi def setDpi(self, dpi): """ Set the dpi for the Theme :type dpi: float :rtype: None """ self._dpi = dpi def name(self): """ Return the name for the Theme :rtype: str """ return self._name def setName(self, name): """ Set the name for the Theme :type name: str :rtype: None """ self._name = name def isDark(self): """ Return True if the current theme is dark. rtype: bool """ # The luminance for digital formats are (0.299, 0.587, 0.114) red = self.backgroundColor().redF() * 0.299 green = self.backgroundColor().greenF() * 0.587 blue = self.backgroundColor().blueF() * 0.114 darkness = red + green + blue if darkness < 0.6: return True return False def setDark(self): """ Set the current theme to the default dark color. :rtype: None """ self.setBackgroundColor(self.DEFAULT_DARK_COLOR) def setLight(self): """ Set the current theme to the default light color. :rtype: None """ self.setBackgroundColor(self.DEFAULT_LIGHT_COLOR) def iconColor(self): """ Return the icon color for the theme. :rtype: studioqt.Color """ return self.forgroundColor() def accentForgroundColor(self): """ Return the foreground color for the accent color. :rtype: studioqt.Color """ return studioqt.Color(255, 255, 255, 255) def forgroundColor(self): """ Return the foreground color for the theme. :rtype: studioqt.Color """ if self.isDark(): return studioqt.Color(250, 250, 250, 225) else: return studioqt.Color(0, 40, 80, 180) def itemBackgroundColor(self): """ Return the item background color. :rtype: studioqt.Color """ if self.isDark(): return studioqt.Color(255, 255, 255, 20) else: return studioqt.Color(255, 255, 255, 120) def itemBackgroundHoverColor(self): """ Return the item background color when the mouse hovers over the item. :rtype: studioqt.Color """ return studioqt.Color(255, 255, 255, 60) def accentColor(self): """ Return the accent color for the theme. :rtype: studioqt.Color or None """ return self._accentColor def backgroundColor(self): """ Return the background color for the theme. :rtype: studioqt.Color or None """ return self._backgroundColor def setAccentColor(self, color): """ Set the accent color for the theme. :type color: studioqt.Color | QtGui.QColor """ if isinstance(color, basestring): color = studioqt.Color.fromString(color) if isinstance(color, QtGui.QColor): color = studioqt.Color.fromColor(color) self._accentColor = color self.updated.emit() def setBackgroundColor(self, color): """ Set the background color for the theme. :type color: studioqt.Color | QtGui.QColor """ if isinstance(color, basestring): color = studioqt.Color.fromString(color) if isinstance(color, QtGui.QColor): color = studioqt.Color.fromColor(color) self._backgroundColor = color self.updated.emit() def createColorDialog( self, parent, standardColors=None, currentColor=None, ): """ Create a new instance of the color dialog. :type parent: QtWidgets.QWidget :type standardColors: list[int] :rtype: QtWidgets.QColorDialog """ dialog = QtWidgets.QColorDialog(parent) if standardColors: index = -1 for colorR, colorG, colorB in standardColors: index += 1 color = QtGui.QColor(colorR, colorG, colorB).rgba() try: # Support for new qt5 signature color = QtGui.QColor(color) dialog.setStandardColor(index, color) except: # Support for new qt4 signature color = QtGui.QColor(color).rgba() dialog.setStandardColor(index, color) # PySide2 doesn't support d.open(), so we need to pass a blank slot. dialog.open(self, QtCore.SLOT("blankSlot()")) if currentColor: dialog.setCurrentColor(currentColor) return dialog def browseAccentColor(self, parent=None): """ Show the color dialog for changing the accent color. :type parent: QtWidgets.QWidget :rtype: None """ standardColors = [(230, 60, 60), (210, 40, 40), (190, 20, 20), (250, 80, 130), (230, 60, 110), (210, 40, 90), (255, 90, 40), (235, 70, 20), (215, 50, 0), (240, 100, 170), (220, 80, 150), (200, 60, 130), (255, 125, 100), (235, 105, 80), (215, 85, 60), (240, 200, 150), (220, 180, 130), (200, 160, 110), (250, 200, 0), (230, 180, 0), (210, 160, 0), (225, 200, 40), (205, 180, 20), (185, 160, 0), (80, 200, 140), (60, 180, 120), (40, 160, 100), (80, 225, 120), (60, 205, 100), (40, 185, 80), (50, 180, 240), (30, 160, 220), (10, 140, 200), (100, 200, 245), (80, 180, 225), (60, 160, 205), (130, 110, 240), (110, 90, 220), (90, 70, 200), (180, 160, 255), (160, 140, 235), (140, 120, 215), (180, 110, 240), (160, 90, 220), (140, 70, 200), (210, 110, 255), (190, 90, 235), (170, 70, 215)] currentColor = self.accentColor() dialog = self.createColorDialog(parent, standardColors, currentColor) dialog.currentColorChanged.connect(self.setAccentColor) if dialog.exec_(): self.setAccentColor(dialog.selectedColor()) else: self.setAccentColor(currentColor) def browseBackgroundColor(self, parent=None): """ Show the color dialog for changing the background color. :type parent: QtWidgets.QWidget :rtype: None """ standardColors = [(0, 0, 0), (20, 20, 20), (40, 40, 40), (60, 60, 60), (80, 80, 80), (100, 100, 100), (20, 20, 30), (40, 40, 50), (60, 60, 70), (80, 80, 90), (100, 100, 110), (120, 120, 130), (0, 30, 60), (20, 50, 80), (40, 70, 100), (60, 90, 120), (80, 110, 140), (100, 130, 160), (0, 60, 60), (20, 80, 80), (40, 100, 100), (60, 120, 120), (80, 140, 140), (100, 160, 160), (0, 60, 30), (20, 80, 50), (40, 100, 70), (60, 120, 90), (80, 140, 110), (100, 160, 130), (60, 0, 10), (80, 20, 30), (100, 40, 50), (120, 60, 70), (140, 80, 90), (160, 100, 110), (60, 0, 40), (80, 20, 60), (100, 40, 80), (120, 60, 100), (140, 80, 120), (160, 100, 140), (40, 15, 5), (60, 35, 25), (80, 55, 45), (100, 75, 65), (120, 95, 85), (140, 115, 105)] currentColor = self.backgroundColor() dialog = self.createColorDialog(parent, standardColors, currentColor) dialog.currentColorChanged.connect(self.setBackgroundColor) if dialog.exec_(): self.setBackgroundColor(dialog.selectedColor()) else: self.setBackgroundColor(currentColor) def options(self): """ Return the variables used to customise the style sheet. :rtype: dict """ accentColor = self.accentColor() accentForegroundColor = self.accentForgroundColor() foregroundColor = self.forgroundColor() backgroundColor = self.backgroundColor() itemBackgroundColor = self.itemBackgroundColor() itemBackgroundHoverColor = self.itemBackgroundHoverColor() if self.isDark(): darkness = "white" else: darkness = "black" resourceDirname = studiolibrary.resource.RESOURCE_DIRNAME resourceDirname = resourceDirname.replace("\\", "/") options = { "DARKNESS": darkness, "RESOURCE_DIRNAME": resourceDirname, "ACCENT_COLOR": accentColor.toString(), "ACCENT_COLOR_R": str(accentColor.red()), "ACCENT_COLOR_G": str(accentColor.green()), "ACCENT_COLOR_B": str(accentColor.blue()), "ACCENT_FOREGROUND_COLOR": accentForegroundColor.toString(), "FOREGROUND_COLOR": foregroundColor.toString(), "FOREGROUND_COLOR_R": str(foregroundColor.red()), "FOREGROUND_COLOR_G": str(foregroundColor.green()), "FOREGROUND_COLOR_B": str(foregroundColor.blue()), "BACKGROUND_COLOR": backgroundColor.toString(), "BACKGROUND_COLOR_R": str(backgroundColor.red()), "BACKGROUND_COLOR_G": str(backgroundColor.green()), "BACKGROUND_COLOR_B": str(backgroundColor.blue()), "ITEM_TEXT_COLOR": foregroundColor.toString(), "ITEM_TEXT_SELECTED_COLOR": accentForegroundColor.toString(), "ITEM_BACKGROUND_COLOR": itemBackgroundColor.toString(), "ITEM_BACKGROUND_HOVER_COLOR": itemBackgroundHoverColor.toString(), "ITEM_BACKGROUND_SELECTED_COLOR": accentColor.toString(), } return options def styleSheet(self): """ Return the style sheet for this theme. :rtype: str """ options = self.options() path = studiolibrary.resource.get("css", "default.css") styleSheet = studioqt.StyleSheet.fromPath(path, options=options, dpi=self.dpi()) return styleSheet.data()
class FormWidget(QtWidgets.QFrame): accepted = QtCore.Signal(object) stateChanged = QtCore.Signal() validated = QtCore.Signal() def __init__(self, *args, **kwargs): super(FormWidget, self).__init__(*args, **kwargs) self._schema = [] self._widgets = [] self._validator = None layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) self.setLayout(layout) self._fieldsFrame = QtWidgets.QFrame(self) self._fieldsFrame.setObjectName("optionsFrame") layout = QtWidgets.QVBoxLayout(self._fieldsFrame) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) self._fieldsFrame.setLayout(layout) self._titleWidget = QtWidgets.QPushButton(self) self._titleWidget.setCheckable(True) self._titleWidget.setObjectName("titleWidget") self._titleWidget.toggled.connect(self._titleClicked) self._titleWidget.hide() self.layout().addWidget(self._titleWidget) self.layout().addWidget(self._fieldsFrame) def _titleClicked(self, toggle): """Triggered when the user clicks the title widget.""" self.setExpanded(toggle) self.stateChanged.emit() def titleWidget(self): """ Get the title widget. :rtype: QWidget """ return self._titleWidget def setTitle(self, title): """ Set the text for the title widget. :type title: str """ self.titleWidget().setText(title) def setExpanded(self, expand): """ Expands the options if expand is true, otherwise collapses the options. :type expand: bool """ self._titleWidget.blockSignals(True) try: self._titleWidget.setChecked(expand) self._fieldsFrame.setVisible(expand) finally: self._titleWidget.blockSignals(False) def isExpanded(self): """ Returns true if the item is expanded, otherwise returns false. :rtype: bool """ return self._titleWidget.isChecked() def setTitleVisible(self, visible): """ A convenience method for setting the title visible. :type visible: bool """ self.titleWidget().setVisible(visible) def reset(self): """Reset all option widgets back to their default value.""" for widget in self._widgets: widget.reset() self.validate() def savePersistentValues(self): """ Triggered when the user changes the options. """ data = {} for widget in self._widgets: name = widget.data().get("name") if name and widget.data().get("persistent"): key = self.objectName() or "FormWidget" key = widget.data().get("persistentKey", key) data.setdefault(key, {}) data[key][name] = widget.value() for key in data: settings.set(key, data[key]) def loadPersistentValues(self): """ Get the options from the user settings. :rtype: dict """ values = {} defaultValues = self.defaultValues() for field in self.schema(): name = field.get("name") persistent = field.get("persistent") if persistent: key = self.objectName() or "FormWidget" key = field.get("persistentKey", key) value = settings.get(key, {}).get(name) else: value = defaultValues.get(name) if value: values[name] = value self.setValues(values) def schema(self): """ Get the schema for the form. :rtype: dict """ return self._schema def _sortSchema(self, schema): """ Sort the schema depending on the group order. :type schema: list[dict] :rtype: list[dict] """ order = 0 for i, field in enumerate(schema): if field.get("type") == "group": order = field.get("order", order) field["order"] = order def _key(field): return field["order"] return sorted(schema, key=_key) def setSchema(self, schema, layout=None, errorsVisible=False): """ Set the schema for the widget. :type schema: list[dict] :type layout: None or str :type errorsVisible: bool """ self._schema = self._sortSchema(schema) for field in self._schema: cls = FIELD_WIDGET_REGISTRY.get(field.get("type", "label")) if not cls: logger.warning("Cannot find widget for %s", field) continue if layout and not field.get("layout"): field["layout"] = layout errorVisible = field.get("errorVisible") if errorVisible is not None: field["errorVisible"] = errorVisible else: field["errorVisible"] = errorsVisible widget = cls(data=field, parent=self._fieldsFrame, formWidget=self) data_ = widget.defaultData() data_.update(field) widget.setData(data_) value = field.get("value") default = field.get("default") if value is None and default is not None: widget.setValue(default) self._widgets.append(widget) callback = functools.partial(self._fieldChanged, widget) widget.valueChanged.connect(callback) self._fieldsFrame.layout().addWidget(widget) self.loadPersistentValues() def _fieldChanged(self, widget): """ Triggered when the given option widget changes value. :type widget: FieldWidget """ self.validate(widget=widget) def accept(self): """Accept the current options""" self.emitAcceptedCallback() self.savePersistentValues() def closeEvent(self, event): """Called when the widget is closed.""" self.savePersistentValues() super(FormWidget, self).closeEvent(event) def errors(self): """ Get all the errors. :rtype: list[str] """ errors = [] for widget in self._widgets: error = widget.data().get("error") if error: errors.append(error) return errors def hasErrors(self): """ Return True if the form contains any errors. :rtype: bool """ return bool(self.errors()) def setValidator(self, validator): """ Set the validator for the options. :type validator: func """ self._validator = validator def validator(self): """ Return the validator for the form. :rtype: func """ return self._validator def validate(self, widget=None): """Validate the current options using the validator.""" if self._validator: logger.debug("Running validator: form.validate(widget=%s)", widget) values = {} for name, value in self.values().items(): data = self.widget(name).data() if data.get("validate", True): values[name] = value if widget: values["fieldChanged"] = widget.name() fields = self._validator(**values) if fields is not None: self._setState(fields) self.validated.emit() else: logger.debug("No validator set.") def setData(self, name, data): """ Set the data for the given field name. :type name: str :type data: dict """ widget = self.widget(name) widget.setData(data) def setValue(self, name, value): """ Set the value for the given field name and value :type name: str :type value: object """ widget = self.widget(name) widget.setValue(value) def value(self, name): """ Get the value for the given widget name. :type name: str :rtype: object """ widget = self.widget(name) return widget.value() def widget(self, name): """ Get the widget for the given widget name. :type name: str :rtype: FieldWidget """ for widget in self._widgets: if widget.data().get("name") == name: return widget def fields(self): """ Get all the field data for the form. :rtype: dict """ fields = [] for widget in self._widgets: fields.append(widget.data()) return fields def fieldWidgets(self): """ Get all the field widgets. :rtype: list[FieldWidget] """ return self._widgets def setValues(self, values): """ Set the field values for the current form. :type values: dict """ state = [] for name in values: state.append({"name": name, "value": values[name]}) self._setState(state) def values(self): """ Get the all the field values indexed by the field name. :rtype: dict """ values = {} for widget in self._widgets: name = widget.data().get("name") if name: values[name] = widget.value() return values def defaultValues(self): """ Get the all the default field values indexed by the field name. :rtype: dict """ values = {} for widget in self._widgets: name = widget.data().get("name") if name: values[name] = widget.default() return values def state(self): """ Get the current state. :rtype: dict """ fields = [] for widget in self._widgets: fields.append(widget.state()) state = {"fields": fields, "expanded": self.isExpanded()} return state def setState(self, state): """ Set the current state. :type state: dict """ expanded = state.get("expanded") if expanded is not None: self.setExpanded(expanded) fields = state.get("fields") if fields is not None: self._setState(fields) self.validate() def _setState(self, fields): """ Set the state while blocking all signals. :type fields: list[dict] """ for widget in self._widgets: widget.blockSignals(True) for widget in self._widgets: widget.setError("") for field in fields: if field.get("name") == widget.data().get("name"): widget.setData(field) for widget in self._widgets: widget.blockSignals(False) self.stateChanged.emit()
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 align = align | QtCore.Qt.AlignVCenter rect = QtCore.QRect(visualRect) if self.itemsWidget().isIconView(): if self.isLabelOverItem() or self.isLabelUnderItem(): padding = 8 if padding < 8 else padding height = metrics.height() + (padding / 2) y = (rect.y() + rect.height()) - height rect.setY(y) rect.setHeight(height) if self.isLabelOverItem(): color2 = self.itemsWidget().backgroundColor().toRgb() color2.setAlpha(200) painter.setPen(QtCore.Qt.NoPen) painter.setBrush(QtGui.QBrush(color2)) painter.drawRect(rect) pen = QtGui.QPen(color) painter.setPen(pen) painter.setFont(font) painter.drawText(rect, align, text)
class FormDialog(QtWidgets.QFrame): accepted = QtCore.Signal(object) rejected = QtCore.Signal(object) def __init__(self, parent=None, form=None): super(FormDialog, self).__init__(parent) layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) self.setLayout(layout) self._widgets = [] self._validator = None self._title = QtWidgets.QLabel(self) self._title.setObjectName('title') self._title.setText('FORM') self.layout().addWidget(self._title) self._description = QtWidgets.QLabel(self) self._description.setObjectName('description') self.layout().addWidget(self._description) self._formWidget = FormWidget(self) self._formWidget.setObjectName("formWidget") self._formWidget.validated.connect(self._validated) self.layout().addWidget(self._formWidget) self.layout().addStretch(1) buttonLayout = QtWidgets.QHBoxLayout(self) buttonLayout.setContentsMargins(0, 0, 0, 0) buttonLayout.setSpacing(0) self.layout().addLayout(buttonLayout) buttonLayout.addStretch(1) self._acceptButton = QtWidgets.QPushButton(self) self._acceptButton.setObjectName('acceptButton') self._acceptButton.setText('Submit') self._acceptButton.clicked.connect(self.accept) self._rejectButton = QtWidgets.QPushButton(self) self._rejectButton.setObjectName('rejectButton') self._rejectButton.setText('Cancel') self._rejectButton.clicked.connect(self.reject) buttonLayout.addWidget(self._acceptButton) buttonLayout.addWidget(self._rejectButton) if form: self.setSettings(form) # buttonLayout.addStretch(1) def _validated(self): """Triggered when the form has been validated""" self._acceptButton.setEnabled(not self._formWidget.hasErrors()) def acceptButton(self): """ Return the accept button. :rtype: QWidgets.QPushButton """ return self._acceptButton def rejectButton(self): """ Return the reject button. :rtype: QWidgets.QPushButton """ return self._rejectButton def validateAccepted(self, **kwargs): """ Triggered when the accept button has been clicked. :type kwargs: The values of the fields """ self._formWidget.validator()(**kwargs) def validateRejected(self, **kwargs): """ Triggered when the reject button has been clicked. :type kwargs: The default values of the fields """ self._formWidget.validator()(**kwargs) def setSettings(self, settings): self._settings = settings title = settings.get("title") if title is not None: self._title.setText(title) callback = settings.get("accepted") if not callback: self._settings["accepted"] = self.validateAccepted callback = settings.get("rejected") if not callback: self._settings["rejected"] = self.validateRejected description = settings.get("description") if description is not None: self._description.setText(description) validator = settings.get("validator") if validator is not None: self._formWidget.setValidator(validator) layout = settings.get("layout") schema = settings.get("schema") if schema is not None: self._formWidget.setSchema(schema, layout=layout) def accept(self): """Call this method to accept the dialog.""" callback = self._settings.get("accepted") if callback: callback(**self._formWidget.values()) self.close() def reject(self): """Call this method to rejected the dialog.""" callback = self._settings.get("rejected") if callback: callback(**self._formWidget.defaultValues()) self.close()
class Library(QtCore.QObject): Fields = [ "icon", "name", "path", "type", "folder", "category", # "modified" ] SortFields = [ "name", "path", "type", "folder", "category", "Custom Order", # legacy case "modified", ] GroupFields = [ "type", "category", # "modified", ] dataChanged = QtCore.Signal() searchStarted = QtCore.Signal() searchFinished = QtCore.Signal() searchTimeFinished = QtCore.Signal() def __init__(self, path=None, libraryWindow=None, *args): QtCore.QObject.__init__(self, *args) self._path = path self._mtime = None self._data = {} self._items = [] self._fields = [] self._sortBy = [] self._groupBy = [] self._results = [] self._queries = {} self._globalQueries = {} self._groupedResults = {} self._searchTime = 0 self._searchEnabled = True self._libraryWindow = libraryWindow self.setPath(path) self.setDirty(True) def sortBy(self): """ Get the list of fields to sort by. :rtype: list[str] """ return self._sortBy def setSortBy(self, fields): """ Set the list of fields to group by. Example: library.setSortBy(["name:asc", "type:asc"]) :type fields: list[str] """ self._sortBy = fields def groupBy(self): """ Get the list of fields to group by. :rtype: list[str] """ return self._groupBy def setGroupBy(self, fields): """ Set the list of fields to group by. Example: library.setGroupBy(["name:asc", "type:asc"]) :type fields: list[str] """ self._groupBy = fields def settings(self): """ Get the settings for the dataset. :rtype: dict """ return { "sortBy": self.sortBy(), "groupBy": self.groupBy() } def setSettings(self, settings): """ Set the settings for the dataset object. :type settings: dict """ value = settings.get('sortBy') if value is not None: self.setSortBy(value) value = settings.get('groupBy') if value is not None: self.setGroupBy(value) def setSearchEnabled(self, enabled): """Enable or disable the search the for the library.""" self._searchEnabled = enabled def isSearchEnabled(self): """Check if search is enabled for the library.""" return self._searchEnabled def recursiveDepth(self): """ Return the recursive search depth. :rtype: int """ return studiolibrary.config.get('recursiveSearchDepth') def fields(self): """Return all the fields for the library.""" return self._fields def path(self): """ Return the disc location of the db. :rtype: str """ return self._path def setPath(self, path): """ Set the disc location of the db. :type path: str """ self._path = path def databasePath(self): """ Return the path to the database. :rtype: str """ formatString = studiolibrary.config.get('databasePath') return studiolibrary.formatPath(formatString, path=self.path()) def distinct(self, field, queries=None, sortBy="name"): """ Get all the values for the given field. :type field: str :type queries None or list[dict] :type sortBy: str :rtype: list """ results = {} queries = queries or [] queries.extend(self._globalQueries.values()) items = self.createItems() for item in items: value = item.itemData().get(field) if value: results.setdefault(value, {'count': 0, 'name': value}) match = self.match(item.itemData(), queries) if match: results[value]['count'] += 1 def sortKey(facet): return facet.get(sortBy) return sorted(results.values(), key=sortKey) def mtime(self): """ Return when the database was last modified. :rtype: float or None """ path = self.databasePath() mtime = None if os.path.exists(path): mtime = os.path.getmtime(path) return mtime def setDirty(self, value): """ Update the model object with the current database timestamp. :type: bool """ if value: self._mtime = None else: self._mtime = self.mtime() def isDirty(self): """ Return True if the database has changed on disc. :rtype: bool """ return not self._items or self._mtime != self.mtime() def read(self): """ Read the database from disc and return a dict object. :rtype: dict """ if self.path(): if self.isDirty(): self._data = studiolibrary.readJson(self.databasePath()) self.setDirty(False) else: logger.info('No path set for reading the data from disc.') return self._data def save(self, data): """ Write the given dict object to the database on disc. :type data: dict :rtype: None """ if self.path(): studiolibrary.saveJson(self.databasePath(), data) self.setDirty(True) else: logger.info('No path set for saving the data to disc.') def clear(self): """Clear all the item data.""" self._items = [] self._results = [] self._groupedResults = {} self.dataChanged.emit() def sync(self, progressCallback=None): """Sync the file system with the database.""" if not self.path(): logger.info('No path set for syncing data') return if progressCallback: progressCallback("Syncing") new = {} old = self.read() depth = self.recursiveDepth() items = studiolibrary.findItems( self.path(), depth=depth, ) items = list(items) count = len(items) for i, item in enumerate(items): percent = (float(i+1)/float(count)) if progressCallback: percent *= 100 label = "{0:.0f}%".format(percent) progressCallback(label, percent) path = item.path() itemData = old.get(path, {}) itemData.update(item.createItemData()) new[path] = itemData if progressCallback: progressCallback("Post Callbacks") self.postSync(new) if progressCallback: progressCallback("Saving Cache") self.save(new) self.dataChanged.emit() def postSync(self, data): """ Use this function to execute code on the data after sync, but before save and dataChanged.emit :type data: dict :rtype: None """ pass def createItems(self): """ Create all the items for the model. :rtype: list[studiolibrary.LibraryItem] """ # Check if the database has changed since the last read call if self.isDirty(): paths = self.read().keys() items = studiolibrary.itemsFromPaths( paths, library=self, libraryWindow=self._libraryWindow ) self._items = list(items) self.loadItemData(self._items) return self._items def findItems(self, queries): """ Get the items that match the given queries. Examples: queries = [ { 'operator': 'or', 'filters': [ ('folder', 'is' '/library/proj/test'), ('folder', 'startswith', '/library/proj/test'), ] }, { 'operator': 'and', 'filters': [ ('path', 'contains' 'test'), ('path', 'contains', 'run'), ] } ] print(library.find(queries)) :type queries: list[dict] :rtype: list[studiolibrary.LibraryItem] """ fields = [] results = [] queries = copy.copy(queries) queries.extend(self._globalQueries.values()) logger.debug("Search queries:") for query in queries: logger.debug('Query: %s', query) items = self.createItems() for item in items: match = self.match(item.itemData(), queries) if match: results.append(item) fields.extend(item.itemData().keys()) self._fields = list(set(fields)) if self.sortBy(): results = self.sorted(results, self.sortBy()) return results def queries(self, exclude=None): """ Get all the queries for the dataset excluding the given ones. :type exclude: list[str] or None :rtype: list[dict] """ queries = [] exclude = exclude or [] for query in self._queries.values(): if query.get('name') not in exclude: queries.append(query) return queries def addGlobalQuery(self, query): """ Add a global query to library. :type query: dict """ self._globalQueries[query["name"]] = query def addQuery(self, query): """ Add a search query to the library. Examples: addQuery({ 'name': 'My Query', 'operator': 'or', 'filters': [ ('folder', 'is' '/library/proj/test'), ('folder', 'startswith', '/library/proj/test'), ] }) :type query: dict """ self._queries[query["name"]] = query def removeQuery(self, name): """ Remove the query with the given name. :type name: str """ if name in self._queries: del self._queries[name] def queryExists(self, name): """ Check if the given query name exists. :type name: str :rtype: bool """ return name in self._queries def search(self): """Run a search using the queries added to this dataset.""" if not self.isSearchEnabled(): logger.debug('Search is disabled') return t = time.time() logger.debug("Searching items") self.searchStarted.emit() self._results = self.findItems(self.queries()) self._groupedResults = self.groupItems(self._results, self.groupBy()) self.searchFinished.emit() self._searchTime = time.time() - t self.searchTimeFinished.emit() logger.debug('Search time: %s', self._searchTime) def results(self): """ Return the items found after a search is ran. :rtype: list[Item] """ return self._results def groupedResults(self): """ Get the results grouped after a search is ran. :rtype: dict """ return self._groupedResults def searchTime(self): """ Return the time taken to run a search. :rtype: float """ return self._searchTime def addItem(self, item): """ Add the given item to the database. :type item: studiolibrary.LibraryItem :rtype: None """ self.saveItemData([item]) def addItems(self, items): """ Add the given items to the database. :type items: list[studiolibrary.LibraryItem] """ self.saveItemData(items) def updateItem(self, item): """ Update the given item in the database. :type item: studiolibrary.LibraryItem :rtype: None """ self.saveItemData([item]) def saveItemData(self, items, emitDataChanged=True): """ Add the given items to the database. :type items: list[studiolibrary.LibraryItem] :type emitDataChanged: bool """ logger.debug("Save item data %s", items) data_ = self.read() for item in items: path = item.path() data = item.itemData() data_.setdefault(path, {}) data_[path].update(data) self.save(data_) if emitDataChanged: self.search() self.dataChanged.emit() def loadItemData(self, items): """ Load the item data from the database to the given items. :type items: list[studiolibrary.LibraryItem] """ logger.debug("Loading item data %s", items) data = self.read() for item in items: key = item.id() if key in data: item.setItemData(data[key]) def addPaths(self, paths, data=None): """ Add the given path and the given data to the database. :type paths: list[str] :type data: dict or None :rtype: None """ data = data or {} self.updatePaths(paths, data) def updatePaths(self, paths, data): """ Update the given paths with the given data in the database. :type paths: list[str] :type data: dict :rtype: None """ data_ = self.read() paths = studiolibrary.normPaths(paths) for path in paths: if path in data_: data_[path].update(data) else: data_[path] = data self.save(data_) def copyPath(self, src, dst): """ Copy the given source path to the given destination path. :type src: str :type dst: str :rtype: str """ self.addPaths([dst]) return dst def renamePath(self, src, dst): """ Rename the source path to the given name. :type src: str :type dst: str :rtype: str """ studiolibrary.renamePathInFile(self.databasePath(), src, dst) self.setDirty(True) return dst def removePath(self, path): """ Remove the given path from the database. :type path: str :rtype: None """ self.removePaths([path]) def removePaths(self, paths): """ Remove the given paths from the database. :type paths: list[str] :rtype: None """ data = self.read() paths = studiolibrary.normPaths(paths) for path in paths: if path in data: del data[path] self.save(data) @staticmethod def match(data, queries): """ Match the given data with the given queries. Examples: queries = [ { 'operator': 'or', 'filters': [ ('folder', 'is' '/library/proj/test'), ('folder', 'startswith', '/library/proj/test'), ] }, { 'operator': 'and', 'filters': [ ('path', 'contains' 'test'), ('path', 'contains', 'run'), ] } ] print(library.find(queries)) """ matches = [] for query in queries: filters = query.get('filters') operator = query.get('operator', 'and') if not filters: continue match = False for key, cond, value in filters: if key == '*': itemValue = unicode(data) else: itemValue = data.get(key) if isinstance(value, basestring): value = value.lower() if isinstance(itemValue, basestring): itemValue = itemValue.lower() if not itemValue: match = False elif cond == 'contains': match = value in itemValue elif cond == 'not_contains': match = value not in itemValue elif cond == 'is': match = value == itemValue elif cond == 'not': match = value != itemValue elif cond == 'startswith': match = itemValue.startswith(value) if operator == 'or' and match: break if operator == 'and' and not match: break matches.append(match) return all(matches) @staticmethod def sorted(items, sortBy): """ Return the given data sorted using the sortBy argument. Example: data = [ {'name':'red', 'index':1}, {'name':'green', 'index':2}, {'name':'blue', 'index':3}, ] sortBy = ['index:asc', 'name'] # sortBy = ['index:dsc', 'name'] print(sortedData(data, sortBy)) :type items: list[Item] :type sortBy: list[str] :rtype: list[Item] """ logger.debug('Sort by: %s', sortBy) t = time.time() for field in reversed(sortBy): tokens = field.split(':') reverse = False if len(tokens) > 1: field = tokens[0] reverse = tokens[1] != 'asc' def sortKey(item): default = False if reverse else '' return item.itemData().get(field, default) items = sorted(items, key=sortKey, reverse=reverse) logger.debug("Sort items took %s", time.time() - t) return items @staticmethod def groupItems(items, fields): """ Group the given items by the given field. :type items: list[Item] :type fields: list[str] :rtype: dict """ logger.debug('Group by: %s', fields) # Only support for top level grouping at the moment. if fields: field = fields[0] else: return {'None': items} t = time.time() results_ = {} tokens = field.split(':') reverse = False if len(tokens) > 1: field = tokens[0] reverse = tokens[1] != 'asc' for item in items: value = item.itemData().get(field) if value: results_.setdefault(value, []) results_[value].append(item) groups = sorted(results_.keys(), reverse=reverse) results = collections.OrderedDict() for group in groups: results[group] = results_[group] logger.debug("Group Items Took %s", time.time() - t) return results
class ColorPickerWidget(QtWidgets.QFrame): COLOR_BUTTON_CLASS = ColorButton colorChanged = QtCore.Signal(object) def __init__(self, *args): QtWidgets.QFrame.__init__(self, *args) self._buttons = [] self._currentColor = None self._browserColors = None self._menuButton = None layout = QtWidgets.QHBoxLayout() layout.setSpacing(0) layout.setContentsMargins(0, 0, 0, 0) self.setLayout(layout) def enterEvent(self, event): """ Overriding this method to fix a bug with custom actions. :type event: QtCore.QEvent """ if self.parent(): menu = self.parent().parent() if isinstance(menu, QtWidgets.QMenu): menu.setActiveAction(None) def _colorChanged(self, color): """ Triggered when the user clicks or browses for a color. :type color: studioqt.Color :rtype: None """ self.setCurrentColor(color) self.colorChanged.emit(color) def menuButton(self): """ Get the menu button used for browsing for custom colors. :rtype: QtGui.QWidget """ return self._menuButton def deleteButtons(self): """ Delete all the color buttons. :rtype: None """ layout = self.layout() while layout.count(): item = layout.takeAt(0) item.widget().deleteLater() def currentColor(self): """ Return the current color. :rtype: studioqt.Color """ return self._currentColor def setCurrentColor(self, color): """ Set the current color. :type color: studioqt.Color """ self._currentColor = color self.refresh() def refresh(self): """Update the current state of the selected color.""" for button in self._buttons: button.setChecked(button.color() == self.currentColor()) def setColors(self, colors): """ Set the colors for the color bar. :type colors: list[str] or list[studioqt.Color] """ self.deleteButtons() first = True last = False for i, color in enumerate(colors): if i == len(colors)-1: last = True if not isinstance(color, str): color = studioqt.Color(color) color = color.toString() callback = partial(self._colorChanged, color) button = self.COLOR_BUTTON_CLASS(self) button.setObjectName('colorButton') button.setColor(color) button.setSizePolicy( QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred ) button.setProperty("first", first) button.setProperty("last", last) button.clicked.connect(callback) self.layout().addWidget(button) self._buttons.append(button) first = False self._menuButton = QtWidgets.QPushButton("...", self) self._menuButton.setObjectName('menuButton') self._menuButton.setSizePolicy( QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred ) self._menuButton.clicked.connect(self.browseColor) self.layout().addWidget(self._menuButton) self.refresh() def setBrowserColors(self, colors): """ :type colors: list((int,int,int)) """ self._browserColors = colors def browserColors(self): """ Get the colors to be displayed in the browser :rtype: list[studioqt.Color] """ return self._browserColors @QtCore.Slot() def blankSlot(self): """Blank slot to fix an issue with PySide2.QColorDialog.open()""" pass def browseColor(self): """ Show the color dialog. :rtype: None """ color = self.currentColor() if color: color = studioqt.Color.fromString(color) d = QtWidgets.QColorDialog(self) d.setCurrentColor(color) standardColors = self.browserColors() if standardColors: index = -1 for standardColor in standardColors: index += 1 try: # Support for new qt5 signature standardColor = QtGui.QColor(standardColor) d.setStandardColor(index, standardColor) except: # Support for new qt4 signature standardColor = QtGui.QColor(standardColor).rgba() d.setStandardColor(index, standardColor) d.currentColorChanged.connect(self._colorChanged) if d.exec_(): self._colorChanged(d.selectedColor()) else: self._colorChanged(color)
class SidebarWidget(QtWidgets.QTreeWidget): itemDropped = QtCore.Signal(object) itemRenamed = QtCore.Signal(str, str) itemSelectionChanged = QtCore.Signal() def __init__(self, *args): super(SidebarWidget, self).__init__(*args) self._dpi = 1 self._items = [] self._index = {} self._locked = False self._dataset = None self._recursive = True self._options = { 'field': 'path', 'separator': '/', 'recursive': True, 'autoRootPath': True, 'rootText': 'FOLDERS', 'sortBy': None, 'queries': [{ 'filters': [('type', 'is', 'Folder')] }] } self.itemExpanded.connect(self.update) self.itemCollapsed.connect(self.update) self.setDpi(1) self.setAcceptDrops(True) self.setHeaderHidden(True) self.setFrameShape(QtWidgets.QFrame.NoFrame) self.setSelectionMode(QtWidgets.QTreeWidget.ExtendedSelection) self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) def clear(self): """Clear all the items from the tree widget.""" self._items = [] self._index = {} super(SidebarWidget, self).clear() def selectionChanged(self, *args): """Triggered the current selection has changed.""" self.search() def setRecursive(self, enable): """ Set the search query on the dataset to be recursive. :type enable: bool """ self._recursive = enable self.search() def isRecursive(self): """ Get the recursive query enable state. :rtype: bool """ return self._recursive def sortBy(self): """ Get the sortby field. :rtype: str """ return self._options.get('sortBy', [self.field()]) def field(self): """ Get the field. :rtype: str """ return self._options.get('field', '') def rootText(self): """ Get the root text. :rtype: str """ return self._options.get('rootText') def separator(self): """ Get the separator used in the fields to separate level values. :rtype: str """ return self._options.get('separator', DEFAULT_SEPARATOR) def _dataChanged(self): """Triggered when the data set has changed.""" pass # data = collections.OrderedDict() # queries = self._options.get("queries") # # items = self.dataset().findItems(queries) # # for item in items: # itemData = item.itemData() # value = itemData.get(self.field()) # data[value] = {'iconPath': itemData.get('iconPath')} # # if data: # root = findRoot(data.keys(), separator=self.separator()) # self.setPaths(data, root=root) def setDataset(self, dataset): """ Set the dataset for the search widget: :type dataset: studioqt.Dataset """ self._dataset = dataset self._dataset.dataChanged.connect(self._dataChanged) self._dataChanged() def dataset(self): """ Get the dataset for the search widget. :rtype: studioqt.Dataset """ return self._dataset def search(self): """Run the dataset search.""" if self.dataset(): self.dataset().addQuery(self.query()) self.dataset().search() else: logger.info('No dataset found for the sidebar widget.') def query(self): """ Get the query for the sidebar widget. :rtype: dict """ filters = [] for path in self.selectedPaths(): if self.isRecursive(): suffix = "" if path.endswith("/") else "/" filter_ = ('folder', 'startswith', path + suffix) filters.append(filter_) filter_ = ('folder', 'is', path) filters.append(filter_) uniqueName = 'sidebar_widget_' + str(id(self)) return {'name': uniqueName, 'operator': 'or', 'filters': filters} def setLocked(self, locked): """ Set the widget items to read only mode. :type locked: bool :rtype: None """ self._locked = locked def isLocked(self): """ Return True if the items are in read only mode :rtype: bool """ return self._locked def itemAt(self, pos): """ :type pos: QtGui.QPoint :rtype: None or Folder """ index = self.indexAt(pos) if not index.isValid(): return item = self.itemFromIndex(index) return item def dropEvent(self, event): """ :type event: QtCore.QEvent :rtype: None """ if self.isLocked(): logger.debug("Folder is locked! Cannot accept drop!") return self.itemDropped.emit(event) def dragMoveEvent(self, event): """ :type event: QtCore.QEvent :rtype: None """ mimeData = event.mimeData() if mimeData.hasUrls(): event.accept() else: event.ignore() item = self.itemAt(event.pos()) if item: self.selectPaths([item.path()]) def dragEnterEvent(self, event): """ :type event: QtCore.QEvent :rtype: None """ event.accept() def selectItem(self, item): """ :type item: NavigationWidgetItem :rtype: None """ self.selectPaths([item.path()]) def dpi(self): """ Return the dots per inch multiplier. :rtype: float """ return self._dpi def setDpi(self, dpi): """ Set the dots per inch multiplier. :type dpi: float :rtype: None """ size = 24 * dpi self.setIndentation(14 * dpi) self.setMinimumWidth(22 * dpi) self.setIconSize(QtCore.QSize(size, size)) self.setStyleSheet("height: {size}".format(size=size)) def update(self, *args): """ :rtype: None """ for item in self.items(): item.update() def items(self): """ Return a list of all the items in the tree widget. :rtype: list[NavigationWidgetItem] """ items = self.findItems( "*", QtCore.Qt.MatchWildcard | QtCore.Qt.MatchRecursive) return items def itemFromUrl(self, url): """ Return the item for the given url. :type url: QtCore.QUrl :rtype: NavigationWidgetItem """ for item in self.items(): if url == item.url(): return item def itemFromPath(self, path): """ Return the item for the given path. :type path: str :rtype: NavigationWidgetItem """ return self._index.get(path) def settings(self): """ Return a dictionary of the settings for this widget. :rtype: dict """ settings = {} scrollBar = self.verticalScrollBar() settings["verticalScrollBar"] = {"value": scrollBar.value()} scrollBar = self.horizontalScrollBar() settings["horizontalScrollBar"] = {"value": scrollBar.value()} for item in self.items(): itemSettings = item.settings() if itemSettings: settings[item.path()] = item.settings() return settings def setSettings(self, settings): """ Set the settings for this widget :type settings: dict """ for path in sorted(settings.keys()): s = settings.get(path, None) self.setPathSettings(path, s) scrollBarSettings = settings.get("verticalScrollBar", {}) value = scrollBarSettings.get("value", None) if value: self.verticalScrollBar().setValue(value) scrollBarSettings = settings.get("horizontalScrollBar", {}) value = scrollBarSettings.get("value", None) if value: self.horizontalScrollBar().setValue(value) self.setDpi(self.dpi()) def setPathSettings(self, path, settings): """ Show the context menu at the given position. :type path: str :type settings: dict :rtype: None """ item = self.itemFromPath(path) if item and settings: item.setSettings(settings) def showContextMenu(self, position): """ Show the context menu at the given position. :type position: QtCore.QPoint :rtype: None """ menu = self.createContextMenu() menu.exec_(self.viewport().mapToGlobal(position)) def expandedItems(self): """ Return all the expanded items. :rtype: list[NavigationWidgetItem] """ for item in self.items(): if self.isItemExpanded(item): yield item def expandedPaths(self): """ Return all the expanded paths. :rtype: list[NavigationWidgetItem] """ for item in self.expandedItems(): yield item.url() def setExpandedPaths(self, paths): """ Set the given paths to expanded. :type paths: list[str] """ for item in self.items(): if item.url() in paths: item.setExpanded(True) def selectedItem(self): """ Return the last selected item :rtype: SidebarWidgetItem """ path = self.selectedPath() return self.itemFromPath(path) def selectedPath(self): """ Return the last selected path :rtype: str or None """ paths = self.selectedPaths() if paths: return paths[-1] def selectedPaths(self): """ Return the paths that are selected. :rtype: list[str] """ paths = [] items = self.selectedItems() for item in items: path = item.path() paths.append(path) return studiolibrary.normPaths(paths) def selectPath(self, path): """ Select the given path :type: str :rtype: None """ self.selectPaths([path]) def selectPaths(self, paths): """ Select the items with the given paths. :type paths: list[str] :rtype: None """ paths = studiolibrary.normPaths(paths) items = self.items() for item in items: if studiolibrary.normPath(item.path()) in paths: item.setSelected(True) else: item.setSelected(False) def selectUrl(self, url): """ Select the item with the given url. :type url: str :rtype: None """ items = self.items() for item in items: if item.url() == url: item.setSelected(True) else: item.setSelected(False) def selectedUrls(self): """ Return the urls for the selected items. :rtype: list[str] """ urls = [] items = self.selectedItems() for item in items: urls.append(item.url()) return urls def setPaths(self, *args, **kwargs): """ This method has been deprecated. """ logger.warning("This method has been deprecated!") self.setData(*args, **kwargs) def setData(self, data, root="", split=None): """ Set the items to the given items. :type data: list[str] :type root: str :type split: str :rtype: None """ settings = self.settings() self.blockSignals(True) self.clear() if not root: root = findRoot(data.keys(), self.separator()) self.addPaths(data, root=root, split=split) self.setSettings(settings) self.blockSignals(False) self.search() def addPaths(self, paths, root="", split=None): """ Set the given items as a flat list. :type paths: list[str] :type root: str or None :type split: str or None """ data = pathsToDict(paths, root=root, separator=split) self.createItems(data, split=split) if isinstance(paths, dict): self.setSettings(paths) def createItems(self, data, split=None): """ Create the items from the given data dict :type data: dict :type split: str or None :rtype: None """ split = split or DEFAULT_SEPARATOR self._index = {} for key in data: path = split.join([key]) item = SidebarWidgetItem(self) item.setText(0, unicode(key)) item.setPath(path) self._index[path] = item if self.rootText(): item.setText(0, self.rootText()) item.setBold(True) item.setIconPath('none') item.setExpanded(True) def _recursive(parent, children, split=None): for text, val in sorted(children.iteritems()): path = parent.path() path = split.join([path, text]) child = SidebarWidgetItem() child.setText(0, unicode(text)) child.setPath(path) parent.addChild(child) self._index[path] = child _recursive(child, val, split=split) _recursive(item, data[key], split=split) self.update()
class ImageSequence(QtCore.QObject): DEFAULT_FPS = 24 frameChanged = QtCore.Signal(int) def __init__(self, path, *args): QtCore.QObject.__init__(self, *args) self._fps = self.DEFAULT_FPS self._timer = None self._frame = 0 self._frames = [] self._dirname = None self._paused = False if path: self.setPath(path) def firstFrame(self): """ Get the path to the first frame. :rtype: str """ if self._frames: return self._frames[0] return "" def setPath(self, path): """ Set a single frame or a directory to an image sequence. :type path: str """ if os.path.isfile(path): self._frame = 0 self._frames = [path] elif os.path.isdir(path): self.setDirname(path) def setDirname(self, dirname): """ Set the location to the image sequence. :type dirname: str :rtype: None """ def naturalSortItems(items): """ Sort the given list in the way that humans expect. """ convert = lambda text: int(text) if text.isdigit() else text alphanum_key = lambda key: [ convert(c) for c in re.split('([0-9]+)', key) ] items.sort(key=alphanum_key) self._dirname = dirname if os.path.isdir(dirname): self._frames = [ dirname + "/" + filename for filename in os.listdir(dirname) ] naturalSortItems(self._frames) def dirname(self): """ Return the location to the image sequence. :rtype: str """ return self._dirname def reset(self): """ Stop and reset the current frame to 0. :rtype: None """ if not self._timer: self._timer = QtCore.QTimer(self.parent()) self._timer.setSingleShot(False) self._timer.timeout.connect(self._frameChanged) if not self._paused: self._frame = 0 self._timer.stop() def pause(self): """ ImageSequence will enter Paused state. :rtype: None """ self._paused = True self._timer.stop() def resume(self): """ ImageSequence will enter Playing state. :rtype: None """ if self._paused: self._paused = False self._timer.start() def stop(self): """ Stops the movie. ImageSequence enters NotRunning state. :rtype: None """ self._timer.stop() def start(self): """ Starts the movie. ImageSequence will enter Running state :rtype: None """ self.reset() if self._timer: self._timer.start(1000.0 / self._fps) def frames(self): """ Return all the filenames in the image sequence. :rtype: list[str] """ return self._frames def _frameChanged(self): """ Triggered when the current frame changes. :rtype: None """ if not self._frames: return frame = self._frame frame += 1 self.jumpToFrame(frame) def percent(self): """ Return the current frame position as a percentage. :rtype: None """ if len(self._frames) == self._frame + 1: _percent = 1 else: _percent = float( (len(self._frames) + self._frame)) / len(self._frames) - 1 return _percent def frameCount(self): """ Return the number of frames. :rtype: int """ return len(self._frames) def currentIcon(self): """ Returns the current frame as a QIcon. :rtype: QtGui.QIcon """ return QtGui.QIcon(self.currentFilename()) def currentPixmap(self): """ Return the current frame as a QPixmap. :rtype: QtGui.QPixmap """ return QtGui.QPixmap(self.currentFilename()) def currentFilename(self): """ Return the current file name. :rtype: str or None """ try: return self._frames[self.currentFrameNumber()] except IndexError: pass def currentFrameNumber(self): """ Return the current frame. :rtype: int or None """ return self._frame def jumpToFrame(self, frame): """ Set the current frame. :rtype: int or None """ if frame >= self.frameCount(): frame = 0 self._frame = frame self.frameChanged.emit(frame)
def url(self): """Used by the mime data when dragging/dropping the item.""" return QtCore.QUrl("file:///" + self.path())
class Item(QtWidgets.QTreeWidgetItem): """The Item is used to hold rows of information for an item view.""" ICON_PATH = None TYPE_ICON_PATH = None ThreadPool = QtCore.QThreadPool() THUMBNAIL_PATH = "" MAX_ICON_SIZE = 256 DEFAULT_FONT_SIZE = 12 DEFAULT_PLAYHEAD_COLOR = QtGui.QColor(255, 255, 255, 220) THUMBNAIL_COLUMN = 0 ENABLE_THUMBNAIL_THREAD = True PAINT_SLIDER = False _globalSignals = GlobalSignals() sliderChanged = _globalSignals.sliderChanged 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 = None self._typePixmap = None 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 = "" self._sliderDown = False self._sliderValue = 0.0 self._sliderPreviousValue = 0.0 self._sliderPosition = None self._sliderEnabled = 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.resetSlider() def doubleClicked(self): """ Triggered when an item is double clicked. :rtype: None """ pass def setGroupItem(self, groupItem): """ Set the group item that this item is a child to. :type groupItem: groupitem.GroupItem """ self._groupItem = groupItem def groupItem(self): """ Get the group item that this item is a child to. :rtype: groupitem.GroupItem """ 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.isLabelUnderItem(): 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 defaultThumbnailPath(self): """ Get the default thumbnail path. :rtype: str """ return self.THUMBNAIL_PATH def defaultThumbnailIcon(self): """ Get the default thumbnail icon. :rtype: QtGui.QIcon """ return QtGui.QIcon(self.defaultThumbnailPath()) def thumbnailIcon(self): """ Return the thumbnail icon. :rtype: QtGui.QIcon """ thumbnailPath = self.thumbnailPath() 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) self._thumbnailIcon = self.defaultThumbnailIcon() 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): """ Check if the label should be displayed. :rtype: bool """ return self.labelDisplayOption() != LabelDisplayOption.Hide def isLabelOverItem(self): """ Check if the label should be displayed over the item. :rtype: bool """ return self.labelDisplayOption() == LabelDisplayOption.Over def isLabelUnderItem(self): """ Check if the label should be displayed under the item. :rtype: bool """ return self.labelDisplayOption() == LabelDisplayOption.Under def labelDisplayOption(self): """ Return True if the text is visible. :rtype: bool """ return self.itemsWidget().labelDisplayOption() 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.sliderEvent(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: if self.isSliderEnabled(): self.setSliderDown(True) self._sliderPosition = 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.isSliderDown(): self._sliderPosition = None self._sliderPreviousValue = self.sliderValue() 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().itemBackgroundColor() 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 """ return QtCore.QRect(option.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) self.paintIcon(painter, option, index) if index.column() == 0 and self.sliderValue() != 0: self.paintSlider(painter, option, index) if self.isTextVisible(): self.paintText(painter, option, index) if index.column() == 0: self.paintTypeIcon(painter, option) if index.column() == 0 and 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 """ 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)) if not self.itemsWidget().isIconView(): spacing = 1 * self.dpi() height = visualRect.height() - spacing visualRect.setHeight(height) painter.drawRect(visualRect) def paintSlider(self, painter, option, index): """ Draw the virtual slider for the item. :type painter: QtWidgets.QPainter :type option: QtWidgets.QStyleOptionViewItem :type index: QtCore.QModelIndex """ if not self.PAINT_SLIDER: return if not self.itemsWidget().isIconView(): return # Draw slider background painter.setPen(QtGui.QPen(QtCore.Qt.NoPen)) rect = self.visualRect(option) color = self.itemsWidget().backgroundColor().toRgb() color.setAlpha(75) painter.setBrush(QtGui.QBrush(color)) height = rect.height() ratio = self.sliderValue() if ratio < 0: width = 0 elif ratio > 100: width = rect.width() else: width = rect.width() * (float(ratio) / 100) rect.setWidth(width) rect.setHeight(height) painter.drawRect(rect) # Draw slider value rect = self.visualRect(option) rect.setY(rect.y() + (4 * self.dpi())) color = self.itemsWidget().textColor().toRgb() color.setAlpha(220) pen = QtGui.QPen(color) align = QtCore.Qt.AlignTop | QtCore.Qt.AlignHCenter painter.setPen(pen) painter.drawText(rect, align, str(self.sliderValue()) + "%") 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.isLabelUnderItem(): 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 align = align | QtCore.Qt.AlignVCenter rect = QtCore.QRect(visualRect) if self.itemsWidget().isIconView(): if self.isLabelOverItem() or self.isLabelUnderItem(): padding = 8 if padding < 8 else padding height = metrics.height() + (padding / 2) y = (rect.y() + rect.height()) - height rect.setY(y) rect.setHeight(height) if self.isLabelOverItem(): color2 = self.itemsWidget().backgroundColor().toRgb() color2.setAlpha(200) painter.setPen(QtCore.Qt.NoPen) painter.setBrush(QtGui.QBrush(color2)) painter.drawRect(rect) pen = QtGui.QPen(color) painter.setPen(pen) painter.setFont(font) painter.drawText(rect, align, text) # ------------------------------------------------------------------------ # Support for middle mouse slider # ------------------------------------------------------------------------ def setSliderEnabled(self, enabled): """ Set if middle mouse slider is enabled. :type enabled: bool """ self._sliderEnabled = enabled def isSliderEnabled(self): """ Return true if middle mouse slider is enabled. :rtype: bool """ return self._sliderEnabled def sliderEvent(self, event): """ Called when the mouse moves while the middle mouse button is held down. :param event: QtGui.QMouseEvent """ if self.isSliderDown(): value = (event.pos().x() - self.sliderPosition().x()) / 1.5 value = math.ceil(value) + self.sliderPreviousValue() try: self.setSliderValue(value) except Exception: self.setSliderDown(False) def resetSlider(self): """Reset the slider value to zero.""" self._sliderValue = 0.0 self._sliderPreviousValue = 0.0 def setSliderDown(self, down): """Called when the middle mouse button is released.""" self._sliderDown = down if not down: self._sliderPosition = None self._sliderPreviousValue = self.sliderValue() def isSliderDown(self): """ Return True if blending. :rtype: bool """ return self._sliderDown def setSliderValue(self, value): """ Set the blend value. :type value: float :rtype: bool """ if self.isSliderEnabled(): self._sliderValue = value if self.PAINT_SLIDER: self.update() self.sliderChanged.emit(value) if self.PAINT_SLIDER: self.update() logger.debug("Blending:" + str(value)) def sliderValue(self): """ Return the blend value. :rtype: float """ return self._sliderValue def sliderPreviousValue(self): """ :rtype: float """ return self._sliderPreviousValue def sliderPosition(self): """ :rtype: QtGui.QPoint """ return self._sliderPosition # ------------------------------------------------------------------------ # 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 """ self._imageSequencePath = path def imageSequencePath(self): """ :rtype: str """ return self._imageSequencePath def stop(self): """Stop playing the image sequence movie.""" if self.imageSequence(): self.imageSequence().stop() def play(self): """Start playing the image sequence movie.""" 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) def typeIconPath(self): """ Return the type icon path on disc. :rtype: path or None """ if self.TYPE_ICON_PATH is None: return self.ICON_PATH return self.TYPE_ICON_PATH def typePixmap(self): """ Return the type pixmap for the plugin. :rtype: QtWidgets.QPixmap """ if not self._typePixmap: iconPath = self.typeIconPath() if iconPath and os.path.exists(iconPath): self._typePixmap = QtGui.QPixmap(iconPath) return self._typePixmap def typeIconRect(self, option): """ Return the type icon rect. :rtype: QtGui.QRect """ padding = 2 * self.dpi() r = self.iconRect(option) x = r.x() + padding y = r.y() + padding rect = QtCore.QRect(x, y, 13 * self.dpi(), 13 * self.dpi()) return rect def paintTypeIcon(self, painter, option): """ Draw the item type icon at the top left. :type painter: QtWidgets.QPainter :type option: QtWidgets.QStyleOptionViewItem :rtype: None """ rect = self.typeIconRect(option) typePixmap = self.typePixmap() if typePixmap: painter.setOpacity(0.5) painter.drawPixmap(rect, typePixmap) painter.setOpacity(1)
class Library(QtCore.QObject): Fields = [ { "name": "icon", "sortable": False, "groupable": False, }, { "name": "name", "sortable": True, "groupable": False, }, { "name": "path", "sortable": True, "groupable": False, }, { "name": "type", "sortable": True, "groupable": True, }, { "name": "folder", "sortable": True, "groupable": False, }, { "name": "category", "sortable": True, "groupable": True, }, { "name": "modified", "sortable": True, "groupable": False, }, { "name": "Custom Order", "sortable": True, "groupable": False, }, ] dataChanged = QtCore.Signal() searchStarted = QtCore.Signal() searchFinished = QtCore.Signal() searchTimeFinished = QtCore.Signal() def __init__(self, path=None, libraryWindow=None, *args): QtCore.QObject.__init__(self, *args) self._path = path self._mtime = None self._data = {} self._items = [] self._fields = [] self._sortBy = [] self._groupBy = [] self._results = [] self._queries = {} self._globalQueries = {} self._groupedResults = {} self._searchTime = 0 self._searchEnabled = True self._registeredItems = None self._libraryWindow = libraryWindow self.setPath(path) self.setDirty(True) def sortBy(self): """ Get the list of fields to sort by. :rtype: list[str] """ return self._sortBy def setSortBy(self, fields): """ Set the list of fields to group by. Example: library.setSortBy(["name:asc", "type:asc"]) :type fields: list[str] """ self._sortBy = fields def groupBy(self): """ Get the list of fields to group by. :rtype: list[str] """ return self._groupBy def setGroupBy(self, fields): """ Set the list of fields to group by. Example: library.setGroupBy(["name:asc", "type:asc"]) :type fields: list[str] """ self._groupBy = fields def settings(self): """ Get the settings for the dataset. :rtype: dict """ return { "sortBy": self.sortBy(), "groupBy": self.groupBy() } def setSettings(self, settings): """ Set the settings for the dataset object. :type settings: dict """ value = settings.get('sortBy') if value is not None: self.setSortBy(value) value = settings.get('groupBy') if value is not None: self.setGroupBy(value) def setSearchEnabled(self, enabled): """Enable or disable the search the for the library.""" self._searchEnabled = enabled def isSearchEnabled(self): """Check if search is enabled for the library.""" return self._searchEnabled def recursiveDepth(self): """ Return the recursive search depth. :rtype: int """ return studiolibrary.config.get('recursiveSearchDepth') def fields(self): """ Get all the fields for the library. :rtype: list[dict] """ return self.Fields def fieldNames(self): """ Get all the field names for the library. :rtype: list[str] """ return [field["name"] for field in self.fields()] def path(self): """ Return the disc location of the db. :rtype: str """ return self._path def setPath(self, path): """ Set the disc location of the db. :type path: str """ self._path = path def databasePath(self): """ Return the path to the database. :rtype: str """ formatString = studiolibrary.config.get('databasePath') return studiolibrary.formatPath(formatString, path=self.path()) def distinct(self, field, queries=None, sortBy="name"): """ Get all the values for the given field. :type field: str :type queries None or list[dict] :type sortBy: str :rtype: list """ results = {} queries = queries or [] queries.extend(self._globalQueries.values()) items = self.createItems() for item in items: value = item.itemData().get(field) if value: results.setdefault(value, {'count': 0, 'name': value}) match = self.match(item.itemData(), queries) if match: results[value]['count'] += 1 def sortKey(facet): return facet.get(sortBy) return sorted(results.values(), key=sortKey) def mtime(self): """ Return when the database was last modified. :rtype: float or None """ path = self.databasePath() mtime = None if os.path.exists(path): mtime = os.path.getmtime(path) return mtime def setDirty(self, value): """ Update the model object with the current database timestamp. :type: bool """ if value: self._mtime = None else: self._mtime = self.mtime() def isDirty(self): """ Return True if the database has changed on disc. :rtype: bool """ return not self._items or self._mtime != self.mtime() def read(self): """ Read the database from disc and return a dict object. :rtype: dict """ if self.path(): if self.isDirty(): self._data = studiolibrary.readJson(self.databasePath()) self.setDirty(False) else: logger.info('No path set for reading the data from disc.') return self._data def save(self, data): """ Write the given dict object to the database on disc. :type data: dict :rtype: None """ if self.path(): studiolibrary.saveJson(self.databasePath(), data) self.setDirty(True) else: logger.info('No path set for saving the data to disc.') def clear(self): """Clear all the item data.""" self._items = [] self._results = [] self._groupedResults = {} self._registeredItems = None self.dataChanged.emit() def registeredItems(self): """ Get registered items for the library. :rtype: list[LibraryItem.__class__] """ return studiolibrary.registeredItems() def isValidPath(self, path): """ Check if the given item path should be ignored. :type path: str :rtype: bool """ for ignore in studiolibrary.config.get('ignorePaths', []): if ignore in path: return False return True def walker(self, path): """ Walk the given root path for valid items and return the item data. :type path: str :rtype: collections.Iterable[dict] """ path = studiolibrary.normPath(path) maxDepth = self.recursiveDepth() startDepth = path.count(os.path.sep) for root, dirs, files in os.walk(path, followlinks=True): files.extend(dirs) for filename in files: # Normalise the path for consistent matching path = studiolibrary.normPath(os.path.join(root, filename)) # Ignore any paths that have been specified in the config if not self.isValidPath(path): continue # Match the path with a registered item item = self.itemFromPath(path) remove = False if item: # Yield the item data that matches the current path yield item.createItemData() # Stop walking if the item doesn't support nested items if not item.ENABLE_NESTED_ITEMS: remove = True if remove and filename in dirs: dirs.remove(filename) if maxDepth == 1: break # Stop walking the directory if the maximum depth has been reached currentDepth = root.count(os.path.sep) if (currentDepth - startDepth) >= maxDepth: del dirs[:] def sync(self, progressCallback=None): """ Sync the file system with the database. :type progressCallback: None or func """ if not self.path(): logger.info('No path set for syncing data') return if progressCallback: progressCallback("Syncing") new = {} old = self.read() items = list(self.walker(self.path())) count = len(items) for i, item in enumerate(items): percent = (float(i+1)/float(count)) if progressCallback: percent *= 100 label = "{0:.0f}%".format(percent) progressCallback(label, percent) path = item.get("path") new[path] = old.get(path, {}) new[path].update(item) if progressCallback: progressCallback("Post Callbacks") self.postSync(new) if progressCallback: progressCallback("Saving Cache") self.save(new) self.dataChanged.emit() def postSync(self, data): """ Use this function to execute code on the data after sync, but before save and dataChanged.emit :type data: dict :rtype: None """ pass def createItems(self): """ Create all the items for the model. :rtype: list[studiolibrary.LibraryItem] """ # Check if the cache has changed since the last read call if self.isDirty(): logger.debug("Creating items") self._items = [] data = self.read() modules = [] for itemData in data.values(): if '__class__' in itemData: modules.append(itemData.get("__class__")) modules = set(modules) classes = {} for module in modules: classes[module] = studiolibrary.resolveModule(module) for path in data.keys(): module = data[path].get("__class__") cls = classes.get(module) if cls: item = cls(path, library=self, libraryWindow=self._libraryWindow) item.setItemData(data[path]) self._items.append(item) else: # This is to support the older database data before v2.6. # Will remove in a later version. item = self.itemFromPath(path, library=self, libraryWindow=self._libraryWindow) if item: item.setItemData(data[path]) self._items.append(item) return self._items def itemFromPath(self, path, **kwargs): """ Return a new item instance for the given path. :type path: str :rtype: studiolibrary.LibraryItem or None """ path = studiolibrary.normPath(path) for cls in self.registeredItems(): if cls.match(path): return cls(path, **kwargs) def itemsFromPaths(self, paths, **kwargs): """ Return new item instances for the given paths. :type paths: list[str]: :rtype: collections.Iterable[studiolibrary.LibraryItem] """ for path in paths: item = self.itemFromPath(path, **kwargs) if item: yield item def itemsFromUrls(self, urls, **kwargs): """ Return new item instances for the given QUrl objects. :type urls: list[QtGui.QUrl] :rtype: list[studiolibrary.LibraryItem] """ items = [] for path in studiolibrary.pathsFromUrls(urls): item = self.itemFromPath(path, **kwargs) if item: data = item.createItemData() item.setItemData(data) items.append(item) else: msg = 'Cannot find the item for path "{0}"' msg = msg.format(path) logger.warning(msg) return items def findItems(self, queries): """ Get the items that match the given queries. Examples: queries = [ { 'operator': 'or', 'filters': [ ('folder', 'is' '/library/proj/test'), ('folder', 'startswith', '/library/proj/test'), ] }, { 'operator': 'and', 'filters': [ ('path', 'contains' 'test'), ('path', 'contains', 'run'), ] } ] print(library.find(queries)) :type queries: list[dict] :rtype: list[studiolibrary.LibraryItem] """ fields = [] results = [] queries = copy.copy(queries) queries.extend(self._globalQueries.values()) logger.debug("Search queries:") for query in queries: logger.debug('Query: %s', query) items = self.createItems() for item in items: match = self.match(item.itemData(), queries) if match: results.append(item) fields.extend(item.itemData().keys()) self._fields = list(set(fields)) if self.sortBy(): results = self.sorted(results, self.sortBy()) return results def queries(self, exclude=None): """ Get all the queries for the dataset excluding the given ones. :type exclude: list[str] or None :rtype: list[dict] """ queries = [] exclude = exclude or [] for query in self._queries.values(): if query.get('name') not in exclude: queries.append(query) return queries def addGlobalQuery(self, query): """ Add a global query to library. :type query: dict """ self._globalQueries[query["name"]] = query def addQuery(self, query): """ Add a search query to the library. Examples: addQuery({ 'name': 'My Query', 'operator': 'or', 'filters': [ ('folder', 'is' '/library/proj/test'), ('folder', 'startswith', '/library/proj/test'), ] }) :type query: dict """ self._queries[query["name"]] = query def removeQuery(self, name): """ Remove the query with the given name. :type name: str """ if name in self._queries: del self._queries[name] def queryExists(self, name): """ Check if the given query name exists. :type name: str :rtype: bool """ return name in self._queries def search(self): """Run a search using the queries added to this dataset.""" if not self.isSearchEnabled(): logger.debug('Search is disabled') return t = time.time() logger.debug("Searching items") self.searchStarted.emit() self._results = self.findItems(self.queries()) self._groupedResults = self.groupItems(self._results, self.groupBy()) self.searchFinished.emit() self._searchTime = time.time() - t self.searchTimeFinished.emit() logger.debug('Search time: %s', self._searchTime) def results(self): """ Return the items found after a search is ran. :rtype: list[Item] """ return self._results def groupedResults(self): """ Get the results grouped after a search is ran. :rtype: dict """ return self._groupedResults def searchTime(self): """ Return the time taken to run a search. :rtype: float """ return self._searchTime def addItem(self, item): """ Add the given item to the database. :type item: studiolibrary.LibraryItem :rtype: None """ self.saveItemData([item]) def addItems(self, items): """ Add the given items to the database. :type items: list[studiolibrary.LibraryItem] """ self.saveItemData(items) def saveItemData(self, items, emitDataChanged=True): """ Add the given items to the database. :type items: list[studiolibrary.LibraryItem] :type emitDataChanged: bool """ logger.debug("Save item data %s", items) data_ = self.read() for item in items: path = item.path() data = item.itemData() data_.setdefault(path, {}) data_[path].update(data) self.save(data_) if emitDataChanged: self.search() self.dataChanged.emit() def addPaths(self, paths, data=None): """ Add the given path and the given data to the database. :type paths: list[str] :type data: dict or None :rtype: None """ data = data or {} self.updatePaths(paths, data) def updatePaths(self, paths, data): """ Update the given paths with the given data in the database. :type paths: list[str] :type data: dict :rtype: None """ data_ = self.read() paths = studiolibrary.normPaths(paths) for path in paths: if path in data_: data_[path].update(data) else: data_[path] = data self.save(data_) def copyPath(self, src, dst): """ Copy the given source path to the given destination path. :type src: str :type dst: str :rtype: str """ self.addPaths([dst]) return dst def renamePath(self, src, dst): """ Rename the source path to the given name. :type src: str :type dst: str :rtype: str """ studiolibrary.renamePathInFile(self.databasePath(), src, dst) self.setDirty(True) return dst def removePath(self, path): """ Remove the given path from the database. :type path: str :rtype: None """ self.removePaths([path]) def removePaths(self, paths): """ Remove the given paths from the database. :type paths: list[str] :rtype: None """ data = self.read() paths = studiolibrary.normPaths(paths) for path in paths: if path in data: del data[path] self.save(data) @staticmethod def match(data, queries): """ Match the given data with the given queries. Examples: queries = [ { 'operator': 'or', 'filters': [ ('folder', 'is' '/library/proj/test'), ('folder', 'startswith', '/library/proj/test'), ] }, { 'operator': 'and', 'filters': [ ('path', 'contains' 'test'), ('path', 'contains', 'run'), ] } ] print(library.find(queries)) """ matches = [] for query in queries: filters = query.get('filters') operator = query.get('operator', 'and') if not filters: continue match = False for key, cond, value in filters: if key == '*': itemValue = six.text_type(data) else: itemValue = data.get(key) if isinstance(value, six.string_types): value = value.lower() if isinstance(itemValue, six.string_types): itemValue = itemValue.lower() if not itemValue: match = False elif cond == 'contains': match = value in itemValue elif cond == 'not_contains': match = value not in itemValue elif cond == 'is': match = value == itemValue elif cond == 'not': match = value != itemValue elif cond == 'startswith': match = itemValue.startswith(value) if operator == 'or' and match: break if operator == 'and' and not match: break matches.append(match) return all(matches) @staticmethod def sorted(items, sortBy): """ Return the given data sorted using the sortBy argument. Example: data = [ {'name':'red', 'index':1}, {'name':'green', 'index':2}, {'name':'blue', 'index':3}, ] sortBy = ['index:asc', 'name'] # sortBy = ['index:dsc', 'name'] print(sortedData(data, sortBy)) :type items: list[Item] :type sortBy: list[str] :rtype: list[Item] """ logger.debug('Sort by: %s', sortBy) t = time.time() for field in reversed(sortBy): tokens = field.split(':') reverse = False if len(tokens) > 1: field = tokens[0] reverse = tokens[1] != 'asc' def sortKey(item): default = False if reverse else '' return item.itemData().get(field, default) items = sorted(items, key=sortKey, reverse=reverse) logger.debug("Sort items took %s", time.time() - t) return items @staticmethod def groupItems(items, fields): """ Group the given items by the given field. :type items: list[Item] :type fields: list[str] :rtype: dict """ logger.debug('Group by: %s', fields) # Only support for top level grouping at the moment. if fields: field = fields[0] else: return {'None': items} t = time.time() results_ = {} tokens = field.split(':') reverse = False if len(tokens) > 1: field = tokens[0] reverse = tokens[1] != 'asc' for item in items: value = item.itemData().get(field) if value: results_.setdefault(value, []) results_[value].append(item) groups = sorted(results_.keys(), reverse=reverse) results = collections.OrderedDict() for group in groups: results[group] = results_[group] logger.debug("Group Items Took %s", time.time() - t) return results
class SidebarWidget(QtWidgets.QWidget): itemDropped = QtCore.Signal(object) itemRenamed = QtCore.Signal(str, str) itemSelectionChanged = QtCore.Signal() settingsMenuRequested = QtCore.Signal(object) def __init__(self, *args): super(SidebarWidget, self).__init__(*args) self._dataset = None self._lineEdit = None self._previousFilterText = "" layout = QtWidgets.QVBoxLayout(self) layout.setSpacing(0) layout.setContentsMargins(0,0,0,0) self.setLayout(layout) self._treeWidget = TreeWidget(self) self._treeWidget.itemDropped = self.itemDropped self._treeWidget.itemRenamed = self.itemRenamed self._treeWidget.itemSelectionChanged.connect(self._itemSelectionChanged) self._titleWidget = self.createTitleWidget() self._titleWidget.ui.menuButton.clicked.connect(self.showSettingsMenu) self._titleWidget.ui.titleButton.clicked.connect(self.clearSelection) self.layout().addWidget(self._titleWidget) self.layout().addWidget(self._treeWidget) self._treeWidget.installEventFilter(self) def _itemSelectionChanged(self, *args): self.itemSelectionChanged.emit() def eventFilter(self, obj, event): """Using an event filter to show the search widget on key press.""" if event.type() == QtCore.QEvent.KeyPress: self._keyPressEvent(event) return super(SidebarWidget, self).eventFilter(obj, event) def _keyPressEvent(self, event): """ Triggered from the tree widget key press event. :type event: QKeyEvent """ text = event.text().strip() if not text.isalpha() and not text.isdigit(): return if text and not self._titleWidget.ui.filterEdit.hasFocus(): self._titleWidget.ui.filterEdit.setText(text) self.setFilterVisible(True) self._previousFilterText = text def _filterVisibleTrigger(self, visible): """ Triggered by the filter visible action. :type visible: bool """ self.setFilterVisible(visible) self._titleWidget.ui.filterEdit.selectAll() def createTitleWidget(self): """ Create a new instance of the title bar widget. :rtype: QtWidgets.QFrame """ class UI(object): """Proxy class for attaching ui widgets as properties.""" pass titleWidget = QtWidgets.QFrame(self) titleWidget.setObjectName("titleWidget") titleWidget.ui = UI() vlayout = QtWidgets.QVBoxLayout(self) vlayout.setSpacing(0) vlayout.setContentsMargins(0,0,0,0) hlayout = QtWidgets.QHBoxLayout(self) hlayout.setSpacing(0) hlayout.setContentsMargins(0,0,0,0) vlayout.addLayout(hlayout) titleButton = QtWidgets.QPushButton(self) titleButton.setText("Folders") titleButton.setObjectName("titleButton") titleWidget.ui.titleButton = titleButton hlayout.addWidget(titleButton) menuButton = QtWidgets.QPushButton(self) menuButton.setText("...") menuButton.setObjectName("menuButton") titleWidget.ui.menuButton = menuButton hlayout.addWidget(menuButton) self._lineEdit = studiolibrary.widgets.LineEdit(self) self._lineEdit.hide() self._lineEdit.setObjectName("filterEdit") self._lineEdit.setText(self.treeWidget().filterText()) self._lineEdit.textChanged.connect(self.searchChanged) titleWidget.ui.filterEdit = self._lineEdit vlayout.addWidget(self._lineEdit) titleWidget.setLayout(vlayout) return titleWidget def _dataChanged(self): pass def setDataset(self, dataset): """ Set the dataset for the search widget: :type dataset: studioqt.Dataset """ self._dataset = dataset self._dataset.dataChanged.connect(self._dataChanged) self._dataChanged() def dataset(self): """ Get the dataset for the search widget. :rtype: studioqt.Dataset """ return self._dataset def search(self): """Run the dataset search.""" if self.dataset(): self.dataset().addQuery(self.query()) self.dataset().search() else: logger.info('No dataset found for the sidebar widget.') def query(self): """ Get the query for the sidebar widget. :rtype: dict """ filters = [] for path in self.selectedPaths(): if self.isRecursive(): suffix = "" if path.endswith("/") else "/" filter_ = ('folder', 'startswith', path + suffix) filters.append(filter_) filter_ = ('folder', 'is', path) filters.append(filter_) uniqueName = 'sidebar_widget_' + str(id(self)) return {'name': uniqueName, 'operator': 'or', 'filters': filters} def searchChanged(self, text): """ Triggered when the search filter has changed. :type text: str """ self.refreshFilter() if text: self.setFilterVisible(True) else: self.treeWidget().setFocus() self.setFilterVisible(False) def showSettingsMenu(self): """Create and show a new settings menu instance.""" menu = studioqt.Menu(self) self.settingsMenuRequested.emit(menu) self.createSettingsMenu(menu) point = QtGui.QCursor.pos() point.setX(point.x() + 3) point.setY(point.y() + 3) action = menu.exec_(point) menu.close() def createSettingsMenu(self, menu): """ Create a new settings menu instance. :rtype: QMenu """ action = menu.addAction("Show Filter") action.setCheckable(True) action.setChecked(self.isFilterVisible()) callback = functools.partial(self._filterVisibleTrigger, not self.isFilterVisible()) action.triggered.connect(callback) action = menu.addAction("Show Icons") action.setCheckable(True) action.setChecked(self.iconsVisible()) callback = functools.partial(self.setIconsVisible, not self.iconsVisible()) action.triggered.connect(callback) action = menu.addAction("Show Root Folder") action.setCheckable(True) action.setChecked(self.isRootVisible()) callback = functools.partial(self.setRootVisible, not self.isRootVisible()) action.triggered.connect(callback) return menu def setFilterVisible(self, visible): """ Set the filter widget visible :type visible: bool """ self._titleWidget.ui.filterEdit.setVisible(visible) self._titleWidget.ui.filterEdit.setFocus() if not visible and bool(self.treeWidget().filterText()): self.treeWidget().setFilterText("") else: self.refreshFilter() def setSettings(self, settings): """ Set the settings for the widget. :type settings: dict """ self.treeWidget().setSettings(settings) value = settings.get("filterVisible") if value is not None: self.setFilterVisible(value) value = settings.get("filterText") if value is not None: self.setFilterText(value) def settings(self): """ Get the settings for the widget. :rtype: dict """ settings = self.treeWidget().settings() settings["filterText"] = self.filterText() settings["filterVisible"] = self.isFilterVisible() return settings # -------------------------------- # convenience methods # -------------------------------- def filterText(self): return self.treeWidget().filterText() def setFilterText(self, text): self._titleWidget.ui.filterEdit.setText(text) def refreshFilter(self): self.treeWidget().setFilterText(self._titleWidget.ui.filterEdit.text()) def isFilterVisible(self): return bool(self.treeWidget().filterText()) or self._titleWidget.ui.filterEdit.isVisible() def setIconsVisible(self, visible): self.treeWidget().setIconsVisible(visible) def iconsVisible(self): return self.treeWidget().iconsVisible() def setRootVisible(self, visible): self.treeWidget().setRootVisible(visible) def isRootVisible(self): return self.treeWidget().isRootVisible() def treeWidget(self): return self._treeWidget def setDpi(self, dpi): self.treeWidget().setDpi(dpi) def setRecursive(self, enabled): self.treeWidget().setRecursive(enabled) def isRecursive(self): return self.treeWidget().isRecursive() def setData(self, *args, **kwargs): self.treeWidget().setData(*args, **kwargs) def setItemData(self, id, data): self.treeWidget().setPathSettings(id, data) def setLocked(self, locked): self.treeWidget().setLocked(locked) def selectedPath(self): return self.treeWidget().selectedPath() def selectPaths(self, paths): self.treeWidget().selectPaths(paths) def selectedPaths(self): return self.treeWidget().selectedPaths() def clearSelection(self): self.treeWidget().clearSelection()
class GlobalSignals(QtCore.QObject): """""" sliderChanged = QtCore.Signal(float)
class FieldWidget(QtWidgets.QFrame): """The base widget for all field widgets. Examples: data = { 'name': 'startFrame', 'type': 'int' 'value': 1, } fieldWidget = FieldWidget(data) """ valueChanged = QtCore.Signal() DefaultLayout = "horizontal" def __init__(self, parent=None, data=None, formWidget=None): super(FieldWidget, self).__init__(parent) self._data = data or {} self._widget = None self._default = None self._required = None self._collapsed = False self._errorLabel = None self._menuButton = None self._actionResult = None self._formWidget = None if formWidget: self.setFormWidget(formWidget) self.setObjectName("fieldWidget") direction = self._data.get("layout", self.DefaultLayout) if direction == "vertical": layout = QtWidgets.QVBoxLayout(self) else: layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) self.setContentsMargins(0, 0, 0, 0) self._label = QtWidgets.QLabel(self) self._label.setObjectName('label') self._label.setSizePolicy( QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred, ) layout.addWidget(self._label) self._layout2 = QtWidgets.QHBoxLayout(self) layout.addLayout(self._layout2) if direction == "vertical": self._label.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) else: self._label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) widget = self.createWidget() if widget: self.setWidget(widget) def name(self): """ Get the name of field widget. :rtype: str """ return self.data()["name"] def defaultData(self): """ This is the default data used by the schema. :rtype: dict """ return {} def setFormWidget(self, formWidget): """ Set the form widget which contains the field widget. :type formWidget: studiolibrary.widgets.formwidget.FormWidget """ self._formWidget = formWidget def formWidget(self): """ Get the form widget the contains the field widget. :return: studiolibrary.widgets.formwidget.FormWidget """ return self._formWidget def setCollapsed(self, collapsed): """ Set the field widget collapsed used by the GroupFieldWidget. :return: studiolibrary.widgets.formwidget.FormWidget """ self._collapsed = collapsed def isCollapsed(self): """ Get the collapsed state of the field widget. :return: studiolibrary.widgets.formwidget.FormWidget """ return self._collapsed def createWidget(self): """ Create the widget to be used by the field. :rtype: QtWidgets.Widget or None """ return None def label(self): """ Get the label widget. :rtype: QtWidgets.QLabel """ return self._label def state(self): """ Get the current state of the data. :rtype: dict """ return {"name": self._data["name"], "value": self.value()} def data(self): """ Get the data for the widget. :rtype: dict """ return self._data def title(self): """ Get the title to be displayed for the field. :rtype: str """ data = self.data() title = data.get("title") if title is None: title = data.get("name", "") title = toTitle(title) if self.isRequired(): title += '*' return title def setData(self, data): """ Set the current state of the field widget using a dictionary. :type data: dict """ self.blockSignals(True) items = data.get('items') if items is not None: self.setItems(items) value = data.get('value') default = data.get('default') # Must set the default before value if default is not None: self.setDefault(default) elif value is not None: self.setDefault(value) if value is not None or (value and value != self.value()): try: self.setValue(value) except TypeError as error: logger.exception(error) enabled = data.get('enabled') if enabled is not None: self.setEnabled(enabled) self._label.setEnabled(enabled) hidden = data.get('hidden') if hidden is not None: self.setHidden(hidden) visible = data.get('visible') if visible is not None and not self.isCollapsed(): self.setVisible(visible) required = data.get('required') if required is not None: self.setRequired(required) error = data.get('error') if error is not None: self.setError(error) value = data.get('errorVisible') if value is not None: self.setErrorVisible(value) toolTip = data.get('toolTip') if toolTip is not None: self.setToolTip(toolTip) self.setStatusTip(toolTip) placeholder = data.get("placeholder") if placeholder is not None: self.setPlaceholder(placeholder) style = data.get("style") if style is not None: self.setStyleSheet(style) title = self.title() or "" self.setText(title) label = data.get('label') if label is not None: text = label.get("name") if text is not None: self.setText(text) visible = label.get('visible') if visible is not None: self.label().setVisible(visible) # Menu Items actions = data.get('actions') if actions is not None: self._menuButton.setVisible(True) # Menu Button menu = data.get('menu') if menu is not None: text = menu.get("name") if text is not None: self._menuButton.setText(text) visible = menu.get("visible", True) self._menuButton.setVisible(visible) self._data.update(data) self.refresh() self.blockSignals(False) def setPlaceholder(self, placeholder): """ Set the placeholder text to be displayed for the widget. :type placeholder: str :raises: NotImplementedError """ NotImplementedError( 'The method "setPlaceholder" needs to be implemented') def hasError(self): """ Check if the field contains any errors. :rtype: bool """ return bool(self.data().get("error")) def setErrorVisible(self, visible): """ Set the error message visibility. :type visible: bool """ self._data["errorVisible"] = visible self.refreshError() def setError(self, message): """ Set the error message to be displayed for the field widget. :type message: str """ self._data["error"] = message self.refreshError() def refreshError(self): """Refresh the error message with the current data.""" error = self.data().get("error") if self.hasError() and self.data().get("errorVisible", False): self._errorLabel.setText(error) self._errorLabel.setHidden(False) self.setToolTip(error) else: self._errorLabel.setText("") self._errorLabel.setHidden(True) self.setToolTip(self.data().get('toolTip')) self.refresh() def setText(self, text): """ Set the label text for the field widget. :type text: str """ self._label.setText(text) def setValue(self, value): """ Set the value of the field widget. Will emit valueChanged() if the new value is different from the old one. :type value: object """ self.emitValueChanged() def value(self): """ Get the value of the field widget. :rtype: object """ raise NotImplementedError('The method "value" needs to be implemented') def setItems(self, items): """ Set the items for the field widget. :type items: list[str] """ raise NotImplementedError( 'The method "setItems" needs to be implemented') def reset(self): """Reset the field widget back to the defaults.""" self.setState(self._data) def setRequired(self, required): """ Set True if a value is required for this field. :type required: bool """ self._required = required self.setProperty('required', required) self.setStyleSheet(self.styleSheet()) def isRequired(self): """ Check if a value is required for the field widget. :rtype: bool """ return bool(self._required) def setDefault(self, default): """ Set the default value for the field widget. :type default: object """ self._default = default def default(self): """ Get the default value for the field widget. :rtype: object """ return self._default def isDefault(self): """ Check if the current value is the same as the default value. :rtype: bool """ return self.value() == self.default() def emitValueChanged(self, *args): """ Emit the value changed signal. :type args: list """ self.valueChanged.emit() self.refresh() def setWidget(self, widget): """ Set the widget used to set and get the field value. :type widget: QtWidgets.QWidget """ widgetLayout = QtWidgets.QHBoxLayout(self) widgetLayout.setContentsMargins(0, 0, 0, 0) widgetLayout.setSpacing(0) self._widget = widget self._widget.setParent(self) self._widget.setObjectName('widget') self._widget.setSizePolicy( QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred, ) self._menuButton = QtWidgets.QPushButton("...") self._menuButton.setHidden(True) self._menuButton.setObjectName("menuButton") self._menuButton.clicked.connect(self._menuCallback) self._menuButton.setSizePolicy( QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Expanding, ) widgetLayout.addWidget(self._widget) widgetLayout.addWidget(self._menuButton) layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) self._errorLabel = QtWidgets.QLabel(self) self._errorLabel.setHidden(True) self._errorLabel.setObjectName("errorLabel") self._errorLabel.setSizePolicy( QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred, ) layout.addLayout(widgetLayout) layout.addWidget(self._errorLabel) self._layout2.addLayout(layout) def _menuCallback(self): callback = self.data().get("menu", {}).get("callback", self.showMenu) callback() def _actionCallback(self, callback): """ Wrap schema callback to get the return value. :type callback: func """ self._actionResult = callback() def showMenu(self): """Show the menu using the actions from the data.""" menu = QtWidgets.QMenu(self) actions = self.data().get("actions", []) for action in actions: name = action.get("name", "No name found") enabled = action.get("enabled", True) callback = action.get("callback") func = functools.partial(self._actionCallback, callback) action = menu.addAction(name) action.setEnabled(enabled) action.triggered.connect(func) point = QtGui.QCursor.pos() point.setX(point.x() + 3) point.setY(point.y() + 3) # Reset the action results self._actionResult = None menu.exec_(point) if self._actionResult is not None: self.setValue(self._actionResult) def widget(self, ): """ Get the widget used to set and get the field value. :rtype: QtWidgets.QWidget """ return self._widget def refresh(self): """Refresh the style properties.""" direction = self._data.get("layout", self.DefaultLayout) self.setProperty("layout", direction) self.setProperty('default', self.isDefault()) if self.data().get("errorVisible", False): self.setProperty('error', self.hasError()) self.setStyleSheet(self.styleSheet())
class BaseItemSignals(QtCore.QObject): """""" loadValueChanged = QtCore.Signal(object, object)
class WorkerSignals(QtCore.QObject): triggered = QtCore.Signal(object)