Beispiel #1
0
    def unfold_menu(self):
        """
        展开/收起菜单
        通过修改左侧widget的最大宽度来实现,展开时设置为180,
        收起时设置为60,刚好是一个图标左右的宽度
        """

        self.tree_menu_action = QPropertyAnimation(self.widget,
                                                   b'maximumWidth')
        self.tree_menu_action.stop()
        mini_width = 50
        start = self.widget.width()
        if self.widget.width() > mini_width:
            icon = self.resource.font_icon('ei.chevron-right', color="#d2d2d2")
            end = mini_width
            self.menu_is_close = True
            # 在收缩的时候看看是不是有列表是展开的
            # 如果有则先对齐收缩再收缩左侧导航栏
            if self.expanded_item:
                self.expanded_item.setExpanded(False)
        else:
            icon = self.resource.font_icon('ei.chevron-left', color="#d2d2d2")
            end = 180
            self.menu_is_close = False
        self.pushButton_8.setIcon(icon)
        self.tree_menu_action.setStartValue(start)
        self.tree_menu_action.setEndValue(end)
        self.tree_menu_action.setEasingCurve(QEasingCurve.InOutBack)
        self.tree_menu_action.setDuration(500)
        self.tree_menu_action.start()
Beispiel #2
0
 def _fade_setup(self):
     """ """
     self._fade_running = True
     self.effect = QGraphicsOpacityEffect(self)
     self.setGraphicsEffect(self.effect)
     self.anim = QPropertyAnimation(self.effect,
                                    to_binary_string("opacity"))
Beispiel #3
0
def add_flash_animation(widget: QWidget,
                        duration: int = 300,
                        color: Array = (0.5, 0.5, 0.5, 0.5)):
    """Add flash animation to widget to highlight certain action (e.g. taking a screenshot).

    Parameters
    ----------
    widget : QWidget
        Any Qt widget.
    duration : int
        Duration of the flash animation.
    color : Array
        Color of the flash animation. By default, we use light gray.
    """
    color = transform_color(color)[0]
    color = (255 * color).astype("int")

    effect = QGraphicsColorizeEffect(widget)
    widget.setGraphicsEffect(effect)

    widget._flash_animation = QPropertyAnimation(effect, b"color")
    widget._flash_animation.setStartValue(QColor(0, 0, 0, 0))
    widget._flash_animation.setEndValue(QColor(0, 0, 0, 0))
    widget._flash_animation.setLoopCount(1)

    # let's make sure to remove the animation from the widget because
    # if we don't, the widget will actually be black and white.
    widget._flash_animation.finished.connect(
        partial(remove_flash_animation, weakref.ref(widget)))

    widget._flash_animation.start()

    # now  set an actual time for the flashing and an intermediate color
    widget._flash_animation.setDuration(duration)
    widget._flash_animation.setKeyValueAt(0.1, QColor(*color))
Beispiel #4
0
    def __init__(
        self,
        message: str,
        severity: Union[
            str, NotificationSeverity
        ] = NotificationSeverity.WARNING,
        source: Optional[str] = None,
        actions: ActionSequence = (),
    ):
        """[summary]


        """
        super().__init__(None)
        # FIXME: this does not work with multiple viewers.
        # we need a way to detect the viewer in which the error occured.
        for wdg in QApplication.topLevelWidgets():
            if isinstance(wdg, QMainWindow):
                try:
                    # TODO: making the canvas the parent makes it easier to
                    # move/resize, but also means that the notification can get
                    # clipped on the left if the canvas is too small.
                    canvas = wdg.centralWidget().children()[1].canvas.native
                    self.setParent(canvas)
                    canvas.resized.connect(self.move_to_bottom_right)
                    break
                except Exception:
                    pass
        self.setupUi()
        self.setAttribute(Qt.WA_DeleteOnClose)
        self.setup_buttons(actions)
        self.setMouseTracking(True)

        self.severity_icon.setText(NotificationSeverity(severity).as_icon())
        self.message.setText(message)
        if source:
            self.source_label.setText(f'Source: {source}')

        self.close_button.clicked.connect(self.close)
        self.expand_button.clicked.connect(self.toggle_expansion)

        self.timer = None
        self.opacity = QGraphicsOpacityEffect()
        self.setGraphicsEffect(self.opacity)
        self.opacity_anim = QPropertyAnimation(self.opacity, b"opacity", self)
        self.geom_anim = QPropertyAnimation(self, b"geometry", self)
        self.move_to_bottom_right()
Beispiel #5
0
    def __init__(self):
        super(TourTestWindow, self).__init__()
        self.setGeometry(300, 100, 400, 600)
        self.setWindowTitle('Exploring QMainWindow')

        self.exit = QAction('Exit', self)
        self.exit.setStatusTip('Exit program')

        # create the menu bar
        menubar = self.menuBar()
        file_ = menubar.addMenu('&File')
        file_.addAction(self.exit)

        # create the status bar
        self.statusBar()

        # QWidget or its instance needed for box layout
        self.widget = QWidget(self)

        self.button = QPushButton('test')
        self.button1 = QPushButton('1')
        self.button2 = QPushButton('2')

        effect = QGraphicsOpacityEffect(self.button2)
        self.button2.setGraphicsEffect(effect)
        self.anim = QPropertyAnimation(effect, to_binary_string("opacity"))
        self.anim.setStartValue(0.01)
        self.anim.setEndValue(1.0)
        self.anim.setDuration(500)

        lay = QVBoxLayout()
        lay.addWidget(self.button)
        lay.addStretch()
        lay.addWidget(self.button1)
        lay.addWidget(self.button2)

        self.widget.setLayout(lay)

        self.setCentralWidget(self.widget)
        self.button.clicked.connect(self.action1)
        self.button1.clicked.connect(self.action2)

        self.tour = AnimatedTour(self)
Beispiel #6
0
    def __init__(
        self,
        message: str,
        severity: Union[str, NotificationSeverity] = 'WARNING',
        source: Optional[str] = None,
        actions: ActionSequence = (),
    ):
        super().__init__()

        from ..qt_main_window import _QtMainWindow

        current_window = _QtMainWindow.current()
        if current_window is not None:
            canvas = current_window.qt_viewer._canvas_overlay
            self.setParent(canvas)
            canvas.resized.connect(self.move_to_bottom_right)

        self.setupUi()
        self.setAttribute(Qt.WA_DeleteOnClose)
        self.setup_buttons(actions)
        self.setMouseTracking(True)

        self.severity_icon.setText(NotificationSeverity(severity).as_icon())
        self.message.setText(message)
        if source:
            self.source_label.setText(
                trans._('Source: {source}', source=source)
            )

        self.close_button.clicked.connect(self.close)
        self.expand_button.clicked.connect(self.toggle_expansion)

        self.timer = QTimer()
        self.opacity = QGraphicsOpacityEffect()
        self.setGraphicsEffect(self.opacity)
        self.opacity_anim = QPropertyAnimation(self.opacity, b"opacity", self)
        self.geom_anim = QPropertyAnimation(self, b"geometry", self)
        self.move_to_bottom_right()
Beispiel #7
0
    def __init__(self):
        super(TourTestWindow, self).__init__()
        self.setGeometry(300, 100, 400, 600)
        self.setWindowTitle('Exploring QMainWindow')

        self.exit = QAction('Exit', self)
        self.exit.setStatusTip('Exit program')

        # create the menu bar
        menubar = self.menuBar()
        file_ = menubar.addMenu('&File')
        file_.addAction(self.exit)

        # create the status bar
        self.statusBar()

        # QWidget or its instance needed for box layout
        self.widget = QWidget(self)

        self.button = QPushButton('test')
        self.button1 = QPushButton('1')
        self.button2 = QPushButton('2')

        effect = QGraphicsOpacityEffect(self.button2)
        self.button2.setGraphicsEffect(effect)
        self.anim = QPropertyAnimation(effect, to_binary_string("opacity"))
        self.anim.setStartValue(0.01)
        self.anim.setEndValue(1.0)
        self.anim.setDuration(500)

        lay = QVBoxLayout()
        lay.addWidget(self.button)
        lay.addStretch()
        lay.addWidget(self.button1)
        lay.addWidget(self.button2)

        self.widget.setLayout(lay)

        self.setCentralWidget(self.widget)
        self.button.clicked.connect(self.action1)
        self.button1.clicked.connect(self.action2)

        self.tour = AnimatedTour(self)
Beispiel #8
0
 def set_switch_animation(self,
                          callback=None,
                          duration=200,
                          start=1,
                          end=0,
                          animation=None):
     """设置切换效果"""
     self.fade_out_animation = animation or self.fade_out_animation or QPropertyAnimation(
         self, b'windowOpacity')
     while self.signals:
         key, slot = self.signals.popitem()
         try:
             self.fade_out_animation.finished.disconnect(slot)
         except TypeError:
             pass
     self.fade_out_animation.stop()
     self.fade_out_animation.setDuration(duration)
     self.fade_out_animation.setStartValue(start)
     self.fade_out_animation.setEndValue(end)
     if callback:
         self.signals.update({id(callback): callback})
         self.fade_out_animation.finished.connect(callback)
     self.fade_out_animation.start()
Beispiel #9
0
class NapariQtNotification(QDialog):
    """Notification dialog frame, appears at the bottom right of the canvas.

    By default, only the first line of the notification is shown, and the text
    is elided.  Double-clicking on the text (or clicking the chevron icon) will
    expand to show the full notification.  The dialog will autmatically
    disappear in ``DISMISS_AFTER`` milliseconds, unless hovered or clicked.

    Parameters
    ----------
    message : str
        The message that will appear in the notification
    severity : str or NotificationSeverity, optional
        Severity level {'error', 'warning', 'info', 'none'}.  Will determine
        the icon associated with the message.
        by default NotificationSeverity.WARNING.
    source : str, optional
        A source string for the notifcation (intended to show the module and
        or package responsible for the notification), by default None
    actions : list of tuple, optional
        A sequence of 2-tuples, where each tuple is a string and a callable.
        Each tuple will be used to create button in the dialog, where the text
        on the button is determine by the first item in the tuple, and a
        callback function to call when the button is pressed is the second item
        in the tuple. by default ()
    """

    MAX_OPACITY = 0.9
    FADE_IN_RATE = 220
    FADE_OUT_RATE = 120
    DISMISS_AFTER = 4000
    MIN_WIDTH = 400

    message: MultilineElidedLabel
    source_label: QLabel
    severity_icon: QLabel

    def __init__(
        self,
        message: str,
        severity: Union[str, NotificationSeverity] = 'WARNING',
        source: Optional[str] = None,
        actions: ActionSequence = (),
    ):
        """[summary]"""
        super().__init__(None)

        # FIXME: this does not work with multiple viewers.
        # we need a way to detect the viewer in which the error occured.
        for wdg in QApplication.topLevelWidgets():
            if isinstance(wdg, QMainWindow):
                try:
                    # TODO: making the canvas the parent makes it easier to
                    # move/resize, but also means that the notification can get
                    # clipped on the left if the canvas is too small.
                    canvas = wdg.centralWidget().children()[1].canvas.native
                    self.setParent(canvas)
                    canvas.resized.connect(self.move_to_bottom_right)
                    break
                except Exception:
                    pass
        self.setupUi()
        self.setAttribute(Qt.WA_DeleteOnClose)
        self.setup_buttons(actions)
        self.setMouseTracking(True)

        self.severity_icon.setText(NotificationSeverity(severity).as_icon())
        self.message.setText(message)
        if source:
            self.source_label.setText(
                trans._('Source: {source}', source=source)
            )

        self.close_button.clicked.connect(self.close)
        self.expand_button.clicked.connect(self.toggle_expansion)

        self.timer = QTimer()
        self.opacity = QGraphicsOpacityEffect()
        self.setGraphicsEffect(self.opacity)
        self.opacity_anim = QPropertyAnimation(self.opacity, b"opacity", self)
        self.geom_anim = QPropertyAnimation(self, b"geometry", self)
        self.move_to_bottom_right()

    def move_to_bottom_right(self, offset=(8, 8)):
        """Position widget at the bottom right edge of the parent."""
        if not self.parent():
            return
        sz = self.parent().size() - self.size() - QSize(*offset)
        self.move(QPoint(sz.width(), sz.height()))

    def slide_in(self):
        """Run animation that fades in the dialog with a slight slide up."""
        geom = self.geometry()
        self.geom_anim.setDuration(self.FADE_IN_RATE)
        self.geom_anim.setStartValue(geom.translated(0, 20))
        self.geom_anim.setEndValue(geom)
        self.geom_anim.setEasingCurve(QEasingCurve.OutQuad)
        # fade in
        self.opacity_anim.setDuration(self.FADE_IN_RATE)
        self.opacity_anim.setStartValue(0)
        self.opacity_anim.setEndValue(self.MAX_OPACITY)
        self.geom_anim.start()
        self.opacity_anim.start()

    def show(self):
        """Show the message with a fade and slight slide in from the bottom."""
        super().show()
        self.slide_in()
        if self.DISMISS_AFTER > 0:
            self.timer.setInterval(self.DISMISS_AFTER)
            self.timer.setSingleShot(True)
            self.timer.timeout.connect(self.close)
        self.timer.start()

    def mouseMoveEvent(self, event):
        """On hover, stop the self-destruct timer"""
        self.timer.stop()

    def mouseDoubleClickEvent(self, event):
        """Expand the notification on double click."""
        self.toggle_expansion()

    def close(self):
        """Fade out then close."""
        self.opacity_anim.setDuration(self.FADE_OUT_RATE)
        self.opacity_anim.setStartValue(self.MAX_OPACITY)
        self.opacity_anim.setEndValue(0)
        self.opacity_anim.start()
        self.opacity_anim.finished.connect(super().close)

    def toggle_expansion(self):
        """Toggle the expanded state of the notification frame."""
        self.contract() if self.property('expanded') else self.expand()
        self.timer.stop()

    def expand(self):
        """Expanded the notification so that the full message is visible."""
        curr = self.geometry()
        self.geom_anim.setDuration(100)
        self.geom_anim.setStartValue(curr)
        new_height = self.sizeHint().height()
        delta = new_height - curr.height()
        self.geom_anim.setEndValue(
            QRect(curr.x(), curr.y() - delta, curr.width(), new_height)
        )
        self.geom_anim.setEasingCurve(QEasingCurve.OutQuad)
        self.geom_anim.start()
        self.setProperty('expanded', True)
        self.style().unpolish(self.expand_button)
        self.style().polish(self.expand_button)

    def contract(self):
        """Contract notification to a single elided line of the message."""
        geom = self.geometry()
        self.geom_anim.setDuration(100)
        self.geom_anim.setStartValue(geom)
        dlt = geom.height() - self.minimumHeight()
        self.geom_anim.setEndValue(
            QRect(geom.x(), geom.y() + dlt, geom.width(), geom.height() - dlt)
        )
        self.geom_anim.setEasingCurve(QEasingCurve.OutQuad)
        self.geom_anim.start()
        self.setProperty('expanded', False)
        self.style().unpolish(self.expand_button)
        self.style().polish(self.expand_button)

    def setupUi(self):
        """Set up the UI during initialization."""
        self.setWindowFlags(Qt.SubWindow)
        self.setMinimumWidth(self.MIN_WIDTH)
        self.setMaximumWidth(self.MIN_WIDTH)
        self.setMinimumHeight(40)
        self.setSizeGripEnabled(False)
        self.setModal(False)
        self.verticalLayout = QVBoxLayout(self)
        self.verticalLayout.setContentsMargins(2, 2, 2, 2)
        self.verticalLayout.setSpacing(0)

        self.row1_widget = QWidget(self)
        self.row1 = QHBoxLayout(self.row1_widget)
        self.row1.setContentsMargins(12, 12, 12, 8)
        self.row1.setSpacing(4)
        self.severity_icon = QLabel(self.row1_widget)
        self.severity_icon.setObjectName("severity_icon")
        self.severity_icon.setMinimumWidth(30)
        self.severity_icon.setMaximumWidth(30)
        self.row1.addWidget(self.severity_icon, alignment=Qt.AlignTop)
        self.message = MultilineElidedLabel(self.row1_widget)
        self.message.setMinimumWidth(self.MIN_WIDTH - 200)
        self.message.setSizePolicy(
            QSizePolicy.Expanding, QSizePolicy.Expanding
        )
        self.row1.addWidget(self.message, alignment=Qt.AlignTop)
        self.expand_button = QPushButton(self.row1_widget)
        self.expand_button.setObjectName("expand_button")
        self.expand_button.setCursor(Qt.PointingHandCursor)
        self.expand_button.setMaximumWidth(20)
        self.expand_button.setFlat(True)

        self.row1.addWidget(self.expand_button, alignment=Qt.AlignTop)
        self.close_button = QPushButton(self.row1_widget)
        self.close_button.setObjectName("close_button")
        self.close_button.setCursor(Qt.PointingHandCursor)
        self.close_button.setMaximumWidth(20)
        self.close_button.setFlat(True)

        self.row1.addWidget(self.close_button, alignment=Qt.AlignTop)
        self.verticalLayout.addWidget(self.row1_widget, 1)
        self.row2_widget = QWidget(self)
        self.row2_widget.hide()
        self.row2 = QHBoxLayout(self.row2_widget)
        self.source_label = QLabel(self.row2_widget)
        self.source_label.setObjectName("source_label")
        self.row2.addWidget(self.source_label, alignment=Qt.AlignBottom)
        self.row2.addStretch()
        self.row2.setContentsMargins(12, 2, 16, 12)
        self.row2_widget.setMaximumHeight(34)
        self.row2_widget.setStyleSheet(
            'QPushButton{'
            'padding: 4px 12px 4px 12px; '
            'font-size: 11px;'
            'min-height: 18px; border-radius: 0;}'
        )
        self.verticalLayout.addWidget(self.row2_widget, 0)
        self.setProperty('expanded', False)
        self.resize(self.MIN_WIDTH, 40)

    def setup_buttons(self, actions: ActionSequence = ()):
        """Add buttons to the dialog.

        Parameters
        ----------
        actions : tuple, optional
            A sequence of 2-tuples, where each tuple is a string and a
            callable. Each tuple will be used to create button in the dialog,
            where the text on the button is determine by the first item in the
            tuple, and a callback function to call when the button is pressed
            is the second item in the tuple. by default ()
        """
        if isinstance(actions, dict):
            actions = list(actions.items())

        for text, callback in actions:
            btn = QPushButton(text)

            def call_back_with_self(callback, self):
                """
                we need a higher order function this to capture the reference to self.
                """

                def _inner():
                    return callback(self)

                return _inner

            btn.clicked.connect(call_back_with_self(callback, self))
            btn.clicked.connect(self.close)
            self.row2.addWidget(btn)
        if actions:
            self.row2_widget.show()
            self.setMinimumHeight(
                self.row2_widget.maximumHeight() + self.minimumHeight()
            )

    def sizeHint(self):
        """Return the size required to show the entire message."""
        return QSize(
            super().sizeHint().width(),
            self.row2_widget.height() + self.message.sizeHint().height(),
        )

    @classmethod
    def from_notification(
        cls, notification: Notification
    ) -> NapariQtNotification:

        from ...utils.notifications import ErrorNotification

        actions = notification.actions

        if isinstance(notification, ErrorNotification):

            def show_tb(parent):
                tbdialog = QDialog(parent=parent.parent())
                tbdialog.setModal(True)
                # this is about the minimum width to not get rewrap
                # and the minimum height to not have scrollbar
                tbdialog.resize(650, 270)
                tbdialog.setLayout(QVBoxLayout())

                text = QTextEdit()
                text.setHtml(notification.as_html())
                text.setReadOnly(True)
                btn = QPushButton('Enter Debugger')

                def _enter_debug_mode():
                    btn.setText(
                        'Now Debugging. Please quit debugger in console '
                        'to continue'
                    )
                    _debug_tb(notification.exception.__traceback__)
                    btn.setText('Enter Debugger')

                btn.clicked.connect(_enter_debug_mode)
                tbdialog.layout().addWidget(text)
                tbdialog.layout().addWidget(btn, 0, Qt.AlignRight)
                tbdialog.show()

            actions = tuple(notification.actions) + (
                (trans._('View Traceback'), show_tb),
            )
        else:
            actions = notification.actions

        return cls(
            message=notification.message,
            severity=notification.severity,
            source=notification.source,
            actions=actions,
        )

    @classmethod
    def show_notification(cls, notification: Notification):
        from ...utils.settings import SETTINGS

        # after https://github.com/napari/napari/issues/2370,
        # the os.getenv can be removed (and NAPARI_CATCH_ERRORS retired)
        if (
            os.getenv("NAPARI_CATCH_ERRORS") not in ('0', 'False')
            and notification.severity
            >= SETTINGS.application.gui_notification_level
        ):
            cls.from_notification(notification).show()
Beispiel #10
0
class TourTestWindow(QMainWindow):
    """ """
    sig_resized = Signal("QResizeEvent")
    sig_moved = Signal("QMoveEvent")

    def __init__(self):
        super(TourTestWindow, self).__init__()
        self.setGeometry(300, 100, 400, 600)
        self.setWindowTitle('Exploring QMainWindow')

        self.exit = QAction('Exit', self)
        self.exit.setStatusTip('Exit program')

        # create the menu bar
        menubar = self.menuBar()
        file_ = menubar.addMenu('&File')
        file_.addAction(self.exit)

        # create the status bar
        self.statusBar()

        # QWidget or its instance needed for box layout
        self.widget = QWidget(self)

        self.button = QPushButton('test')
        self.button1 = QPushButton('1')
        self.button2 = QPushButton('2')

        effect = QGraphicsOpacityEffect(self.button2)
        self.button2.setGraphicsEffect(effect)
        self.anim = QPropertyAnimation(effect, to_binary_string("opacity"))
        self.anim.setStartValue(0.01)
        self.anim.setEndValue(1.0)
        self.anim.setDuration(500)

        lay = QVBoxLayout()
        lay.addWidget(self.button)
        lay.addStretch()
        lay.addWidget(self.button1)
        lay.addWidget(self.button2)

        self.widget.setLayout(lay)

        self.setCentralWidget(self.widget)
        self.button.clicked.connect(self.action1)
        self.button1.clicked.connect(self.action2)

        self.tour = AnimatedTour(self)

    def action1(self):
        """ """
        frames = get_tour('test')
        index = 0
        dic = {'last': 0, 'tour': frames}
        self.tour.set_tour(index, dic, self)
        self.tour.start_tour()

    def action2(self):
        """ """
        self.anim.start()

    def resizeEvent(self, event):
        """Reimplement Qt method"""
        QMainWindow.resizeEvent(self, event)
        self.sig_resized.emit(event)

    def moveEvent(self, event):
        """Reimplement Qt method"""
        QMainWindow.moveEvent(self, event)
        self.sig_moved.emit(event)
Beispiel #11
0
 def _fade_setup(self):
     """ """
     self._fade_running = True
     self.effect = QGraphicsOpacityEffect(self)
     self.setGraphicsEffect(self.effect)
     self.anim = QPropertyAnimation(self.effect, to_binary_string("opacity"))
Beispiel #12
0
class FadingDialog(QDialog):
    """A general fade in/fade out QDialog with some builtin functions"""
    sig_key_pressed = Signal()

    def __init__(self, parent, opacity, duration, easing_curve):
        super(FadingDialog, self).__init__(parent)

        self.parent = parent
        self.opacity_min = min(opacity)
        self.opacity_max = max(opacity)
        self.duration_fadein = duration[0]
        self.duration_fadeout = duration[-1]
        self.easing_curve_in = easing_curve[0]
        self.easing_curve_out = easing_curve[-1]
        self.effect = None
        self.anim = None

        self._fade_running = False
        self._funcs_before_fade_in = []
        self._funcs_after_fade_in = []
        self._funcs_before_fade_out = []
        self._funcs_after_fade_out = []

        self.setModal(False)

    def _run(self, funcs):
        """ """
        for func in funcs:
            func()

    def _run_before_fade_in(self):
        """ """
        self._run(self._funcs_before_fade_in)

    def _run_after_fade_in(self):
        """ """
        self._run(self._funcs_after_fade_in)

    def _run_before_fade_out(self):
        """ """
        self._run(self._funcs_before_fade_out)

    def _run_after_fade_out(self):
        """ """
        self._run(self._funcs_after_fade_out)

    def _set_fade_finished(self):
        """ """
        self._fade_running = False

    def _fade_setup(self):
        """ """
        self._fade_running = True
        self.effect = QGraphicsOpacityEffect(self)
        self.setGraphicsEffect(self.effect)
        self.anim = QPropertyAnimation(self.effect, to_binary_string("opacity"))

    # --- public api
    def fade_in(self, on_finished_connect):
        """ """
        self._run_before_fade_in()
        self._fade_setup()
        self.show()
        self.raise_()
        self.anim.setEasingCurve(self.easing_curve_in)
        self.anim.setStartValue(self.opacity_min)
        self.anim.setEndValue(self.opacity_max)
        self.anim.setDuration(self.duration_fadein)
        self.anim.finished.connect(on_finished_connect)
        self.anim.finished.connect(self._set_fade_finished)
        self.anim.finished.connect(self._run_after_fade_in)
        self.anim.start()

    def fade_out(self, on_finished_connect):
        """ """
        self._run_before_fade_out()
        self._fade_setup()
        self.anim.setEasingCurve(self.easing_curve_out)
        self.anim.setStartValue(self.opacity_max)
        self.anim.setEndValue(self.opacity_min)
        self.anim.setDuration(self.duration_fadeout)
        self.anim.finished.connect(on_finished_connect)
        self.anim.finished.connect(self._set_fade_finished)
        self.anim.finished.connect(self._run_after_fade_out)
        self.anim.start()

    def is_fade_running(self):
        """ """
        return self._fade_running

    def set_funcs_before_fade_in(self, funcs):
        """ """
        self._funcs_before_fade_in = funcs

    def set_funcs_after_fade_in(self, funcs):
        """ """
        self._funcs_after_fade_in = funcs

    def set_funcs_before_fade_out(self, funcs):
        """ """
        self._funcs_before_fade_out = funcs

    def set_funcs_after_fade_out(self, funcs):
        """ """
        self._funcs_after_fade_out = funcs
Beispiel #13
0
class TourTestWindow(QMainWindow):
    """ """
    sig_resized = Signal("QResizeEvent")
    sig_moved = Signal("QMoveEvent")

    def __init__(self):
        super(TourTestWindow, self).__init__()
        self.setGeometry(300, 100, 400, 600)
        self.setWindowTitle('Exploring QMainWindow')

        self.exit = QAction('Exit', self)
        self.exit.setStatusTip('Exit program')

        # create the menu bar
        menubar = self.menuBar()
        file_ = menubar.addMenu('&File')
        file_.addAction(self.exit)

        # create the status bar
        self.statusBar()

        # QWidget or its instance needed for box layout
        self.widget = QWidget(self)

        self.button = QPushButton('test')
        self.button1 = QPushButton('1')
        self.button2 = QPushButton('2')

        effect = QGraphicsOpacityEffect(self.button2)
        self.button2.setGraphicsEffect(effect)
        self.anim = QPropertyAnimation(effect, to_binary_string("opacity"))
        self.anim.setStartValue(0.01)
        self.anim.setEndValue(1.0)
        self.anim.setDuration(500)

        lay = QVBoxLayout()
        lay.addWidget(self.button)
        lay.addStretch()
        lay.addWidget(self.button1)
        lay.addWidget(self.button2)

        self.widget.setLayout(lay)

        self.setCentralWidget(self.widget)
        self.button.clicked.connect(self.action1)
        self.button1.clicked.connect(self.action2)

        self.tour = AnimatedTour(self)

    def action1(self):
        """ """
        frames = get_tour('test')
        index = 0
        dic = {'last': 0, 'tour': frames}
        self.tour.set_tour(index, dic, self)
        self.tour.start_tour()

    def action2(self):
        """ """
        self.anim.start()

    def resizeEvent(self, event):
        """Reimplement Qt method"""
        QMainWindow.resizeEvent(self, event)
        self.sig_resized.emit(event)

    def moveEvent(self, event):
        """Reimplement Qt method"""
        QMainWindow.moveEvent(self, event)
        self.sig_moved.emit(event)
Beispiel #14
0
 def fadeIn(self):
     self._effects = []
     for action in self.actions():
         effect = QGraphicsOpacityEffect(self)
         self._effects.append(effect)
         for widget in action.associatedWidgets():
             if widget is not self:
                 widget.setGraphicsEffect(effect)
         a = QPropertyAnimation(effect, b"opacity")
         self._effects.append(a)
         a.setDuration(1000)
         a.setStartValue(0)
         a.setEndValue(1)
         a.setEasingCurve(QEasingCurve.OutBack)
         a.start(QPropertyAnimation.DeleteWhenStopped)
Beispiel #15
0
class SimpleThemeSignal(SimpleThemeDecoration):
    # 左侧展开的item 索引
    expanded_item = None
    tree_menu_action = None
    menu_is_close = False

    def set_signal(self):
        super(SimpleThemeSignal, self).set_signal()
        self.pushButton_6.clicked.connect(self.change_normal)
        self.pushButton_5.clicked.connect(self.accept)
        self.pushButton_7.clicked.connect(self.showMinimized)
        self.pushButton.clicked.connect(self.unfold_menu)
        self.pushButton_8.clicked.connect(self.unfold_menu)
        self.treeWidget.itemClicked.connect(self.tree_unfold)
        self.pushButton_2.clicked.connect(self.show_about)

    @staticmethod
    def show_about():
        """点击关于"""
        about = Badge(source=AboutActivity)
        about.center()
        about.exec()

    def change_normal(self):
        """
        切换到恢复窗口大小按钮,
        """
        self.layout().setContentsMargins(*[0] * 4)
        self.showMaximized()  # 先实现窗口最大化
        self.pushButton_6.setIcon(
            self.resource.font_icon("fa.window-restore", color="black"))
        self.pushButton_6.setToolTip(self.resource.translate('Bar',
                                                             "恢复"))  # 更改按钮提示
        self.pushButton_6.disconnect()  # 断开原本的信号槽连接
        self.pushButton_6.clicked.connect(self.change_max)  # 重新连接信号和槽

    # noinspection PyUnresolvedReferences
    def change_max(self):
        """
        切换到最大化按钮
        """
        self.layout().setContentsMargins(*[self.border_width] * 4)
        self.showNormal()
        self.pushButton_6.setIcon(
            self.resource.font_icon("fa.window-maximize", color="black"))
        self.pushButton_6.setToolTip(self.resource.translate('Bar', "最大化"))
        self.pushButton_6.disconnect()  # 关闭信号与原始槽连接
        self.pushButton_6.clicked.connect(self.change_normal)

    def mouseDoubleClickEvent(self, event):
        """鼠标双击(在y轴上小于标题栏高度的双击均被认为是双击头部,随后进行窗体的最大化跟恢复效果)"""
        if self.bar and event.pos().y() < self.bar.y() + self.bar.height() \
                and event.pos().x() < self.bar.x() + self.bar.width():
            self.pushButton_6.click()

    def unfold_menu(self):
        """
        展开/收起菜单
        通过修改左侧widget的最大宽度来实现,展开时设置为180,
        收起时设置为60,刚好是一个图标左右的宽度
        """

        self.tree_menu_action = QPropertyAnimation(self.widget,
                                                   b'maximumWidth')
        self.tree_menu_action.stop()
        mini_width = 50
        start = self.widget.width()
        if self.widget.width() > mini_width:
            icon = self.resource.font_icon('ei.chevron-right', color="#d2d2d2")
            end = mini_width
            self.menu_is_close = True
            # 在收缩的时候看看是不是有列表是展开的
            # 如果有则先对齐收缩再收缩左侧导航栏
            if self.expanded_item:
                self.expanded_item.setExpanded(False)
        else:
            icon = self.resource.font_icon('ei.chevron-left', color="#d2d2d2")
            end = 180
            self.menu_is_close = False
        self.pushButton_8.setIcon(icon)
        self.tree_menu_action.setStartValue(start)
        self.tree_menu_action.setEndValue(end)
        self.tree_menu_action.setEasingCurve(QEasingCurve.InOutBack)
        self.tree_menu_action.setDuration(500)
        self.tree_menu_action.start()

    def tree_unfold(self, item: QTreeWidgetItem):
        """
        点击存在子项的父类项,如果其没有展开则展开,
        同时关闭其他已经展开的top item,
        反之,若点击已经展开的item则将其关闭
        """
        # 不允许在收缩菜单时对子列表进行展开
        if not self.menu_is_close and item.childCount():
            item = self.treeWidget.currentItem()
            self.expanded_item = item
            if item.isExpanded():
                item.setExpanded(False)
                return
            item.setExpanded(True)
        elif item.parent() is self.expanded_item:
            pass
        elif self.expanded_item and self.expanded_item.isExpanded():
            self.expanded_item.setExpanded(False)
Beispiel #16
0
class FadingDialog(QDialog):
    """A general fade in/fade out QDialog with some builtin functions"""
    sig_key_pressed = Signal()

    def __init__(self, parent, opacity, duration, easing_curve):
        super(FadingDialog, self).__init__(parent)

        self.parent = parent
        self.opacity_min = min(opacity)
        self.opacity_max = max(opacity)
        self.duration_fadein = duration[0]
        self.duration_fadeout = duration[-1]
        self.easing_curve_in = easing_curve[0]
        self.easing_curve_out = easing_curve[-1]
        self.effect = None
        self.anim = None

        self._fade_running = False
        self._funcs_before_fade_in = []
        self._funcs_after_fade_in = []
        self._funcs_before_fade_out = []
        self._funcs_after_fade_out = []

        self.setModal(False)

    def _run(self, funcs):
        """ """
        for func in funcs:
            func()

    def _run_before_fade_in(self):
        """ """
        self._run(self._funcs_before_fade_in)

    def _run_after_fade_in(self):
        """ """
        self._run(self._funcs_after_fade_in)

    def _run_before_fade_out(self):
        """ """
        self._run(self._funcs_before_fade_out)

    def _run_after_fade_out(self):
        """ """
        self._run(self._funcs_after_fade_out)

    def _set_fade_finished(self):
        """ """
        self._fade_running = False

    def _fade_setup(self):
        """ """
        self._fade_running = True
        self.effect = QGraphicsOpacityEffect(self)
        self.setGraphicsEffect(self.effect)
        self.anim = QPropertyAnimation(self.effect,
                                       to_binary_string("opacity"))

    # --- public api
    def fade_in(self, on_finished_connect):
        """ """
        self._run_before_fade_in()
        self._fade_setup()
        self.show()
        self.raise_()
        self.anim.setEasingCurve(self.easing_curve_in)
        self.anim.setStartValue(self.opacity_min)
        self.anim.setEndValue(self.opacity_max)
        self.anim.setDuration(self.duration_fadein)
        self.anim.finished.connect(on_finished_connect)
        self.anim.finished.connect(self._set_fade_finished)
        self.anim.finished.connect(self._run_after_fade_in)
        self.anim.start()

    def fade_out(self, on_finished_connect):
        """ """
        self._run_before_fade_out()
        self._fade_setup()
        self.anim.setEasingCurve(self.easing_curve_out)
        self.anim.setStartValue(self.opacity_max)
        self.anim.setEndValue(self.opacity_min)
        self.anim.setDuration(self.duration_fadeout)
        self.anim.finished.connect(on_finished_connect)
        self.anim.finished.connect(self._set_fade_finished)
        self.anim.finished.connect(self._run_after_fade_out)
        self.anim.start()

    def is_fade_running(self):
        """ """
        return self._fade_running

    def set_funcs_before_fade_in(self, funcs):
        """ """
        self._funcs_before_fade_in = funcs

    def set_funcs_after_fade_in(self, funcs):
        """ """
        self._funcs_after_fade_in = funcs

    def set_funcs_before_fade_out(self, funcs):
        """ """
        self._funcs_before_fade_out = funcs

    def set_funcs_after_fade_out(self, funcs):
        """ """
        self._funcs_after_fade_out = funcs
Beispiel #17
0
class NapariNotification(QDialog):
    """Notification dialog frame, appears at the bottom right of the canvas.

    By default, only the first line of the notification is shown, and the text
    is elided.  Double-clicking on the text (or clicking the chevron icon) will
    expand to show the full notification.  The dialog will autmatically
    disappear in ``DISMISS_AFTER`` milliseconds, unless hovered or clicked.

    Parameters
    ----------
    message : str
        The message that will appear in the notification
    severity : str or NotificationSeverity, optional
        Severity level {'error', 'warning', 'info', 'none'}.  Will determine
        the icon associated with the message.
        by default NotificationSeverity.WARNING.
    source : str, optional
        A source string for the notifcation (intended to show the module and
        or package responsible for the notification), by default None
    actions : list of tuple, optional
        A sequence of 2-tuples, where each tuple is a string and a callable.
        Each tuple will be used to create button in the dialog, where the text
        on the button is determine by the first item in the tuple, and a
        callback function to call when the button is pressed is the second item
        in the tuple. by default ()
    """

    MAX_OPACITY = 0.9
    FADE_IN_RATE = 220
    FADE_OUT_RATE = 120
    DISMISS_AFTER = 4000
    MIN_WIDTH = 400

    def __init__(
            self,
            message: str,
            severity: Union[
                str, NotificationSeverity] = NotificationSeverity.WARNING,
            source: Optional[str] = None,
            actions: ActionSequence = (),
    ):
        """[summary]


        """
        super().__init__(None)
        # FIXME: this does not work with multiple viewers.
        # we need a way to detect the viewer in which the error occured.
        for wdg in QApplication.topLevelWidgets():
            if isinstance(wdg, QMainWindow):
                try:
                    # TODO: making the canvas the parent makes it easier to
                    # move/resize, but also means that the notification can get
                    # clipped on the left if the canvas is too small.
                    canvas = wdg.centralWidget().children()[1].canvas.native
                    self.setParent(canvas)
                    canvas.resized.connect(self.move_to_bottom_right)
                    break
                except Exception:
                    pass
        self.setupUi()
        self.setAttribute(Qt.WA_DeleteOnClose)
        self.setup_buttons(actions)
        self.setMouseTracking(True)

        self.severity_icon.setText(NotificationSeverity(severity).as_icon())
        self.message.setText(message)
        if source:
            self.source_label.setText(f'Source: {source}')

        self.close_button.clicked.connect(self.close)
        self.expand_button.clicked.connect(self.toggle_expansion)

        self.timer = None
        self.opacity = QGraphicsOpacityEffect()
        self.setGraphicsEffect(self.opacity)
        self.opacity_anim = QPropertyAnimation(self.opacity, b"opacity", self)
        self.geom_anim = QPropertyAnimation(self, b"geometry", self)
        self.move_to_bottom_right()

    def move_to_bottom_right(self, offset=(8, 8)):
        """Position widget at the bottom right edge of the parent."""
        if not self.parent():
            return
        sz = self.parent().size() - self.size() - QSize(*offset)
        self.move(QPoint(sz.width(), sz.height()))

    def slide_in(self):
        """Run animation that fades in the dialog with a slight slide up."""
        geom = self.geometry()
        self.geom_anim.setDuration(self.FADE_IN_RATE)
        self.geom_anim.setStartValue(geom.translated(0, 20))
        self.geom_anim.setEndValue(geom)
        self.geom_anim.setEasingCurve(QEasingCurve.OutQuad)
        # fade in
        self.opacity_anim.setDuration(self.FADE_IN_RATE)
        self.opacity_anim.setStartValue(0)
        self.opacity_anim.setEndValue(self.MAX_OPACITY)
        self.geom_anim.start()
        self.opacity_anim.start()

    def show(self):
        """Show the message with a fade and slight slide in from the bottom."""
        super().show()
        self.slide_in()
        self.timer = QTimer()
        self.timer.setInterval(self.DISMISS_AFTER)
        self.timer.setSingleShot(True)
        self.timer.timeout.connect(self.close)
        self.timer.start()

    def mouseMoveEvent(self, event):
        """On hover, stop the self-destruct timer"""
        self.timer.stop()

    def mouseDoubleClickEvent(self, event):
        """Expand the notification on double click."""
        self.toggle_expansion()

    def close(self):
        """Fade out then close."""
        self.opacity_anim.setDuration(self.FADE_OUT_RATE)
        self.opacity_anim.setStartValue(self.MAX_OPACITY)
        self.opacity_anim.setEndValue(0)
        self.opacity_anim.start()
        self.opacity_anim.finished.connect(super().close)

    def toggle_expansion(self):
        """Toggle the expanded state of the notification frame."""
        self.contract() if self.property('expanded') else self.expand()
        self.timer.stop()

    def expand(self):
        """Expanded the notification so that the full message is visible."""
        curr = self.geometry()
        self.geom_anim.setDuration(100)
        self.geom_anim.setStartValue(curr)
        new_height = self.sizeHint().height()
        delta = new_height - curr.height()
        self.geom_anim.setEndValue(
            QRect(curr.x(),
                  curr.y() - delta, curr.width(), new_height))
        self.geom_anim.setEasingCurve(QEasingCurve.OutQuad)
        self.geom_anim.start()
        self.setProperty('expanded', True)
        self.style().unpolish(self.expand_button)
        self.style().polish(self.expand_button)

    def contract(self):
        """Contract notification to a single elided line of the message."""
        geom = self.geometry()
        self.geom_anim.setDuration(100)
        self.geom_anim.setStartValue(geom)
        dlt = geom.height() - self.minimumHeight()
        self.geom_anim.setEndValue(
            QRect(geom.x(),
                  geom.y() + dlt, geom.width(),
                  geom.height() - dlt))
        self.geom_anim.setEasingCurve(QEasingCurve.OutQuad)
        self.geom_anim.start()
        self.setProperty('expanded', False)
        self.style().unpolish(self.expand_button)
        self.style().polish(self.expand_button)

    def setupUi(self):
        """Set up the UI during initialization."""
        self.setWindowFlags(Qt.SubWindow)
        self.setMinimumWidth(self.MIN_WIDTH)
        self.setMaximumWidth(self.MIN_WIDTH)
        self.setMinimumHeight(40)
        self.setSizeGripEnabled(False)
        self.setModal(False)
        self.verticalLayout = QVBoxLayout(self)
        self.verticalLayout.setContentsMargins(2, 2, 2, 2)
        self.verticalLayout.setSpacing(0)

        self.row1_widget = QWidget(self)
        self.row1 = QHBoxLayout(self.row1_widget)
        self.row1.setContentsMargins(12, 12, 12, 8)
        self.row1.setSpacing(4)
        self.severity_icon = QLabel(self.row1_widget)
        self.severity_icon.setObjectName("severity_icon")
        self.severity_icon.setMinimumWidth(30)
        self.severity_icon.setMaximumWidth(30)
        self.row1.addWidget(self.severity_icon, alignment=Qt.AlignTop)
        self.message = MultilineElidedLabel(self.row1_widget)
        self.message.setMinimumWidth(self.MIN_WIDTH - 200)
        self.message.setSizePolicy(QSizePolicy.Expanding,
                                   QSizePolicy.Expanding)
        self.row1.addWidget(self.message, alignment=Qt.AlignTop)
        self.expand_button = QPushButton(self.row1_widget)
        self.expand_button.setObjectName("expand_button")
        self.expand_button.setCursor(Qt.PointingHandCursor)
        self.expand_button.setMaximumWidth(20)
        self.expand_button.setFlat(True)

        self.row1.addWidget(self.expand_button, alignment=Qt.AlignTop)
        self.close_button = QPushButton(self.row1_widget)
        self.close_button.setObjectName("close_button")
        self.close_button.setCursor(Qt.PointingHandCursor)
        self.close_button.setMaximumWidth(20)
        self.close_button.setFlat(True)

        self.row1.addWidget(self.close_button, alignment=Qt.AlignTop)
        self.verticalLayout.addWidget(self.row1_widget, 1)
        self.row2_widget = QWidget(self)
        self.row2_widget.hide()
        self.row2 = QHBoxLayout(self.row2_widget)
        self.source_label = QLabel(self.row2_widget)
        self.source_label.setObjectName("source_label")
        self.row2.addWidget(self.source_label, alignment=Qt.AlignBottom)
        self.row2.addStretch()
        self.row2.setContentsMargins(12, 2, 16, 12)
        self.row2_widget.setMaximumHeight(34)
        self.row2_widget.setStyleSheet('QPushButton{'
                                       'padding: 4px 12px 4px 12px; '
                                       'font-size: 11px;'
                                       'min-height: 18px; border-radius: 0;}')
        self.verticalLayout.addWidget(self.row2_widget, 0)
        self.setProperty('expanded', False)
        self.resize(self.MIN_WIDTH, 40)

    def setup_buttons(self, actions: ActionSequence = ()):
        """Add buttons to the dialog.


        Parameters
        ----------
        actions : tuple, optional
            A sequence of 2-tuples, where each tuple is a string and a
            callable. Each tuple will be used to create button in the dialog,
            where the text on the button is determine by the first item in the
            tuple, and a callback function to call when the button is pressed
            is the second item in the tuple. by default ()
        """
        if isinstance(actions, dict):
            actions = list(actions.items())
        for text, callback in actions:
            btn = QPushButton(text)
            btn.clicked.connect(callback)
            btn.clicked.connect(self.close)
            self.row2.addWidget(btn)
        if actions:
            self.row2_widget.show()
            self.setMinimumHeight(self.row2_widget.maximumHeight() +
                                  self.minimumHeight())

    def sizeHint(self):
        """Return the size required to show the entire message."""
        return QSize(
            super().sizeHint().width(),
            self.row2_widget.height() + self.message.sizeHint().height(),
        )

    @classmethod
    def from_exception(cls, exception: BaseException) -> 'NapariNotification':
        """Create a NapariNotifcation dialog from an exception object."""
        # TODO: this method could be used to recognize various exception
        # subclasses and populate the dialog accordingly.
        msg = getattr(exception, 'message', str(exception))
        severity = getattr(exception, 'severity', 'WARNING')
        source = None
        actions = getattr(exception, 'actions', ())
        return cls(msg, severity, source, actions)
Beispiel #18
0
    def fadeOut(self, callback, distance=-20):
        duration = 200
        self._effects = []
        for action in self.actions():
            for widget in action.associatedWidgets():
                if widget is not self:
                    a = QPropertyAnimation(widget, b"pos", widget)
                    a.setStartValue(widget.pos())
                    a.setEndValue(widget.pos() + QPoint(0, distance))
                    self._effects.append(a)
                    a.setDuration(duration)
                    a.setEasingCurve(QEasingCurve.OutBack)
                    a.start(QPropertyAnimation.DeleteWhenStopped)

                    effect = QGraphicsOpacityEffect(self)
                    widget.setGraphicsEffect(effect)
                    self._effects.append(effect)
                    b = QPropertyAnimation(effect, b"opacity")
                    self._effects.append(b)
                    b.setDuration(duration)
                    b.setStartValue(1)
                    b.setEndValue(0)
                    b.setEasingCurve(QEasingCurve.OutBack)
                    b.start(QPropertyAnimation.DeleteWhenStopped)
                    b.finished.connect(partial(self.removeAction, action))
                    b.finished.connect(callback)
        if not self.actions():
            callback()
Beispiel #19
0
class NapariQtNotification(QDialog):
    """Notification dialog frame, appears at the bottom right of the canvas.

    By default, only the first line of the notification is shown, and the text
    is elided.  Double-clicking on the text (or clicking the chevron icon) will
    expand to show the full notification.  The dialog will autmatically
    disappear in ``DISMISS_AFTER`` milliseconds, unless hovered or clicked.

    Parameters
    ----------
    message : str
        The message that will appear in the notification
    severity : str or NotificationSeverity, optional
        Severity level {'error', 'warning', 'info', 'none'}.  Will determine
        the icon associated with the message.
        by default NotificationSeverity.WARNING.
    source : str, optional
        A source string for the notifcation (intended to show the module and
        or package responsible for the notification), by default None
    actions : list of tuple, optional
        A sequence of 2-tuples, where each tuple is a string and a callable.
        Each tuple will be used to create button in the dialog, where the text
        on the button is determine by the first item in the tuple, and a
        callback function to call when the button is pressed is the second item
        in the tuple. by default ()
    """

    MAX_OPACITY = 0.9
    FADE_IN_RATE = 220
    FADE_OUT_RATE = 120
    DISMISS_AFTER = 4000
    MIN_WIDTH = 400
    MIN_EXPANSION = 18

    message: QElidingLabel
    source_label: QLabel
    severity_icon: QLabel

    def __init__(
        self,
        message: str,
        severity: Union[str, NotificationSeverity] = 'WARNING',
        source: Optional[str] = None,
        actions: ActionSequence = (),
    ):
        super().__init__()

        from ..qt_main_window import _QtMainWindow

        current_window = _QtMainWindow.current()
        if current_window is not None:
            canvas = current_window._qt_viewer._canvas_overlay
            self.setParent(canvas)
            canvas.resized.connect(self.move_to_bottom_right)

        self.setupUi()
        self.setAttribute(Qt.WA_DeleteOnClose)
        self.setup_buttons(actions)
        self.setMouseTracking(True)

        self._update_icon(str(severity))
        self.message.setText(message)
        if source:
            self.source_label.setText(
                trans._('Source: {source}', source=source)
            )

        self.close_button.clicked.connect(self.close)
        self.expand_button.clicked.connect(self.toggle_expansion)

        self.timer = QTimer()
        self.opacity = QGraphicsOpacityEffect()
        self.setGraphicsEffect(self.opacity)
        self.opacity_anim = QPropertyAnimation(self.opacity, b"opacity", self)
        self.geom_anim = QPropertyAnimation(self, b"geometry", self)
        self.move_to_bottom_right()

    def _update_icon(self, severity: str):
        """Update the icon to match the severity level."""
        from ...settings import get_settings
        from ...utils.theme import get_theme

        settings = get_settings()
        theme = settings.appearance.theme
        default_color = getattr(get_theme(theme, False), 'icon')

        # FIXME: Should these be defined at the theme level?
        # Currently there is a warning one
        colors = {
            'error': "#D85E38",
            'warning': "#E3B617",
            'info': default_color,
            'debug': default_color,
            'none': default_color,
        }
        color = colors.get(severity, default_color)
        icon = QColoredSVGIcon.from_resources(severity)
        self.severity_icon.setPixmap(icon.colored(color=color).pixmap(15, 15))

    def move_to_bottom_right(self, offset=(8, 8)):
        """Position widget at the bottom right edge of the parent."""
        if not self.parent():
            return
        sz = self.parent().size() - self.size() - QSize(*offset)
        self.move(QPoint(sz.width(), sz.height()))

    def slide_in(self):
        """Run animation that fades in the dialog with a slight slide up."""
        geom = self.geometry()
        self.geom_anim.setDuration(self.FADE_IN_RATE)
        self.geom_anim.setStartValue(geom.translated(0, 20))
        self.geom_anim.setEndValue(geom)
        self.geom_anim.setEasingCurve(QEasingCurve.OutQuad)
        # fade in
        self.opacity_anim.setDuration(self.FADE_IN_RATE)
        self.opacity_anim.setStartValue(0)
        self.opacity_anim.setEndValue(self.MAX_OPACITY)
        self.geom_anim.start()
        self.opacity_anim.start()

    def show(self):
        """Show the message with a fade and slight slide in from the bottom."""
        super().show()
        self.slide_in()
        if self.DISMISS_AFTER > 0:
            self.timer.setInterval(self.DISMISS_AFTER)
            self.timer.setSingleShot(True)
            self.timer.timeout.connect(self.close)
            self.timer.start()

    def mouseMoveEvent(self, event):
        """On hover, stop the self-destruct timer"""
        self.timer.stop()

    def mouseDoubleClickEvent(self, event):
        """Expand the notification on double click."""
        self.toggle_expansion()

    def close(self):
        """Fade out then close."""
        self.opacity_anim.setDuration(self.FADE_OUT_RATE)
        self.opacity_anim.setStartValue(self.MAX_OPACITY)
        self.opacity_anim.setEndValue(0)
        self.opacity_anim.start()
        self.opacity_anim.finished.connect(super().close)

    def toggle_expansion(self):
        """Toggle the expanded state of the notification frame."""
        self.contract() if self.property('expanded') else self.expand()
        self.timer.stop()

    def expand(self):
        """Expanded the notification so that the full message is visible."""
        curr = self.geometry()
        self.geom_anim.setDuration(100)
        self.geom_anim.setStartValue(curr)
        new_height = self.sizeHint().height()
        if new_height < curr.height():
            # new height would shift notification down, ensure some expansion
            new_height = curr.height() + self.MIN_EXPANSION
        delta = new_height - curr.height()
        self.geom_anim.setEndValue(
            QRect(curr.x(), curr.y() - delta, curr.width(), new_height)
        )
        self.geom_anim.setEasingCurve(QEasingCurve.OutQuad)
        self.geom_anim.start()
        self.setProperty('expanded', True)
        self.style().unpolish(self.expand_button)
        self.style().polish(self.expand_button)

    def contract(self):
        """Contract notification to a single elided line of the message."""
        geom = self.geometry()
        self.geom_anim.setDuration(100)
        self.geom_anim.setStartValue(geom)
        dlt = geom.height() - self.minimumHeight()
        self.geom_anim.setEndValue(
            QRect(geom.x(), geom.y() + dlt, geom.width(), geom.height() - dlt)
        )
        self.geom_anim.setEasingCurve(QEasingCurve.OutQuad)
        self.geom_anim.start()
        self.setProperty('expanded', False)
        self.style().unpolish(self.expand_button)
        self.style().polish(self.expand_button)

    def setupUi(self):
        """Set up the UI during initialization."""
        self.setWindowFlags(Qt.SubWindow)
        self.setMinimumWidth(self.MIN_WIDTH)
        self.setMaximumWidth(self.MIN_WIDTH)
        self.setMinimumHeight(40)
        self.setSizeGripEnabled(False)
        self.setModal(False)
        self.verticalLayout = QVBoxLayout(self)
        self.verticalLayout.setContentsMargins(2, 2, 2, 2)
        self.verticalLayout.setSpacing(0)

        self.row1_widget = QWidget(self)
        self.row1 = QHBoxLayout(self.row1_widget)
        self.row1.setContentsMargins(12, 12, 12, 8)
        self.row1.setSpacing(4)
        self.severity_icon = QLabel(self.row1_widget)
        self.severity_icon.setObjectName("severity_icon")
        self.severity_icon.setMinimumWidth(30)
        self.severity_icon.setMaximumWidth(30)
        self.row1.addWidget(self.severity_icon, alignment=Qt.AlignTop)
        self.message = QElidingLabel()
        self.message.setWordWrap(True)
        self.message.setTextInteractionFlags(Qt.TextSelectableByMouse)
        self.message.setMinimumWidth(self.MIN_WIDTH - 200)
        self.message.setSizePolicy(
            QSizePolicy.Expanding, QSizePolicy.Expanding
        )
        self.row1.addWidget(self.message, alignment=Qt.AlignTop)
        self.expand_button = QPushButton(self.row1_widget)
        self.expand_button.setObjectName("expand_button")
        self.expand_button.setCursor(Qt.PointingHandCursor)
        self.expand_button.setMaximumWidth(20)
        self.expand_button.setFlat(True)

        self.row1.addWidget(self.expand_button, alignment=Qt.AlignTop)
        self.close_button = QPushButton(self.row1_widget)
        self.close_button.setObjectName("close_button")
        self.close_button.setCursor(Qt.PointingHandCursor)
        self.close_button.setMaximumWidth(20)
        self.close_button.setFlat(True)

        self.row1.addWidget(self.close_button, alignment=Qt.AlignTop)
        self.verticalLayout.addWidget(self.row1_widget, 1)
        self.row2_widget = QWidget(self)
        self.row2_widget.hide()
        self.row2 = QHBoxLayout(self.row2_widget)
        self.source_label = QLabel(self.row2_widget)
        self.source_label.setObjectName("source_label")
        self.row2.addWidget(self.source_label, alignment=Qt.AlignBottom)
        self.row2.addStretch()
        self.row2.setContentsMargins(12, 2, 16, 12)
        self.row2_widget.setMaximumHeight(34)
        self.row2_widget.setStyleSheet(
            'QPushButton{'
            'padding: 4px 12px 4px 12px; '
            'font-size: 11px;'
            'min-height: 18px; border-radius: 0;}'
        )
        self.verticalLayout.addWidget(self.row2_widget, 0)
        self.setProperty('expanded', False)
        self.resize(self.MIN_WIDTH, 40)

    def setup_buttons(self, actions: ActionSequence = ()):
        """Add buttons to the dialog.

        Parameters
        ----------
        actions : tuple, optional
            A sequence of 2-tuples, where each tuple is a string and a
            callable. Each tuple will be used to create button in the dialog,
            where the text on the button is determine by the first item in the
            tuple, and a callback function to call when the button is pressed
            is the second item in the tuple. by default ()
        """
        if isinstance(actions, dict):
            actions = list(actions.items())

        for text, callback in actions:
            btn = QPushButton(text)

            def call_back_with_self(callback, self):
                """
                We need a higher order function this to capture the reference to self.
                """

                def _inner():
                    return callback(self)

                return _inner

            btn.clicked.connect(call_back_with_self(callback, self))
            btn.clicked.connect(self.close)
            self.row2.addWidget(btn)
        if actions:
            self.row2_widget.show()
            self.setMinimumHeight(
                self.row2_widget.maximumHeight() + self.minimumHeight()
            )

    def sizeHint(self):
        """Return the size required to show the entire message."""
        return QSize(
            super().sizeHint().width(),
            self.row2_widget.height() + self.message.sizeHint().height(),
        )

    @classmethod
    def from_notification(
        cls, notification: Notification
    ) -> NapariQtNotification:

        from ...utils.notifications import ErrorNotification

        actions = notification.actions

        if isinstance(notification, ErrorNotification):

            def show_tb(parent):
                tbdialog = QDialog(parent=parent.parent())
                tbdialog.setModal(True)
                # this is about the minimum width to not get rewrap
                # and the minimum height to not have scrollbar
                tbdialog.resize(650, 270)
                tbdialog.setLayout(QVBoxLayout())

                text = QTextEdit()
                theme = get_theme(
                    get_settings().appearance.theme, as_dict=False
                )
                _highlight = Pylighter(  # noqa: F841
                    text.document(), "python", theme.syntax_style
                )
                text.setText(notification.as_text())
                text.setReadOnly(True)
                btn = QPushButton(trans._('Enter Debugger'))

                def _enter_debug_mode():
                    btn.setText(
                        trans._(
                            'Now Debugging. Please quit debugger in console to continue'
                        )
                    )
                    _debug_tb(notification.exception.__traceback__)
                    btn.setText(trans._('Enter Debugger'))

                btn.clicked.connect(_enter_debug_mode)
                tbdialog.layout().addWidget(text)
                tbdialog.layout().addWidget(btn, 0, Qt.AlignRight)
                tbdialog.show()

            actions = tuple(notification.actions) + (
                (trans._('View Traceback'), show_tb),
            )
        else:
            actions = notification.actions

        return cls(
            message=notification.message,
            severity=notification.severity,
            source=notification.source,
            actions=actions,
        )

    @classmethod
    @ensure_main_thread
    def show_notification(cls, notification: Notification):
        from ...settings import get_settings

        settings = get_settings()

        # after https://github.com/napari/napari/issues/2370,
        # the os.getenv can be removed (and NAPARI_CATCH_ERRORS retired)
        if (
            notification.severity
            >= settings.application.gui_notification_level
        ):
            cls.from_notification(notification).show()