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 _fade_setup(self): """ """ self._fade_running = True self.effect = QGraphicsOpacityEffect(self) self.setGraphicsEffect(self.effect) self.anim = QPropertyAnimation(self.effect, to_binary_string("opacity"))
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))
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 __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 __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()
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()
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()
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)
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
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)
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)
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)
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()
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()