def test_dock_standalone(self): widget = QWidget() layout = QHBoxLayout() widget.setLayout(layout) layout.addStretch(1) widget.show() dock = CollapsibleDockWidget() layout.addWidget(dock) list_view = QListView() list_view.setModel(QStringListModel(["a", "b"], list_view)) label = QLabel("A label. ") label.setWordWrap(True) dock.setExpandedWidget(label) dock.setCollapsedWidget(list_view) dock.setExpanded(True) self.app.processEvents() def toogle(): dock.setExpanded(not dock.expanded()) self.singleShot(2000, toogle) toogle() self.app.exec_()
def test_dock_standalone(self): widget = QWidget() layout = QHBoxLayout() widget.setLayout(layout) layout.addStretch(1) widget.show() dock = CollapsibleDockWidget() layout.addWidget(dock) list_view = QListView() list_view.setModel(QStringListModel(["a", "b"], list_view)) label = QLabel("A label. ") label.setWordWrap(True) dock.setExpandedWidget(label) dock.setCollapsedWidget(list_view) dock.setExpanded(True) dock.setExpanded(False) timer = QTimer(dock, interval=50) timer.timeout.connect(lambda: dock.setExpanded(not dock.expanded())) timer.start() self.qWait() timer.stop()
def show_tip(widget: QWidget, pos: QPoint, text: str, timeout=-1, textFormat=Qt.AutoText, wordWrap=None): propname = __name__ + "::show_tip_qlabel" if timeout < 0: timeout = widget.toolTipDuration() if timeout < 0: timeout = 5000 + 40 * max(0, len(text) - 100) tip = widget.property(propname) if not text and tip is None: return def hide(): w = tip.parent() w.setProperty(propname, None) tip.timer.stop() tip.close() tip.deleteLater() if not isinstance(tip, QLabel): tip = QLabel(objectName="tip-label", focusPolicy=Qt.NoFocus) tip.setBackgroundRole(QPalette.ToolTipBase) tip.setForegroundRole(QPalette.ToolTipText) tip.setPalette(QToolTip.palette()) tip.setFont(QApplication.font("QTipLabel")) tip.timer = QTimer(tip, singleShot=True, objectName="hide-timer") tip.timer.timeout.connect(hide) widget.setProperty(propname, tip) tip.setParent(widget, Qt.ToolTip) tip.setText(text) tip.setTextFormat(textFormat) if wordWrap is None: wordWrap = textFormat != Qt.PlainText tip.setWordWrap(wordWrap) if not text: hide() else: tip.timer.start(timeout) tip.show() tip.move(pos)
def test_dock_standalone(self): widget = QWidget() layout = QHBoxLayout() widget.setLayout(layout) layout.addStretch(1) widget.show() dock = CollapsibleDockWidget() layout.addWidget(dock) list_view = QListView() list_view.setModel(QStringListModel(["a", "b"], list_view)) label = QLabel("A label. ") label.setWordWrap(True) dock.setExpandedWidget(label) dock.setCollapsedWidget(list_view) dock.setExpanded(True) dock.setExpanded(False) timer = QTimer(dock, interval=200) timer.timeout.connect(lambda: dock.setExpanded(not dock.expanded())) timer.start()
class PreviewBrowser(QWidget): """A Preview Browser for recent/premade scheme selection. """ # Emitted when the current previewed item changes currentIndexChanged = Signal(int) # Emitted when an item is double clicked in the preview list. activated = Signal(int) def __init__(self, *args): QWidget.__init__(self, *args) self.__model = None self.__currentIndex = -1 self.__template = DESCRIPTION_TEMPLATE self.__setupUi() def __setupUi(self): vlayout = QVBoxLayout() vlayout.setContentsMargins(0, 0, 0, 0) top_layout = QHBoxLayout() top_layout.setContentsMargins(12, 12, 12, 12) # Top row with full text description and a large preview # image. self.__label = QLabel( self, objectName="description-label", wordWrap=True, alignment=Qt.AlignTop | Qt.AlignLeft, ) self.__label.setWordWrap(True) self.__label.setFixedSize(220, PREVIEW_SIZE[1]) self.__image = QSvgWidget(self, objectName="preview-image") self.__image.setFixedSize(*PREVIEW_SIZE) self.__imageFrame = DropShadowFrame(self) self.__imageFrame.setWidget(self.__image) # Path text below the description and image path_layout = QHBoxLayout() path_layout.setContentsMargins(12, 0, 12, 0) path_label = QLabel("<b>{0!s}</b>".format(self.tr("Path:")), self, objectName="path-label") self.__path = TextLabel(self, objectName="path-text") path_layout.addWidget(path_label) path_layout.addWidget(self.__path) self.__selectAction = QAction(self.tr("Select"), self, objectName="select-action") top_layout.addWidget(self.__label, 1, alignment=Qt.AlignTop | Qt.AlignLeft) top_layout.addWidget(self.__image, 1, alignment=Qt.AlignTop | Qt.AlignRight) vlayout.addLayout(top_layout) vlayout.addLayout(path_layout) # An list view with small preview icons. self.__previewList = LinearIconView(objectName="preview-list-view") self.__previewList.doubleClicked.connect(self.__onDoubleClicked) vlayout.addWidget(self.__previewList) self.setLayout(vlayout) def setModel(self, model): """Set the item model for preview. """ if self.__model != model: if self.__model: s_model = self.__previewList.selectionModel() s_model.selectionChanged.disconnect(self.__onSelectionChanged) self.__model.dataChanged.disconnect(self.__onDataChanged) self.__model = model self.__previewList.setModel(model) if model: s_model = self.__previewList.selectionModel() s_model.selectionChanged.connect(self.__onSelectionChanged) self.__model.dataChanged.connect(self.__onDataChanged) if model and model.rowCount(): self.setCurrentIndex(0) def model(self): """Return the item model. """ return self.__model def setPreviewDelegate(self, delegate): """Set the delegate to render the preview images. """ raise NotImplementedError def setDescriptionTemplate(self, template): self.__template = template self.__update() def setCurrentIndex(self, index): """Set the selected preview item index. """ if self.__model is not None and self.__model.rowCount(): index = min(index, self.__model.rowCount() - 1) index = self.__model.index(index, 0) sel_model = self.__previewList.selectionModel() # This emits selectionChanged signal and triggers # __onSelectionChanged, currentIndex is updated there. sel_model.select(index, sel_model.ClearAndSelect) elif self.__currentIndex != -1: self.__currentIndex = -1 self.__update() self.currentIndexChanged.emit(-1) def currentIndex(self): """Return the current selected index. """ return self.__currentIndex def __onSelectionChanged(self, *args): """Selected item in the preview list has changed. Set the new description and large preview image. """ rows = self.__previewList.selectedIndexes() if rows: index = rows[0] self.__currentIndex = index.row() else: index = QModelIndex() self.__currentIndex = -1 self.__update() self.currentIndexChanged.emit(self.__currentIndex) def __onDataChanged(self, topleft, bottomRight): """Data changed, update the preview if current index in the changed range. """ if (self.__currentIndex <= topleft.row() and self.__currentIndex >= bottomRight.row()): self.__update() def __onDoubleClicked(self, index): """Double click on an item in the preview item list. """ self.activated.emit(index.row()) def __update(self): """Update the current description. """ if self.__currentIndex != -1: index = self.model().index(self.__currentIndex, 0) else: index = QModelIndex() if not index.isValid(): description = "" name = "" path = "" svg = NO_PREVIEW_SVG else: description = str(index.data(Qt.WhatsThisRole)) if not description: description = "No description." description = escape(description) description = description.replace("\n", "<br/>") name = str(index.data(Qt.DisplayRole)) if not name: name = "Untitled" name = escape(name) path = str(index.data(Qt.StatusTipRole)) svg = str(index.data(previewmodel.ThumbnailSVGRole)) desc_text = self.__template.format(description=description, name=name) self.__label.setText(desc_text) self.__path.setText(path) if not svg: svg = NO_PREVIEW_SVG if svg: self.__image.load(QByteArray(svg.encode("utf-8")))
class PreviewBrowser(QWidget): """A Preview Browser for recent/premade scheme selection. """ # Emitted when the current previewed item changes currentIndexChanged = Signal(int) # Emitted when an item is double clicked in the preview list. activated = Signal(int) def __init__(self, *args, heading="", previewMargins=12, **kwargs): super().__init__(*args) self.__model = None self.__currentIndex = -1 self.__template = DESCRIPTION_TEMPLATE self.__margin = previewMargins self.__setupUi() self.setHeading(heading) def __setupUi(self): vlayout = QVBoxLayout() vlayout.setContentsMargins(0, 0, 0, 0) top_layout = QVBoxLayout(objectName="top-layout") margin = self.__margin top_layout.setContentsMargins(margin, margin, margin, margin) # Optional heading label self.__heading = QLabel( self, objectName="heading", visible=False ) # Horizontal row with full text description and a large preview # image. hlayout = QHBoxLayout() hlayout.setContentsMargins(0, 0, 0, 0) self.__label = QLabel( self, objectName="description-label", wordWrap=True, alignment=Qt.AlignTop | Qt.AlignLeft ) self.__label.setWordWrap(True) self.__label.setFixedSize(220, PREVIEW_SIZE[1]) self.__label.setMinimumWidth(PREVIEW_SIZE[0] // 2) self.__label.setMaximumHeight(PREVIEW_SIZE[1]) self.__image = QSvgWidget(self, objectName="preview-image") self.__image.setFixedSize(*PREVIEW_SIZE) self.__imageFrame = DropShadowFrame(self) self.__imageFrame.setWidget(self.__image) hlayout.addWidget(self.__label) hlayout.addWidget(self.__image) # Path text below the description and image path_layout = QHBoxLayout() path_layout.setContentsMargins(0, 0, 0, 0) path_label = QLabel("<b>{0!s}</b>".format(self.tr("Path:")), self, objectName="path-label") self.__path = TextLabel(self, objectName="path-text") path_layout.addWidget(path_label) path_layout.addWidget(self.__path) top_layout.addWidget(self.__heading) top_layout.addLayout(hlayout) top_layout.addLayout(path_layout) vlayout.addLayout(top_layout) # An list view with small preview icons. self.__previewList = LinearIconView( objectName="preview-list-view", wordWrap=True ) self.__previewList.doubleClicked.connect(self.__onDoubleClicked) vlayout.addWidget(self.__previewList) self.setLayout(vlayout) def setHeading(self, text): self.__heading.setVisible(bool(text)) self.__heading.setText(text) def setPreviewMargins(self, margin): # type: (int) -> None """ Set the left, top and right margins of the top widget part (heading and description) Parameters ---------- margin : int Margin """ if margin != self.__margin: layout = self.layout().itemAt(0).layout() assert isinstance(layout, QVBoxLayout) assert layout.objectName() == "top-layout" layout.setContentsMargins(margin, margin, margin, 0) def setModel(self, model): """Set the item model for preview. """ if self.__model != model: if self.__model: s_model = self.__previewList.selectionModel() s_model.selectionChanged.disconnect(self.__onSelectionChanged) self.__model.dataChanged.disconnect(self.__onDataChanged) self.__model = model self.__previewList.setModel(model) if model: s_model = self.__previewList.selectionModel() s_model.selectionChanged.connect(self.__onSelectionChanged) self.__model.dataChanged.connect(self.__onDataChanged) if model and model.rowCount(): self.setCurrentIndex(0) def model(self): """Return the item model. """ return self.__model def setPreviewDelegate(self, delegate): """Set the delegate to render the preview images. """ raise NotImplementedError def setDescriptionTemplate(self, template): self.__template = template self.__update() def setCurrentIndex(self, index): """Set the selected preview item index. """ if self.__model is not None and self.__model.rowCount(): index = min(index, self.__model.rowCount() - 1) index = self.__model.index(index, 0) sel_model = self.__previewList.selectionModel() # This emits selectionChanged signal and triggers # __onSelectionChanged, currentIndex is updated there. sel_model.select(index, sel_model.ClearAndSelect) elif self.__currentIndex != -1: self.__currentIndex = -1 self.__update() self.currentIndexChanged.emit(-1) def currentIndex(self): """Return the current selected index. """ return self.__currentIndex def __onSelectionChanged(self, *args): """Selected item in the preview list has changed. Set the new description and large preview image. """ rows = self.__previewList.selectedIndexes() if rows: index = rows[0] self.__currentIndex = index.row() else: index = QModelIndex() self.__currentIndex = -1 self.__update() self.currentIndexChanged.emit(self.__currentIndex) def __onDataChanged(self, topleft, bottomRight): """Data changed, update the preview if current index in the changed range. """ if self.__currentIndex <= topleft.row() and \ self.__currentIndex >= bottomRight.row(): self.__update() def __onDoubleClicked(self, index): """Double click on an item in the preview item list. """ self.activated.emit(index.row()) def __update(self): """Update the current description. """ if self.__currentIndex != -1: index = self.model().index(self.__currentIndex, 0) else: index = QModelIndex() if not index.isValid(): description = "" name = "" path = "" svg = NO_PREVIEW_SVG else: description = index.data(Qt.WhatsThisRole) if description: description = description else: description = "No description." description = escape(description) description = description.replace("\n", "<br/>") name = index.data(Qt.DisplayRole) if name: name = name else: name = "Untitled" name = escape(name) path = str(index.data(Qt.StatusTipRole)) svg = str(index.data(previewmodel.ThumbnailSVGRole)) desc_text = self.__template.format(description=description, name=name) self.__label.setText(desc_text) self.__path.setText(contractuser(path)) if not svg: svg = NO_PREVIEW_SVG if svg: self.__image.load(QByteArray(svg.encode("utf-8")))
class MessageWidget(QWidget): """ A widget displaying a simple message to the user. This is an alternative to a full QMessageBox intended for inline modeless messages. [[icon] {Message text} (Ok) (Cancel)] """ #: Emitted when a button with the AcceptRole is clicked accepted = Signal() #: Emitted when a button with the RejectRole is clicked rejected = Signal() #: Emitted when a button with the HelpRole is clicked helpRequested = Signal() #: Emitted when a button is clicked clicked = Signal(QAbstractButton) class StandardButton(enum.IntEnum): NoButton, Ok, Close, Help = 0x0, 0x1, 0x2, 0x4 NoButton, Ok, Close, Help = list(StandardButton) class ButtonRole(enum.IntEnum): InvalidRole, AcceptRole, RejectRole, HelpRole = 0, 1, 2, 3 InvalidRole, AcceptRole, RejectRole, HelpRole = list(ButtonRole) _Button = namedtuple("_Button", ["button", "role", "stdbutton"]) def __init__(self, parent=None, icon=QIcon(), text="", wordWrap=False, textFormat=Qt.AutoText, standardButtons=NoButton, **kwargs): super().__init__(parent, **kwargs) self.__text = text self.__icon = QIcon() self.__wordWrap = wordWrap self.__standardButtons = MessageWidget.NoButton self.__buttons = [] layout = QHBoxLayout() layout.setContentsMargins(8, 0, 8, 0) self.__iconlabel = QLabel(objectName="icon-label") self.__iconlabel.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.__textlabel = QLabel(objectName="text-label", text=text, wordWrap=wordWrap, textFormat=textFormat) if sys.platform == "darwin": self.__textlabel.setAttribute(Qt.WA_MacSmallSize) layout.addWidget(self.__iconlabel) layout.addWidget(self.__textlabel) self.setLayout(layout) self.setIcon(icon) self.setStandardButtons(standardButtons) def setText(self, text): """ Set the current message text. :type message: str """ if self.__text != text: self.__text = text self.__textlabel.setText(text) def text(self): """ Return the current message text. :rtype: str """ return self.__text def setIcon(self, icon): """ Set the message icon. :type icon: QIcon | QPixmap | QString | QStyle.StandardPixmap """ if isinstance(icon, QStyle.StandardPixmap): icon = self.style().standardIcon(icon) else: icon = QIcon(icon) if self.__icon != icon: self.__icon = QIcon(icon) if not self.__icon.isNull(): size = self.style().pixelMetric( QStyle.PM_SmallIconSize, None, self) pm = self.__icon.pixmap(QSize(size, size)) else: pm = QPixmap() self.__iconlabel.setPixmap(pm) self.__iconlabel.setVisible(not pm.isNull()) def icon(self): """ Return the current icon. :rtype: QIcon """ return QIcon(self.__icon) def setWordWrap(self, wordWrap): """ Set the message text wrap property :type wordWrap: bool """ if self.__wordWrap != wordWrap: self.__wordWrap = wordWrap self.__textlabel.setWordWrap(wordWrap) def wordWrap(self): """ Return the message text wrap property. :rtype: bool """ return self.__wordWrap def setTextFormat(self, textFormat): """ Set message text format :type textFormat: Qt.TextFormat """ self.__textlabel.setTextFormat(textFormat) def textFormat(self): """ Return the message text format. :rtype: Qt.TextFormat """ return self.__textlabel.textFormat() def changeEvent(self, event): # reimplemented if event.type() == 177: # QEvent.MacSizeChange: ... super().changeEvent(event) def setStandardButtons(self, buttons): for button in MessageWidget.StandardButton: existing = self.button(button) if button & buttons and existing is None: self.addButton(button) elif existing is not None: self.removeButton(existing) def standardButtons(self): return functools.reduce( operator.ior, (slot.stdbutton for slot in self.__buttons if slot.stdbutton is not None), MessageWidget.NoButton) def addButton(self, button, *rolearg): """ addButton(QAbstractButton, ButtonRole) addButton(str, ButtonRole) addButton(StandardButton) Add and return a button """ stdbutton = None if isinstance(button, QAbstractButton): if len(rolearg) != 1: raise TypeError("Wrong number of arguments for " "addButton(QAbstractButton, role)") role = rolearg[0] elif isinstance(button, MessageWidget.StandardButton): if len(rolearg) != 0: raise TypeError("Wrong number of arguments for " "addButton(StandardButton)") stdbutton = button if button == MessageWidget.Ok: role = MessageWidget.AcceptRole button = QPushButton("Ok", default=False, autoDefault=False) elif button == MessageWidget.Close: role = MessageWidget.RejectRole # button = QPushButton( # default=False, autoDefault=False, flat=True, # icon=QIcon(self.style().standardIcon( # QStyle.SP_TitleBarCloseButton))) button = SimpleButton( icon=QIcon(self.style().standardIcon( QStyle.SP_TitleBarCloseButton))) elif button == MessageWidget.Help: role = MessageWidget.HelpRole button = QPushButton("Help", default=False, autoDefault=False) elif isinstance(button, str): if len(rolearg) != 1: raise TypeError("Wrong number of arguments for " "addButton(str, ButtonRole)") role = rolearg[0] button = QPushButton(button, default=False, autoDefault=False) if sys.platform == "darwin": button.setAttribute(Qt.WA_MacSmallSize) self.__buttons.append(MessageWidget._Button(button, role, stdbutton)) button.clicked.connect(self.__button_clicked) self.__relayout() return button def removeButton(self, button): """ Remove a `button`. :type button: QAbstractButton """ slot = [s for s in self.__buttons if s.button is button] if slot: slot = slot[0] self.__buttons.remove(slot) self.layout().removeWidget(slot.button) slot.button.setParent(None) def buttonRole(self, button): """ Return the ButtonRole for button :type button: QAbsstractButton """ for slot in self.__buttons: if slot.button is button: return slot.role else: return MessageWidget.InvalidRole def button(self, standardButton): """ Return the button for the StandardButton. :type standardButton: StandardButton """ for slot in self.__buttons: if slot.stdbutton == standardButton: return slot.button else: return None def __button_clicked(self): button = self.sender() role = self.buttonRole(button) self.clicked.emit(button) if role == MessageWidget.AcceptRole: self.accepted.emit() self.close() elif role == MessageWidget.RejectRole: self.rejected.emit() self.close() elif role == MessageWidget.HelpRole: self.helpRequested.emit() def __relayout(self): for slot in self.__buttons: self.layout().removeWidget(slot.button) order = { MessageOverlayWidget.HelpRole: 0, MessageOverlayWidget.AcceptRole: 2, MessageOverlayWidget.RejectRole: 3, } orderd = sorted(self.__buttons, key=lambda slot: order.get(slot.role, -1)) prev = self.__textlabel for slot in orderd: self.layout().addWidget(slot.button) QWidget.setTabOrder(prev, slot.button)
def test(self): window = QWidget() layout = QVBoxLayout() window.setLayout(layout) stack = stackedwidget.AnimatedStackedWidget() stack.transitionFinished.connect(self.app.exit) layout.addStretch(2) layout.addWidget(stack) layout.addStretch(2) window.show() widget1 = QLabel("A label " * 10) widget1.setWordWrap(True) widget2 = QGroupBox("Group") widget3 = QListView() self.assertEqual(stack.count(), 0) self.assertEqual(stack.currentIndex(), -1) stack.addWidget(widget1) self.assertEqual(stack.count(), 1) self.assertEqual(stack.currentIndex(), 0) stack.addWidget(widget2) stack.addWidget(widget3) self.assertEqual(stack.count(), 3) self.assertEqual(stack.currentIndex(), 0) def widgets(): return [stack.widget(i) for i in range(stack.count())] self.assertSequenceEqual([widget1, widget2, widget3], widgets()) stack.show() stack.removeWidget(widget2) self.assertEqual(stack.count(), 2) self.assertEqual(stack.currentIndex(), 0) self.assertSequenceEqual([widget1, widget3], widgets()) stack.setCurrentIndex(1) # wait until animation finished self.app.exec_() self.assertEqual(stack.currentIndex(), 1) widget2 = QGroupBox("Group") stack.insertWidget(1, widget2) self.assertEqual(stack.count(), 3) self.assertEqual(stack.currentIndex(), 2) self.assertSequenceEqual([widget1, widget2, widget3], widgets()) stack.transitionFinished.disconnect(self.app.exit) def toogle(): idx = stack.currentIndex() stack.setCurrentIndex((idx + 1) % stack.count()) timer = QTimer(stack, interval=1000) timer.timeout.connect(toogle) timer.start() self.app.exec_()
class NotificationMessageWidget(QWidget): #: Emitted when a button with the AcceptRole is clicked accepted = Signal() #: Emitted when a button with the RejectRole is clicked rejected = Signal() #: Emitted when a button is clicked clicked = Signal(QAbstractButton) class StandardButton(enum.IntEnum): NoButton, Ok, Close = 0x0, 0x1, 0x2 NoButton, Ok, Close = list(StandardButton) class ButtonRole(enum.IntEnum): InvalidRole, AcceptRole, RejectRole, DismissRole = 0, 1, 2, 3 InvalidRole, AcceptRole, RejectRole, DismissRole = list(ButtonRole) _Button = namedtuple("_Button", ["button", "role", "stdbutton"]) def __init__(self, parent=None, icon=QIcon(), title="", text="", wordWrap=False, textFormat=Qt.PlainText, standardButtons=NoButton, acceptLabel="Ok", rejectLabel="No", **kwargs): super().__init__(parent, **kwargs) self._title = title self._text = text self._icon = QIcon() self._wordWrap = wordWrap self._standardButtons = NotificationMessageWidget.NoButton self._buttons = [] self._acceptLabel = acceptLabel self._rejectLabel = rejectLabel self._iconlabel = QLabel(objectName="icon-label") self._titlelabel = QLabel(objectName="title-label", text=title, wordWrap=wordWrap, textFormat=textFormat) self._textlabel = QLabel(objectName="text-label", text=text, wordWrap=wordWrap, textFormat=textFormat) self._textlabel.setTextInteractionFlags(Qt.TextBrowserInteraction) self._textlabel.setOpenExternalLinks(True) if sys.platform == "darwin": self._titlelabel.setAttribute(Qt.WA_MacSmallSize) self._textlabel.setAttribute(Qt.WA_MacSmallSize) layout = QHBoxLayout() self._iconlabel.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) layout.addWidget(self._iconlabel) layout.setAlignment(self._iconlabel, Qt.AlignTop) message_layout = QVBoxLayout() self._titlelabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) if sys.platform == "darwin": self._titlelabel.setContentsMargins(0, 1, 0, 0) else: self._titlelabel.setContentsMargins(0, 0, 0, 0) message_layout.addWidget(self._titlelabel) self._textlabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) message_layout.addWidget(self._textlabel) self.button_layout = QHBoxLayout() self.button_layout.setAlignment(Qt.AlignLeft) message_layout.addLayout(self.button_layout) layout.addLayout(message_layout) layout.setSpacing(7) self.setLayout(layout) self.setIcon(icon) self.setStandardButtons(standardButtons) def setText(self, text): """ Set the current message text. :type message: str """ if self._text != text: self._text = text self._textlabel.setText(text) def text(self): """ Return the current message text. :rtype: str """ return self._text def setTitle(self, title): """ Set the current title text. :type title: str """ if self._title != title: self._title = title self._titleLabel.setText(title) def title(self): """ Return the current title text. :rtype: str """ return self._title def setIcon(self, icon): """ Set the message icon. :type icon: QIcon | QPixmap | QString | QStyle.StandardPixmap """ if isinstance(icon, QStyle.StandardPixmap): icon = self.style().standardIcon(icon) else: icon = QIcon(icon) if self._icon != icon: self._icon = QIcon(icon) if not self._icon.isNull(): size = self.style().pixelMetric(QStyle.PM_SmallIconSize, None, self) pm = self._icon.pixmap(QSize(size, size)) else: pm = QPixmap() self._iconlabel.setPixmap(pm) self._iconlabel.setVisible(not pm.isNull()) def icon(self): """ Return the current icon. :rtype: QIcon """ return QIcon(self._icon) def setWordWrap(self, wordWrap): """ Set the message text wrap property :type wordWrap: bool """ if self._wordWrap != wordWrap: self._wordWrap = wordWrap self._textlabel.setWordWrap(wordWrap) def wordWrap(self): """ Return the message text wrap property. :rtype: bool """ return self._wordWrap def setTextFormat(self, textFormat): """ Set message text format :type textFormat: Qt.TextFormat """ self._textlabel.setTextFormat(textFormat) def textFormat(self): """ Return the message text format. :rtype: Qt.TextFormat """ return self._textlabel.textFormat() def setAcceptLabel(self, label): """ Set the accept button label. :type label: str """ self._acceptLabel = label def acceptLabel(self): """ Return the accept button label. :rtype str """ return self._acceptLabel def setRejectLabel(self, label): """ Set the reject button label. :type label: str """ self._rejectLabel = label def rejectLabel(self): """ Return the reject button label. :rtype str """ return self._rejectLabel def setStandardButtons(self, buttons): for button in NotificationMessageWidget.StandardButton: existing = self.button(button) if button & buttons and existing is None: self.addButton(button) elif existing is not None: self.removeButton(existing) def standardButtons(self): return functools.reduce( operator.ior, (slot.stdbutton for slot in self._buttons if slot.stdbutton is not None), NotificationMessageWidget.NoButton) def addButton(self, button, *rolearg): """ addButton(QAbstractButton, ButtonRole) addButton(str, ButtonRole) addButton(StandardButton) Add and return a button """ stdbutton = None if isinstance(button, QAbstractButton): if len(rolearg) != 1: raise TypeError("Wrong number of arguments for " "addButton(QAbstractButton, role)") role = rolearg[0] elif isinstance(button, NotificationMessageWidget.StandardButton): if rolearg: raise TypeError("Wrong number of arguments for " "addButton(StandardButton)") stdbutton = button if button == NotificationMessageWidget.Ok: role = NotificationMessageWidget.AcceptRole button = QPushButton(self._acceptLabel, default=False, autoDefault=False) elif button == NotificationMessageWidget.Close: role = NotificationMessageWidget.RejectRole button = QPushButton(self._rejectLabel, default=False, autoDefault=False) elif isinstance(button, str): if len(rolearg) != 1: raise TypeError("Wrong number of arguments for " "addButton(str, ButtonRole)") role = rolearg[0] button = QPushButton(button, default=False, autoDefault=False) if sys.platform == "darwin": button.setAttribute(Qt.WA_MacSmallSize) self._buttons.append( NotificationMessageWidget._Button(button, role, stdbutton)) button.clicked.connect(self._button_clicked) self._relayout() return button def _relayout(self): for slot in self._buttons: self.button_layout.removeWidget(slot.button) order = { NotificationWidget.AcceptRole: 0, NotificationWidget.RejectRole: 1, } ordered = sorted([ b for b in self._buttons if self.buttonRole(b.button) != NotificationMessageWidget.DismissRole ], key=lambda slot: order.get(slot.role, -1)) prev = self._textlabel for slot in ordered: self.button_layout.addWidget(slot.button) QWidget.setTabOrder(prev, slot.button) def removeButton(self, button): """ Remove a `button`. :type button: QAbstractButton """ slot = [s for s in self._buttons if s.button is button] if slot: slot = slot[0] self._buttons.remove(slot) self.layout().removeWidget(slot.button) slot.button.setParent(None) def buttonRole(self, button): """ Return the ButtonRole for button :type button: QAbstractButton """ for slot in self._buttons: if slot.button is button: return slot.role return NotificationMessageWidget.InvalidRole def button(self, standardButton): """ Return the button for the StandardButton. :type standardButton: StandardButton """ for slot in self._buttons: if slot.stdbutton == standardButton: return slot.button return None def _button_clicked(self): button = self.sender() role = self.buttonRole(button) self.clicked.emit(button) if role == NotificationMessageWidget.AcceptRole: self.accepted.emit() self.close() elif role == NotificationMessageWidget.RejectRole: self.rejected.emit() self.close()
def test(self): window = QWidget() layout = QVBoxLayout() window.setLayout(layout) stack = stackedwidget.AnimatedStackedWidget() stack.transitionFinished.connect(self.app.exit) layout.addStretch(2) layout.addWidget(stack) layout.addStretch(2) window.show() widget1 = QLabel("A label " * 10) widget1.setWordWrap(True) widget2 = QGroupBox("Group") widget3 = QListView() self.assertEqual(stack.count(), 0) self.assertEqual(stack.currentIndex(), -1) stack.addWidget(widget1) self.assertEqual(stack.count(), 1) self.assertEqual(stack.currentIndex(), 0) stack.addWidget(widget2) stack.addWidget(widget3) self.assertEqual(stack.count(), 3) self.assertEqual(stack.currentIndex(), 0) def widgets(): return [stack.widget(i) for i in range(stack.count())] self.assertSequenceEqual([widget1, widget2, widget3], widgets()) stack.show() stack.removeWidget(widget2) self.assertEqual(stack.count(), 2) self.assertEqual(stack.currentIndex(), 0) self.assertSequenceEqual([widget1, widget3], widgets()) stack.setCurrentIndex(1) # wait until animation finished self.app.exec_() self.assertEqual(stack.currentIndex(), 1) widget2 = QGroupBox("Group") stack.insertWidget(1, widget2) self.assertEqual(stack.count(), 3) self.assertEqual(stack.currentIndex(), 2) self.assertSequenceEqual([widget1, widget2, widget3], widgets()) stack.transitionFinished.disconnect(self.app.exit) self.singleShot(2000, lambda: stack.setCurrentIndex(0)) self.singleShot(4000, lambda: stack.setCurrentIndex(1)) self.singleShot(6000, lambda: stack.setCurrentIndex(2)) self.app.exec_()
class OWResolweFilter(widget.OWWidget): name = "Resolwe Filter" icon = 'icons/OWResolweFilter.svg' description = "Filter cells/genes" priority = 40 class Inputs: data = widget.Input("Data", resolwe.Data) class Outputs: data = widget.Output("Data", resolwe.Data) class Warning(widget.OWWidget.Warning): invalid_range = widget.Msg( "Negative values in input data.\n" "This filter only makes sense for non-negative measurements " "where 0 indicates a lack (of) and/or a neutral reading.") sampling_in_effect = widget.Msg("Too many data points to display.\n" "Sampling {} of {} data points.") #: Filter mode. #: Filter out rows/columns. Cells, Genes = Cells, Genes settings_version = 1 #: The selected filter mode selected_filter_type = settings.Setting(Cells) # type: int #: Selected filter statistics / QC measure indexed by filter_type selected_filter_metric = settings.Setting(TotalCounts) # type: int #: Augment the violin plot with a dot plot (strip plot) of the (non-zero) #: measurement counts in Cells/Genes mode or data matrix values in Data #: mode. display_dotplot = settings.Setting(True) # type: bool #: Is min/max range selection enable limit_lower_enabled = settings.Setting(True) # type: bool limit_upper_enabled = settings.Setting(True) # type: bool #: The lower and upper selection limit for each filter type thresholds = settings.Setting({ (Cells, DetectionCount): (0, 2**31 - 1), (Cells, TotalCounts): (0, 2**31 - 1), (Genes, DetectionCount): (0, 2**31 - 1), (Genes, TotalCounts): (0, 2**31 - 1) }) # type: Dict[Tuple[int, int], Tuple[float, float]] auto_commit = settings.Setting(False) # type: bool def __init__(self): super().__init__() self.data_table_object = None # type: Optional[resolwe.Data] self._counts = None # type: Optional[np.ndarray] self._counts_data_obj = None # type: Optional[resolwe.Data] self._counts_slug = 'data-filter-counts' # type: str self._selection_data_obj = None # type: Optional[resolwe.Data] self._selection_slug = 'data-table-filter' # type: str # threading self._task = None # type: Optional[ResolweTask] self._executor = ThreadExecutor() self.res = ResolweHelper() box = gui.widgetBox(self.controlArea, "Info") self._info = QLabel(box) self._info.setWordWrap(True) self._info.setText("No data in input\n") box.layout().addWidget(self._info) box = gui.widgetBox(self.controlArea, "Filter Type", spacing=-1) rbg = QButtonGroup(box, exclusive=True) layout = QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) for id_ in [Cells, Genes]: name, _, tip = FilterInfo[id_] b = QRadioButton(name, toolTip=tip, checked=id_ == self.selected_filter_type) rbg.addButton(b, id_) layout.addWidget(b, stretch=10, alignment=Qt.AlignCenter) box.layout().addLayout(layout) rbg.buttonClicked[int].connect(self.set_filter_type) self.filter_metric_cb = gui.comboBox(box, self, "selected_filter_metric", callback=self._update_metric) for id_ in [DetectionCount, TotalCounts]: text, ttip = MeasureInfo[id_] self.filter_metric_cb.addItem(text) idx = self.filter_metric_cb.count() - 1 self.filter_metric_cb.setItemData(idx, ttip, Qt.ToolTipRole) self.filter_metric_cb.setCurrentIndex(self.selected_filter_metric) form = QFormLayout(labelAlignment=Qt.AlignLeft, formAlignment=Qt.AlignLeft, fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow) self._filter_box = box = gui.widgetBox( self.controlArea, "Filter", orientation=form) # type: QGroupBox self.threshold_stacks = ( QStackedWidget(enabled=self.limit_lower_enabled), QStackedWidget(enabled=self.limit_upper_enabled), ) finfo = np.finfo(np.float64) for filter_ in [Cells, Genes]: if filter_ in {Cells, Genes}: minimum = 0.0 ndecimals = 1 metric = self.selected_filter_metric else: minimum = finfo.min ndecimals = 3 metric = -1 spinlower = QDoubleSpinBox( self, minimum=minimum, maximum=finfo.max, decimals=ndecimals, keyboardTracking=False, ) spinupper = QDoubleSpinBox( self, minimum=minimum, maximum=finfo.max, decimals=ndecimals, keyboardTracking=False, ) lower, upper = self.thresholds.get((filter_, metric), (0, 0)) spinlower.setValue(lower) spinupper.setValue(upper) self.threshold_stacks[0].addWidget(spinlower) self.threshold_stacks[1].addWidget(spinupper) spinlower.valueChanged.connect(self._limitchanged) spinupper.valueChanged.connect(self._limitchanged) self.threshold_stacks[0].setCurrentIndex(self.selected_filter_type) self.threshold_stacks[1].setCurrentIndex(self.selected_filter_type) self.limit_lower_enabled_cb = cb = QCheckBox( "Min", checked=self.limit_lower_enabled) cb.toggled.connect(self.set_lower_limit_enabled) cb.setAttribute(Qt.WA_LayoutUsesWidgetRect, True) form.addRow(cb, self.threshold_stacks[0]) self.limit_upper_enabled_cb = cb = QCheckBox( "Max", checked=self.limit_upper_enabled) cb.toggled.connect(self.set_upper_limit_enabled) cb.setAttribute(Qt.WA_LayoutUsesWidgetRect, True) form.addRow(cb, self.threshold_stacks[1]) box = gui.widgetBox(self.controlArea, "View") self._showpoints = gui.checkBox(box, self, "display_dotplot", "Show data points", callback=self._update_dotplot) self.controlArea.layout().addStretch(10) gui.auto_commit(self.controlArea, self, "auto_commit", "Commit") self._view = pg.GraphicsView() self._view.enableMouse(False) self._view.setAntialiasing(True) self._plot = plot = ViolinPlot() self._plot.setDataPointsVisible(self.display_dotplot) self._plot.setSelectionMode( (ViolinPlot.Low if self.limit_lower_enabled else 0) | (ViolinPlot.High if self.limit_upper_enabled else 0)) self._plot.selectionEdited.connect(self._limitchanged_plot) self._view.setCentralWidget(self._plot) self._plot.setTitle(FilterInfo[self.selected_filter_metric][1]) bottom = self._plot.getAxis("bottom") # type: pg.AxisItem bottom.hide() plot.setMouseEnabled(False, False) plot.hideButtons() self.mainArea.layout().addWidget(self._view) self.addAction( QAction("Select All", self, shortcut=QKeySequence.SelectAll, triggered=self._select_all)) def cancel(self): """Cancel the current task (if any).""" if self._task is not None: self._task.cancel() assert self._task.future.done() # disconnect the `_task_finished` slot self._task.watcher.done.disconnect(self.task_finished) self._task = None def run_task(self, slug, func): if self._task is not None: self.cancel() assert self._task is None self.progressBarInit() self._task = ResolweTask(slug) self._task.future = self._executor.submit(func) self._task.watcher = FutureWatcher(self._task.future) self._task.watcher.done.connect(self.task_finished) @Slot(Future, name='Future') def task_finished(self, future): assert threading.current_thread() == threading.main_thread() assert self._task is not None assert self._task.future is future assert future.done() try: future_result = future.result() except Exception as ex: # TODO: raise exceptions raise ex else: if self._task.slug == self._counts_slug: self._counts_data_obj = future_result self._setup_plot(future_result) elif self._task.slug == self._selection_slug: self._selection_data_obj = future_result self.Outputs.data.send(self._selection_data_obj) self._update_info() return self._selection_data_obj finally: self.progressBarFinished() self._task = None @Inputs.data def set_data(self, data): # type: (Optional[resolwe.Data]) -> None self.clear() self.data_table_object = data if data is not None: # self.res.get_object(id=data.id) self._setup(data, self.filter_type()) def commit(self): if self._counts_data_obj: inputs = { 'data_table': self.data_table_object, 'counts': self._counts_data_obj, 'axis': self._counts_data_obj.input['axis'] } if self.limit_upper_enabled: inputs['upper_limit'] = self.limit_upper if self.limit_lower_enabled: inputs['lower_limit'] = self.limit_lower func = partial(self.res.run_process, self._selection_slug, **inputs) self.run_task(self._selection_slug, func) self.Outputs.data.send(None) def _setup(self, data, filter_type): self.clear() axis = 1 if filter_type == Cells else 0 func = partial(self.res.run_process, self._counts_slug, data_table=data, axis=axis, measure=self.selected_filter_metric) # move filter process in thread self.run_task(self._counts_slug, func) def _setup_plot(self, data_object): # type: (resolwe.Data) -> None filter_data = self.res.get_json(data_object, 'counts_json', 'counts') axis_on_input = data_object.input['axis'] measure = self.selected_filter_metric if axis_on_input == Cells: title = "Cell Filter" if measure == TotalCounts: axis_label = "Total counts (library size)" else: axis_label = "Number of expressed genes" else: title = "Gene Filter" if measure == TotalCounts: axis_label = "Total counts" else: # TODO: Too long axis_label = "Number of cells a gene is expressed in" span = -1.0 # data span x = np.asarray(filter_data) if x.size: span = np.ptp(x) self._counts = x self.Warning.sampling_in_effect.clear() spinlow = self.threshold_stacks[0].widget(axis_on_input) spinhigh = self.threshold_stacks[1].widget(axis_on_input) if measure == TotalCounts: if span > 0: ndecimals = max(4 - int(np.floor(np.log10(span))), 1) else: ndecimals = 1 else: ndecimals = 1 spinlow.setDecimals(ndecimals) spinhigh.setDecimals(ndecimals) if x.size: xmin, xmax = np.min(x), np.max(x) self.limit_lower = np.clip(self.limit_lower, xmin, xmax) self.limit_upper = np.clip(self.limit_upper, xmin, xmax) if x.size > 0: # TODO: Need correction for lower bounded distribution (counts) # Use reflection around 0, but gaussian_kde does not provide # sufficient flexibility w.r.t bandwidth selection. self._plot.setData(x, 1000) self._plot.setBoundary(self.limit_lower, self.limit_upper) ax = self._plot.getAxis("left") # type: pg.AxisItem ax.setLabel(axis_label) self._plot.setTitle(title) self._update_info() def sizeHint(self): sh = super().sizeHint() # type: QSize return sh.expandedTo(QSize(800, 600)) def set_filter_type(self, type_): if self.selected_filter_type != type_: assert type_ in (Cells, Genes), str(type_) self.selected_filter_type = type_ self.threshold_stacks[0].setCurrentIndex(type_) self.threshold_stacks[1].setCurrentIndex(type_) if self.data_table_object is not None: self._setup(self.data_table_object, type_) def filter_type(self): return self.selected_filter_type def _update_metric(self): if self.data_table_object is not None: self._setup( self.data_table_object, self.selected_filter_type, ) def set_upper_limit_enabled(self, enabled): if enabled != self.limit_upper_enabled: self.limit_upper_enabled = enabled self.threshold_stacks[1].setEnabled(enabled) self.limit_upper_enabled_cb.setChecked(enabled) self._update_filter() def set_lower_limit_enabled(self, enabled): if enabled != self.limit_lower_enabled: self.limit_lower_enabled = enabled self.threshold_stacks[0].setEnabled(enabled) self.limit_lower_enabled_cb.setChecked(enabled) self._update_filter() def _update_filter(self): mode = 0 if self.limit_lower_enabled: mode |= ViolinPlot.Low if self.limit_upper_enabled: mode |= ViolinPlot.High self._plot.setSelectionMode(mode) def _is_filter_enabled(self): return self.limit_lower_enabled or self.limit_upper_enabled def clear(self): self._plot.clear() self._selection_data_obj = None self._counts_data_obj = None self._counts = None self._update_info() self.Warning.clear() def _update_info(self): text = [] if self.data_table_object: text.append('Input Data (object id): {}'.format( self.data_table_object.id)) if self._selection_data_obj and self._counts_data_obj: num_selected = self._selection_data_obj.output.get( 'num_selected', None) axis = self._counts_data_obj.input.get('axis', None) if num_selected is not None and axis is not None: text.append('Output data ({instance}{s}): {num} '.format( instance='gene' if axis == 0 else 'cell', s='s' if num_selected > 0 else '', num=num_selected)) self._info.setText('\n'.join(text)) def _select_all(self): self.limit_lower = 0 self.limit_upper = 2**31 - 1 self._limitchanged() def _update_dotplot(self): self._plot.setDataPointsVisible(self.display_dotplot) def current_filter_thresholds(self): if self.selected_filter_type in {Cells, Genes}: metric = self.selected_filter_metric else: metric = -1 return self.thresholds[self.selected_filter_type, metric] def set_current_filter_thesholds(self, lower, upper): if self.selected_filter_type in {Cells, Genes}: metric = self.selected_filter_metric else: metric = -1 self.thresholds[self.selected_filter_type, metric] = (lower, upper) @property def limit_lower(self): return self.current_filter_thresholds()[0] @limit_lower.setter def limit_lower(self, value): _, upper = self.current_filter_thresholds() self.set_current_filter_thesholds(value, upper) stacklower, _ = self.threshold_stacks sb = stacklower.widget(self.selected_filter_type) # prevent changes due to spin box rounding sb.setValue(value) @property def limit_upper(self): return self.current_filter_thresholds()[1] @limit_upper.setter def limit_upper(self, value): lower, _ = self.current_filter_thresholds() self.set_current_filter_thesholds(lower, value) _, stackupper = self.threshold_stacks sb = stackupper.widget(self.selected_filter_type) sb.setValue(value) @Slot() def _limitchanged(self): # Low/high limit changed via the spin boxes stacklow, stackhigh = self.threshold_stacks filter_ = self.selected_filter_type lower = stacklow.widget(filter_).value() upper = stackhigh.widget(filter_).value() self.set_current_filter_thesholds(lower, upper) if self._counts is not None and self._counts.size: xmin = np.min(self._counts) xmax = np.max(self._counts) self._plot.setBoundary(np.clip(lower, xmin, xmax), np.clip(upper, xmin, xmax)) def _limitchanged_plot(self): # Low/high limit changed via the plot if self._counts is not None: newlower, newupper = self._plot.boundary() filter_ = self.selected_filter_type lower, upper = self.current_filter_thresholds() stacklow, stackhigh = self.threshold_stacks spin_lower = stacklow.widget(filter_) spin_upper = stackhigh.widget(filter_) # do rounding to match the spin box's precision if self.limit_lower_enabled: newlower = round(newlower, spin_lower.decimals()) else: newlower = lower if self.limit_upper_enabled: newupper = round(newupper, spin_upper.decimals()) else: newupper = upper if self.limit_lower_enabled and newlower != lower: self.limit_lower = newlower if self.limit_upper_enabled and newupper != upper: self.limit_upper = newupper self._plot.setBoundary(newlower, newupper) def onDeleteWidget(self): self.data_table_object = None self.clear() self._plot.close() super().onDeleteWidget() @classmethod def migrate_settings(cls, settings, version): if (version is None or version < 2) and \ ("limit_lower" in settings and "limit_upper" in settings): # v2 changed limit_lower, limit_upper to per filter limits stored # in a single dict lower = settings.pop("limit_lower") upper = settings.pop("limit_upper") settings["thresholds"] = { (Cells, TotalCounts): (lower, upper), (Cells, DetectionCount): (lower, upper), (Genes, TotalCounts): (lower, upper), (Genes, DetectionCount): (lower, upper), } if version == 2: thresholds = settings["thresholds"] c = thresholds.pop(Cells) g = thresholds.pop(Genes) thresholds = { (Cells, TotalCounts): c, (Cells, DetectionCount): c, (Genes, TotalCounts): g, (Genes, DetectionCount): g, } settings["thresholds"] = thresholds
class PreviewBrowser(QWidget): """A Preview Browser for recent/premade scheme selection. """ # Emitted when the current previewed item changes currentIndexChanged = Signal(int) # Emitted when an item is double clicked in the preview list. activated = Signal(int) def __init__(self, *args): QWidget.__init__(self, *args) self.__model = None self.__currentIndex = -1 self.__template = DESCRIPTION_TEMPLATE self.__setupUi() def __setupUi(self): vlayout = QVBoxLayout() vlayout.setContentsMargins(0, 0, 0, 0) top_layout = QHBoxLayout() top_layout.setContentsMargins(12, 12, 12, 12) # Top row with full text description and a large preview # image. self.__label = QLabel(self, objectName="description-label", wordWrap=True, alignment=Qt.AlignTop | Qt.AlignLeft) self.__label.setWordWrap(True) self.__label.setFixedSize(220, PREVIEW_SIZE[1]) self.__image = QSvgWidget(self, objectName="preview-image") self.__image.setFixedSize(*PREVIEW_SIZE) self.__imageFrame = DropShadowFrame(self) self.__imageFrame.setWidget(self.__image) # Path text below the description and image path_layout = QHBoxLayout() path_layout.setContentsMargins(12, 0, 12, 0) path_label = QLabel("<b>{0!s}</b>".format(self.tr("Path:")), self, objectName="path-label") self.__path = TextLabel(self, objectName="path-text") path_layout.addWidget(path_label) path_layout.addWidget(self.__path) self.__selectAction = \ QAction(self.tr("Select"), self, objectName="select-action", ) top_layout.addWidget(self.__label, 1, alignment=Qt.AlignTop | Qt.AlignLeft) top_layout.addWidget(self.__image, 1, alignment=Qt.AlignTop | Qt.AlignRight) vlayout.addLayout(top_layout) vlayout.addLayout(path_layout) # An list view with small preview icons. self.__previewList = LinearIconView(objectName="preview-list-view") self.__previewList.doubleClicked.connect(self.__onDoubleClicked) vlayout.addWidget(self.__previewList) self.setLayout(vlayout) def setModel(self, model): """Set the item model for preview. """ if self.__model != model: if self.__model: s_model = self.__previewList.selectionModel() s_model.selectionChanged.disconnect(self.__onSelectionChanged) self.__model.dataChanged.disconnect(self.__onDataChanged) self.__model = model self.__previewList.setModel(model) if model: s_model = self.__previewList.selectionModel() s_model.selectionChanged.connect(self.__onSelectionChanged) self.__model.dataChanged.connect(self.__onDataChanged) if model and model.rowCount(): self.setCurrentIndex(0) def model(self): """Return the item model. """ return self.__model def setPreviewDelegate(self, delegate): """Set the delegate to render the preview images. """ raise NotImplementedError def setDescriptionTemplate(self, template): self.__template = template self.__update() def setCurrentIndex(self, index): """Set the selected preview item index. """ if self.__model is not None and self.__model.rowCount(): index = min(index, self.__model.rowCount() - 1) index = self.__model.index(index, 0) sel_model = self.__previewList.selectionModel() # This emits selectionChanged signal and triggers # __onSelectionChanged, currentIndex is updated there. sel_model.select(index, sel_model.ClearAndSelect) elif self.__currentIndex != -1: self.__currentIndex = -1 self.__update() self.currentIndexChanged.emit(-1) def currentIndex(self): """Return the current selected index. """ return self.__currentIndex def __onSelectionChanged(self, *args): """Selected item in the preview list has changed. Set the new description and large preview image. """ rows = self.__previewList.selectedIndexes() if rows: index = rows[0] self.__currentIndex = index.row() else: index = QModelIndex() self.__currentIndex = -1 self.__update() self.currentIndexChanged.emit(self.__currentIndex) def __onDataChanged(self, topleft, bottomRight): """Data changed, update the preview if current index in the changed range. """ if self.__currentIndex <= topleft.row() and \ self.__currentIndex >= bottomRight.row(): self.__update() def __onDoubleClicked(self, index): """Double click on an item in the preview item list. """ self.activated.emit(index.row()) def __update(self): """Update the current description. """ if self.__currentIndex != -1: index = self.model().index(self.__currentIndex, 0) else: index = QModelIndex() if not index.isValid(): description = "" name = "" path = "" svg = NO_PREVIEW_SVG else: description = str(index.data(Qt.WhatsThisRole)) if not description: description = "No description." description = escape(description) description = description.replace("\n", "<br/>") name = str(index.data(Qt.DisplayRole)) if not name: name = "Untitled" name = escape(name) path = str(index.data(Qt.StatusTipRole)) svg = str(index.data(previewmodel.ThumbnailSVGRole)) desc_text = self.__template.format(description=description, name=name) self.__label.setText(desc_text) self.__path.setText(path) if not svg: svg = NO_PREVIEW_SVG if svg: self.__image.load(QByteArray(svg.encode("utf-8")))
class MessageWidget(QWidget): """ A widget displaying a simple message to the user. This is an alternative to a full QMessageBox intended for inline modeless messages. [[icon] {Message text} (Ok) (Cancel)] """ #: Emitted when a button with the AcceptRole is clicked accepted = Signal() #: Emitted when a button with the RejectRole is clicked rejected = Signal() #: Emitted when a button with the HelpRole is clicked helpRequested = Signal() #: Emitted when a button is clicked clicked = Signal(QAbstractButton) class StandardButton(enum.IntEnum): NoButton, Ok, Close, Help = 0x0, 0x1, 0x2, 0x4 NoButton, Ok, Close, Help = list(StandardButton) class ButtonRole(enum.IntEnum): InvalidRole, AcceptRole, RejectRole, HelpRole = 0, 1, 2, 3 InvalidRole, AcceptRole, RejectRole, HelpRole = list(ButtonRole) _Button = namedtuple("_Button", ["button", "role", "stdbutton"]) def __init__(self, parent=None, icon=QIcon(), text="", wordWrap=False, textFormat=Qt.AutoText, standardButtons=NoButton, **kwargs): super().__init__(parent, **kwargs) self.__text = text self.__icon = QIcon() self.__wordWrap = wordWrap self.__standardButtons = MessageWidget.NoButton self._buttons = [] layout = QHBoxLayout() layout.setContentsMargins(8, 0, 8, 0) self.__iconlabel = QLabel(objectName="icon-label") self.__iconlabel.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.__textlabel = QLabel(objectName="text-label", text=text, wordWrap=wordWrap, textFormat=textFormat) if sys.platform == "darwin": self.__textlabel.setAttribute(Qt.WA_MacSmallSize) layout.addWidget(self.__iconlabel) layout.addWidget(self.__textlabel) self.setLayout(layout) self.setIcon(icon) self.setStandardButtons(standardButtons) def setText(self, text): """ Set the current message text. :type message: str """ if self.__text != text: self.__text = text self.__textlabel.setText(text) def text(self): """ Return the current message text. :rtype: str """ return self.__text def setIcon(self, icon): """ Set the message icon. :type icon: QIcon | QPixmap | QString | QStyle.StandardPixmap """ if isinstance(icon, QStyle.StandardPixmap): icon = self.style().standardIcon(icon) else: icon = QIcon(icon) if self.__icon != icon: self.__icon = QIcon(icon) if not self.__icon.isNull(): size = self.style().pixelMetric(QStyle.PM_SmallIconSize, None, self) pm = self.__icon.pixmap(QSize(size, size)) else: pm = QPixmap() self.__iconlabel.setPixmap(pm) self.__iconlabel.setVisible(not pm.isNull()) def icon(self): """ Return the current icon. :rtype: QIcon """ return QIcon(self.__icon) def setWordWrap(self, wordWrap): """ Set the message text wrap property :type wordWrap: bool """ if self.__wordWrap != wordWrap: self.__wordWrap = wordWrap self.__textlabel.setWordWrap(wordWrap) def wordWrap(self): """ Return the message text wrap property. :rtype: bool """ return self.__wordWrap def setTextFormat(self, textFormat): """ Set message text format :type textFormat: Qt.TextFormat """ self.__textlabel.setTextFormat(textFormat) def textFormat(self): """ Return the message text format. :rtype: Qt.TextFormat """ return self.__textlabel.textFormat() def changeEvent(self, event): # reimplemented if event.type() == 177: # QEvent.MacSizeChange: ... super().changeEvent(event) def setStandardButtons(self, buttons): for button in MessageWidget.StandardButton: existing = self.button(button) if button & buttons and existing is None: self.addButton(button) elif existing is not None: self.removeButton(existing) def standardButtons(self): return functools.reduce( operator.ior, (slot.stdbutton for slot in self._buttons if slot.stdbutton is not None), MessageWidget.NoButton) def addButton(self, button, *rolearg): """ addButton(QAbstractButton, ButtonRole) addButton(str, ButtonRole) addButton(StandardButton) Add and return a button """ stdbutton = None if isinstance(button, QAbstractButton): if len(rolearg) != 1: raise TypeError("Wrong number of arguments for " "addButton(QAbstractButton, role)") role = rolearg[0] elif isinstance(button, MessageWidget.StandardButton): if len(rolearg) != 0: raise TypeError("Wrong number of arguments for " "addButton(StandardButton)") stdbutton = button if button == MessageWidget.Ok: role = MessageWidget.AcceptRole button = QPushButton("Ok", default=False, autoDefault=False) elif button == MessageWidget.Close: role = MessageWidget.RejectRole # button = QPushButton( # default=False, autoDefault=False, flat=True, # icon=QIcon(self.style().standardIcon( # QStyle.SP_TitleBarCloseButton))) button = SimpleButton(icon=QIcon(self.style().standardIcon( QStyle.SP_TitleBarCloseButton))) elif button == MessageWidget.Help: role = MessageWidget.HelpRole button = QPushButton("Help", default=False, autoDefault=False) elif isinstance(button, str): if len(rolearg) != 1: raise TypeError("Wrong number of arguments for " "addButton(str, ButtonRole)") role = rolearg[0] button = QPushButton(button, default=False, autoDefault=False) if sys.platform == "darwin": button.setAttribute(Qt.WA_MacSmallSize) self._buttons.append(MessageWidget._Button(button, role, stdbutton)) button.clicked.connect(self._button_clicked) self._relayout() return button def removeButton(self, button): """ Remove a `button`. :type button: QAbstractButton """ slot = [s for s in self._buttons if s.button is button] if slot: slot = slot[0] self._buttons.remove(slot) self.layout().removeWidget(slot.button) slot.button.setParent(None) def buttonRole(self, button): """ Return the ButtonRole for button :type button: QAbstractButton """ for slot in self._buttons: if slot.button is button: return slot.role else: return MessageWidget.InvalidRole def button(self, standardButton): """ Return the button for the StandardButton. :type standardButton: StandardButton """ for slot in self._buttons: if slot.stdbutton == standardButton: return slot.button else: return None def _button_clicked(self): button = self.sender() role = self.buttonRole(button) self.clicked.emit(button) if role == MessageWidget.AcceptRole: self.accepted.emit() self.close() elif role == MessageWidget.RejectRole: self.rejected.emit() self.close() elif role == MessageWidget.HelpRole: self.helpRequested.emit() def _relayout(self): for slot in self._buttons: self.layout().removeWidget(slot.button) order = { MessageOverlayWidget.HelpRole: 0, MessageOverlayWidget.AcceptRole: 2, MessageOverlayWidget.RejectRole: 3, } orderd = sorted(self._buttons, key=lambda slot: order.get(slot.role, -1)) prev = self.__textlabel for slot in orderd: self.layout().addWidget(slot.button) QWidget.setTabOrder(prev, slot.button)
class PreviewBrowser(QWidget): """ A Preview Browser for recent/example workflow selection. """ # Emitted when the current previewed item changes currentIndexChanged = Signal(int) # Emitted when an item is double clicked in the preview list. activated = Signal(int) def __init__(self, *args, heading="", previewMargins=12, **kwargs): # type: (Any, str, int, Any) -> None super().__init__(*args, **kwargs) self.__model = None # type: Optional[QAbstractItemModel] self.__currentIndex = -1 self.__template = DESCRIPTION_TEMPLATE self.__margin = previewMargins vlayout = QVBoxLayout() vlayout.setContentsMargins(0, 0, 0, 0) top_layout = QVBoxLayout(objectName="top-layout") margin = self.__margin top_layout.setContentsMargins(margin, margin, margin, margin) # Optional heading label self.__heading = QLabel(self, objectName="heading", visible=False) # Horizontal row with full text description and a large preview # image. hlayout = QHBoxLayout() hlayout.setContentsMargins(0, 0, 0, 0) self.__label = QLabel(self, objectName="description-label", wordWrap=True, alignment=Qt.AlignTop | Qt.AlignLeft) self.__label.setWordWrap(True) self.__label.setFixedSize(220, PREVIEW_SIZE[1]) self.__label.setMinimumWidth(PREVIEW_SIZE[0] // 2) self.__label.setMaximumHeight(PREVIEW_SIZE[1]) self.__image = QSvgWidget(self, objectName="preview-image") self.__image.setFixedSize(*PREVIEW_SIZE) self.__imageFrame = DropShadowFrame(self) self.__imageFrame.setWidget(self.__image) hlayout.addWidget(self.__label) hlayout.addWidget(self.__image) # Path text below the description and image path_layout = QHBoxLayout() path_layout.setContentsMargins(0, 0, 0, 0) path_label = QLabel("<b>{0!s}</b>".format(self.tr("Path:")), self, objectName="path-label") self.__path = TextLabel(self, objectName="path-text") path_layout.addWidget(path_label) path_layout.addWidget(self.__path) top_layout.addWidget(self.__heading) top_layout.addLayout(hlayout) top_layout.addLayout(path_layout) vlayout.addLayout(top_layout) # An list view with small preview icons. self.__previewList = LinearIconView(objectName="preview-list-view", wordWrap=True) self.__previewList.doubleClicked.connect(self.__onDoubleClicked) vlayout.addWidget(self.__previewList) self.setLayout(vlayout) self.setHeading(heading) def setHeading(self, text): # type: (str) -> None """ Set the heading text. Parameters ---------- text: str The new heading text. If empty the heading is hidden. """ self.__heading.setVisible(bool(text)) self.__heading.setText(text) def setPreviewMargins(self, margin): # type: (int) -> None """ Set the left, top and right margins of the top widget part (heading and description) Parameters ---------- margin : int Margin """ if margin != self.__margin: layout = self.layout().itemAt(0).layout() assert isinstance(layout, QVBoxLayout) assert layout.objectName() == "top-layout" layout.setContentsMargins(margin, margin, margin, 0) def setModel(self, model): # type: (QAbstractItemModel) -> None """ Set the item model for preview. Parameters ---------- model : QAbstractItemModel """ if self.__model != model: if self.__model: s_model = self.__previewList.selectionModel() s_model.selectionChanged.disconnect(self.__onSelectionChanged) self.__model.dataChanged.disconnect(self.__onDataChanged) self.__model = model self.__previewList.setModel(model) if model: s_model = self.__previewList.selectionModel() s_model.selectionChanged.connect(self.__onSelectionChanged) self.__model.dataChanged.connect(self.__onDataChanged) if model and model.rowCount(): self.setCurrentIndex(0) def model(self): # type: () -> Optional[QAbstractItemModel] """ Return the item model. """ return self.__model def setDescriptionTemplate(self, template): self.__template = template self.__update() def setCurrentIndex(self, index): # type: (int) -> None """ Set the selected preview item index. Parameters ---------- index : int The current selected index. """ if self.__model is not None and self.__model.rowCount(): index = min(index, self.__model.rowCount() - 1) index = self.__model.index(index, 0) sel_model = self.__previewList.selectionModel() # This emits selectionChanged signal and triggers # __onSelectionChanged, currentIndex is updated there. sel_model.select(index, sel_model.ClearAndSelect) elif self.__currentIndex != -1: self.__currentIndex = -1 self.__update() self.currentIndexChanged.emit(-1) def currentIndex(self): # type: () -> int """ Return the current selected index. """ return self.__currentIndex def __onSelectionChanged(self): # type: () -> None """Selected item in the preview list has changed. Set the new description and large preview image. """ rows = self.__previewList.selectedIndexes() if rows: index = rows[0] self.__currentIndex = index.row() else: self.__currentIndex = -1 self.__update() self.currentIndexChanged.emit(self.__currentIndex) def __onDataChanged(self, topLeft, bottomRight): # type: (QModelIndex, QModelIndex) -> None """Data changed, update the preview if current index in the changed range. """ if topLeft.row() <= self.__currentIndex <= bottomRight.row(): self.__update() def __onDoubleClicked(self, index): # type: (QModelIndex) -> None """Double click on an item in the preview item list. """ self.activated.emit(index.row()) def __update(self): # type: () -> None """Update the current description. """ if self.__currentIndex != -1 and self.__model is not None: index = self.__model.index(self.__currentIndex, 0) else: index = QModelIndex() if not index.isValid(): description = "" name = "" path = "" svg = NO_PREVIEW_SVG else: description = index.data(Qt.WhatsThisRole) if description: description = description else: description = "没有说明。" description = escape(description) description = description.replace("\n", "<br/>") name = index.data(Qt.DisplayRole) if name: name = name else: name = "Untitled" name = escape(name) path = str(index.data(Qt.StatusTipRole)) svg = str(index.data(previewmodel.ThumbnailSVGRole)) desc_text = self.__template.format(description=description, name=name) self.__label.setText(desc_text) self.__path.setText(contractuser(path)) if not svg: svg = NO_PREVIEW_SVG if svg: self.__image.load(QByteArray(svg.encode("utf-8")))