Пример #1
0
    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_()
Пример #2
0
    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()
Пример #3
0
    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_()
Пример #4
0
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)
Пример #5
0
    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()
Пример #6
0
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")))
Пример #7
0
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")))
Пример #8
0
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)
Пример #9
0
    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_()
Пример #10
0
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_()
Пример #12
0
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
Пример #13
0
    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_()
Пример #14
0
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")))
Пример #15
0
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)
Пример #16
0
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")))
    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_()