def initAddButton(self): addTabButton = QPushButton() addTabButton.setParent(self) addTabButton.setIconSize(QSize(22, 22)) addTabButton.setObjectName("addTabButton") addTabButton.setCursor(Qt.PointingHandCursor) self.addTabButton = addTabButton self.updateAddButtonPos()
def initUI(self): addSampleButton = QPushButton() addSampleButton.setText("Add sample") addSampleButton.setCursor(Qt.PointingHandCursor) self.addSampleButton = addSampleButton deleteSampleButton = QPushButton() deleteSampleButton.setText("Delete sample") deleteSampleButton.setObjectName("deleteSampleButton") deleteSampleButton.setDisabled(True) deleteSampleButton.setCursor(Qt.ForbiddenCursor) self.deleteSampleButton = deleteSampleButton sortSamplesButton = QPushButton() sortSamplesButton.setText("Sort") sortSamplesButton.setCursor(Qt.PointingHandCursor) self.sortSamplesButton = sortSamplesButton sampleButtonsLayout = QHBoxLayout() sampleButtonsLayout.addWidget(self.addSampleButton) sampleButtonsLayout.addWidget(self.deleteSampleButton) sampleButtonsLayout.addWidget(self.sortSamplesButton) addCohortButton = QPushButton() addCohortButton.setText("Add cohort") addCohortButton.setCursor(Qt.PointingHandCursor) self.addCohortButton = addCohortButton deleteCohortButton = QPushButton() deleteCohortButton.setText("Delete cohort") deleteCohortButton.setObjectName("deleteCohortButton") deleteCohortButton.setDisabled(True) deleteCohortButton.setCursor(Qt.ForbiddenCursor) self.deleteCohortButton = deleteCohortButton cohortButtonsLayout = QHBoxLayout() cohortButtonsLayout.addWidget(self.addCohortButton) cohortButtonsLayout.addWidget(self.deleteCohortButton) layout = QVBoxLayout() layout.addWidget(self.tableView) layout.addLayout(sampleButtonsLayout) layout.addLayout(cohortButtonsLayout) groupBox = QGroupBox() groupBox.setTitle("Samples") groupBox.setLayout(layout) parentLayout = QVBoxLayout() parentLayout.addWidget(groupBox) self.setLayout(parentLayout)
def initUI(self): addFormulationButton = QPushButton() addFormulationButton.setText("Add formulation") addFormulationButton.setCursor(Qt.PointingHandCursor) self.addFormulationButton = addFormulationButton deleteFormulationButton = QPushButton() deleteFormulationButton.setText("Delete formulation") deleteFormulationButton.setObjectName("deleteFormulationButton") deleteFormulationButton.setDisabled(True) deleteFormulationButton.setCursor(Qt.ForbiddenCursor) self.deleteFormulationButton = deleteFormulationButton sortFormulationsButton = QPushButton() sortFormulationsButton.setText("Sort") sortFormulationsButton.setCursor(Qt.PointingHandCursor) self.sortFormulationsButton = sortFormulationsButton buttonsLayout = QHBoxLayout() buttonsLayout.addWidget(addFormulationButton) buttonsLayout.addWidget(deleteFormulationButton) buttonsLayout.addWidget(sortFormulationsButton) layout = QVBoxLayout() layout.addWidget(self.tableView, 1) layout.addLayout(buttonsLayout, 0) groupBox = QGroupBox() groupBox.setTitle("Formulations") groupBox.setLayout(layout) parentLayout = QVBoxLayout() parentLayout.addWidget(groupBox) self.setLayout(parentLayout)
class PluginListItem(QFrame): def __init__( self, package_name: str, version: str = '', url: str = '', summary: str = '', author: str = '', license: str = "UNKNOWN", *, plugin_name: str = None, parent: QWidget = None, enabled: bool = True, installed: bool = False, npe_version=1, ): super().__init__(parent) self.setup_ui(enabled) self.plugin_name.setText(package_name) self.package_name.setText(version) self.summary.setText(summary) self.package_author.setText(author) self.cancel_btn.setVisible(False) self.help_button.setText(trans._("Website")) self.help_button.setObjectName("help_button") if npe_version != 1: self._handle_npe2_plugin() if installed: self.enabled_checkbox.show() self.action_button.setText(trans._("uninstall")) self.action_button.setObjectName("remove_button") else: self.enabled_checkbox.hide() self.action_button.setText(trans._("install")) self.action_button.setObjectName("install_button") def _handle_npe2_plugin(self): npe2_icon = QLabel(self) icon = QColoredSVGIcon.from_resources('logo_silhouette') npe2_icon.setPixmap(icon.colored(color='#33F0FF').pixmap(20, 20)) self.row1.insertWidget(2, QLabel('npe2')) self.row1.insertWidget(2, npe2_icon) def _get_dialog(self) -> QDialog: p = self.parent() while not isinstance(p, QDialog) and p.parent(): p = p.parent() return p def set_busy(self, text: str, update: bool = False): self.item_status.setText(text) self.cancel_btn.setVisible(True) if not update: self.action_button.setVisible(False) else: self.update_btn.setVisible(False) def setup_ui(self, enabled=True): self.v_lay = QVBoxLayout(self) self.v_lay.setContentsMargins(-1, 6, -1, 6) self.v_lay.setSpacing(0) self.row1 = QHBoxLayout() self.row1.setSpacing(6) self.enabled_checkbox = QCheckBox(self) self.enabled_checkbox.setChecked(enabled) self.enabled_checkbox.stateChanged.connect(self._on_enabled_checkbox) self.enabled_checkbox.setToolTip(trans._("enable/disable")) sizePolicy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth( self.enabled_checkbox.sizePolicy().hasHeightForWidth()) self.enabled_checkbox.setSizePolicy(sizePolicy) self.enabled_checkbox.setMinimumSize(QSize(20, 0)) self.enabled_checkbox.setText("") self.row1.addWidget(self.enabled_checkbox) self.plugin_name = QLabel(self) sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth( self.plugin_name.sizePolicy().hasHeightForWidth()) self.plugin_name.setSizePolicy(sizePolicy) font15 = QFont() font15.setPointSize(15) self.plugin_name.setFont(font15) self.row1.addWidget(self.plugin_name) icon = QColoredSVGIcon.from_resources("warning") self.warning_tooltip = QtToolTipLabel(self) # TODO: This color should come from the theme but the theme needs # to provide the right color. Default warning should be orange, not # red. Code example: # theme_name = get_settings().appearance.theme # napari.utils.theme.get_theme(theme_name, as_dict=False).warning.as_hex() self.warning_tooltip.setPixmap( icon.colored(color="#E3B617").pixmap(15, 15)) self.warning_tooltip.setVisible(False) self.row1.addWidget(self.warning_tooltip) self.item_status = QLabel(self) self.item_status.setObjectName("small_italic_text") self.item_status.setSizePolicy(sizePolicy) self.row1.addWidget(self.item_status) self.row1.addStretch() self.package_name = QLabel(self) self.package_name.setAlignment(Qt.AlignRight | Qt.AlignTrailing | Qt.AlignVCenter) self.row1.addWidget(self.package_name) self.cancel_btn = QPushButton("cancel", self) self.cancel_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.cancel_btn.setObjectName("remove_button") self.row1.addWidget(self.cancel_btn) self.update_btn = QPushButton(self) self.update_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.update_btn.setObjectName("install_button") self.row1.addWidget(self.update_btn) self.update_btn.setVisible(False) self.help_button = QPushButton(self) self.action_button = QPushButton(self) sizePolicy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth( self.action_button.sizePolicy().hasHeightForWidth()) self.help_button.setSizePolicy(sizePolicy) self.action_button.setSizePolicy(sizePolicy) self.row1.addWidget(self.help_button) self.row1.addWidget(self.action_button) self.v_lay.addLayout(self.row1) self.row2 = QHBoxLayout() self.error_indicator = QPushButton() self.error_indicator.setObjectName("warning_icon") self.error_indicator.setCursor(Qt.PointingHandCursor) self.error_indicator.hide() self.row2.addWidget(self.error_indicator) self.row2.setContentsMargins(-1, 4, 0, -1) self.summary = QElidingLabel(parent=self) sizePolicy = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth( self.summary.sizePolicy().hasHeightForWidth()) self.summary.setSizePolicy(sizePolicy) self.summary.setObjectName("small_text") self.row2.addWidget(self.summary) self.package_author = QLabel(self) sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth( self.package_author.sizePolicy().hasHeightForWidth()) self.package_author.setSizePolicy(sizePolicy) self.package_author.setObjectName("small_text") self.row2.addWidget(self.package_author) self.v_lay.addLayout(self.row2) def _on_enabled_checkbox(self, state: int): """Called with `state` when checkbox is clicked.""" enabled = bool(state) plugin_name = self.plugin_name.text() pm2 = PluginManager.instance() if plugin_name in pm2: pm2.enable(plugin_name) if state else pm2.disable(plugin_name) return for npe1_name, _, distname in plugin_manager.iter_available(): if distname and (distname == plugin_name): plugin_manager.set_blocked(npe1_name, not enabled) def show_warning(self, message: str = ""): """Show warning icon and tooltip.""" self.warning_tooltip.setVisible(bool(message)) self.warning_tooltip.setToolTip(message)
class PluginListItem(QFrame): def __init__( self, package_name: str, version: str = '', url: str = '', summary: str = '', author: str = '', license: str = "UNKNOWN", *, plugin_name: str = None, parent: QWidget = None, enabled: bool = True, ): super().__init__(parent) self.setup_ui() if plugin_name: self.plugin_name.setText(plugin_name) self.package_name.setText(f"{package_name} {version}") self.summary.setText(summary) self.package_author.setText(author) self.action_button.setText(trans._("remove")) self.action_button.setObjectName("remove_button") self.enabled_checkbox.setChecked(enabled) if PluginError.get(plugin_name=plugin_name): def _show_error(): rep = QtPluginErrReporter(parent=self._get_dialog(), initial_plugin=plugin_name) rep.setWindowFlags(Qt.Sheet) close = QPushButton(trans._("close"), rep) rep.layout.addWidget(close) rep.plugin_combo.hide() close.clicked.connect(rep.close) rep.open() self.error_indicator.clicked.connect(_show_error) self.error_indicator.show() self.summary.setIndent(18) else: self.summary.setIndent(38) else: self.plugin_name.setText(package_name) self.package_name.setText(version) self.summary.setText(summary) self.package_author.setText(author) self.action_button.setText(trans._("install")) self.enabled_checkbox.hide() def _get_dialog(self) -> QDialog: p = self.parent() while not isinstance(p, QDialog) and p.parent(): p = p.parent() return p def setup_ui(self): self.v_lay = QVBoxLayout(self) self.v_lay.setContentsMargins(-1, 8, -1, 8) self.v_lay.setSpacing(0) self.row1 = QHBoxLayout() self.row1.setSpacing(8) self.enabled_checkbox = QCheckBox(self) self.enabled_checkbox.setChecked(True) self.enabled_checkbox.setDisabled(True) self.enabled_checkbox.setToolTip(trans._("enable/disable")) sizePolicy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth( self.enabled_checkbox.sizePolicy().hasHeightForWidth()) self.enabled_checkbox.setSizePolicy(sizePolicy) self.enabled_checkbox.setMinimumSize(QSize(20, 0)) self.enabled_checkbox.setText("") self.row1.addWidget(self.enabled_checkbox) self.plugin_name = QLabel(self) sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth( self.plugin_name.sizePolicy().hasHeightForWidth()) self.plugin_name.setSizePolicy(sizePolicy) font16 = QFont() font16.setPointSize(16) self.plugin_name.setFont(font16) self.row1.addWidget(self.plugin_name) self.package_name = QLabel(self) self.package_name.setAlignment(Qt.AlignRight | Qt.AlignTrailing | Qt.AlignVCenter) self.row1.addWidget(self.package_name) self.action_button = QPushButton(self) sizePolicy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth( self.action_button.sizePolicy().hasHeightForWidth()) self.action_button.setSizePolicy(sizePolicy) self.row1.addWidget(self.action_button) self.v_lay.addLayout(self.row1) self.row2 = QHBoxLayout() self.error_indicator = QPushButton() self.error_indicator.setObjectName("warning_icon") self.error_indicator.setCursor(Qt.PointingHandCursor) self.error_indicator.hide() self.row2.addWidget(self.error_indicator) self.row2.setContentsMargins(-1, 4, 0, -1) self.summary = ElidingLabel(parent=self) sizePolicy = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth( self.summary.sizePolicy().hasHeightForWidth()) self.summary.setSizePolicy(sizePolicy) self.summary.setObjectName("small_text") self.row2.addWidget(self.summary) self.package_author = QLabel(self) sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth( self.package_author.sizePolicy().hasHeightForWidth()) self.package_author.setSizePolicy(sizePolicy) self.package_author.setObjectName("small_text") self.row2.addWidget(self.package_author) self.v_lay.addLayout(self.row2)
class QtCustomTitleBar(QLabel): """A widget to be used as the titleBar in the QtViewerDockWidget. Keeps vertical size minimal, has a hand cursor and styles (in stylesheet) for hover. Close and float buttons. Parameters ---------- parent : QDockWidget The QtViewerDockWidget to which this titlebar belongs title : str A string to put in the titlebar. vertical : bool Whether this titlebar is oriented vertically or not. """ def __init__(self, parent, title: str = '', vertical=False, close_btn=True): super().__init__(parent) self.setObjectName("QtCustomTitleBar") self.setProperty('vertical', str(vertical)) self.vertical = vertical self.setToolTip(trans._('drag to move. double-click to float')) line = QFrame(self) line.setObjectName("QtCustomTitleBarLine") self.hide_button = QPushButton(self) self.hide_button.setToolTip(trans._('hide this panel')) self.hide_button.setObjectName("QTitleBarHideButton") self.hide_button.setCursor(Qt.ArrowCursor) self.hide_button.clicked.connect(lambda: self.parent().close()) self.float_button = QPushButton(self) self.float_button.setToolTip(trans._('float this panel')) self.float_button.setObjectName("QTitleBarFloatButton") self.float_button.setCursor(Qt.ArrowCursor) self.float_button.clicked.connect( lambda: self.parent().setFloating(not self.parent().isFloating())) self.title = QLabel(title, self) self.title.setSizePolicy( QSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Maximum)) if close_btn: self.close_button = QPushButton(self) self.close_button.setToolTip(trans._('close this panel')) self.close_button.setObjectName("QTitleBarCloseButton") self.close_button.setCursor(Qt.ArrowCursor) self.close_button.clicked.connect( lambda: self.parent().destroyOnClose()) if vertical: layout = QVBoxLayout() layout.setSpacing(4) layout.setContentsMargins(0, 8, 0, 8) line.setFixedWidth(1) if close_btn: layout.addWidget(self.close_button, 0, Qt.AlignHCenter) layout.addWidget(self.hide_button, 0, Qt.AlignHCenter) layout.addWidget(self.float_button, 0, Qt.AlignHCenter) layout.addWidget(line, 0, Qt.AlignHCenter) self.title.hide() else: layout = QHBoxLayout() layout.setSpacing(4) layout.setContentsMargins(8, 1, 8, 0) line.setFixedHeight(1) if close_btn: layout.addWidget(self.close_button) layout.addWidget(self.hide_button) layout.addWidget(self.float_button) layout.addWidget(line) layout.addWidget(self.title) self.setLayout(layout) self.setCursor(Qt.OpenHandCursor) def sizeHint(self): # this seems to be the correct way to set the height of the titlebar szh = super().sizeHint() if self.vertical: szh.setWidth(20) else: szh.setHeight(20) return szh
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 ClearableLineEdit(QLineEdit): passive_color = QColor(194, 194, 194) def __init__(self, placeholder="yyyy-mm-dd"): QLineEdit.__init__(self) self._placeholder_text = placeholder self._active_color = self.palette().color(self.foregroundRole()) self._placeholder_active = False self._clear_button = QPushButton(self) self._clear_button.setIcon(resourceIcon("remove_outlined.svg")) self._clear_button.setFlat(True) self._clear_button.setFocusPolicy(Qt.NoFocus) self._clear_button.setFixedSize(17, 17) self._clear_button.setCursor(Qt.ArrowCursor) self._clear_button.clicked.connect(self.clearButtonClicked) self._clear_button.setVisible(False) self.textChanged.connect(self.toggleClearButtonVisibility) self.showPlaceholder() def toggleClearButtonVisibility(self): self._clear_button.setVisible( len(self.text()) > 0 and not self._placeholder_active) def sizeHint(self): size = QLineEdit.sizeHint(self) return QSize(size.width() + self._clear_button.width() + 3, size.height()) def minimumSizeHint(self): size = QLineEdit.minimumSizeHint(self) return QSize(size.width() + self._clear_button.width() + 3, size.height()) def resizeEvent(self, event): right = self.rect().right() frame_width = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth) self._clear_button.move( right - frame_width - self._clear_button.width(), int((self.height() - self._clear_button.height()) / 2), ) QLineEdit.resizeEvent(self, event) def clearButtonClicked(self): self.setText("") def showPlaceholder(self): if not self._placeholder_active: self._placeholder_active = True QLineEdit.setText(self, self._placeholder_text) palette = self.palette() palette.setColor(self.foregroundRole(), self.passive_color) self.setPalette(palette) def hidePlaceHolder(self): if self._placeholder_active: self._placeholder_active = False QLineEdit.setText(self, "") palette = self.palette() palette.setColor(self.foregroundRole(), self._active_color) self.setPalette(palette) def focusInEvent(self, focus_event): QLineEdit.focusInEvent(self, focus_event) self.hidePlaceHolder() def focusOutEvent(self, focus_event): QLineEdit.focusOutEvent(self, focus_event) if str(QLineEdit.text(self)) == "": self.showPlaceholder() def keyPressEvent(self, key_event): if key_event.key() == Qt.Key_Escape: self.clear() self.clearFocus() key_event.accept() QLineEdit.keyPressEvent(self, key_event) def setText(self, string): self.hidePlaceHolder() QLineEdit.setText(self, string) if len(str(string)) == 0 and not self.hasFocus(): self.showPlaceholder() def text(self): if self._placeholder_active: return "" else: return QLineEdit.text(self)
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)
class PluginListItem(QFrame): def __init__( self, package_name: str, version: str = '', url: str = '', summary: str = '', author: str = '', license: str = "UNKNOWN", *, plugin_name: str = None, parent: QWidget = None, enabled: bool = True, installed: bool = False, npe_version=1, ): super().__init__(parent) self.setup_ui(enabled) self.plugin_name.setText(package_name) self.package_name.setText(version) self.summary.setText(summary) self.package_author.setText(author) self.cancel_btn.setVisible(False) self.help_button.setText(trans._("Website")) self.help_button.setObjectName("help_button") if npe_version != 1: self.enabled_checkbox.setEnabled(False) if installed: self.enabled_checkbox.show() self.action_button.setText(trans._("uninstall")) self.action_button.setObjectName("remove_button") else: self.enabled_checkbox.hide() self.action_button.setText(trans._("install")) self.action_button.setObjectName("install_button") def _get_dialog(self) -> QDialog: p = self.parent() while not isinstance(p, QDialog) and p.parent(): p = p.parent() return p def set_busy(self, text: str, update: bool = False): self.item_status.setText(text) self.cancel_btn.setVisible(True) if not update: self.action_button.setVisible(False) else: self.update_btn.setVisible(False) def setup_ui(self, enabled=True): self.v_lay = QVBoxLayout(self) self.v_lay.setContentsMargins(-1, 6, -1, 6) self.v_lay.setSpacing(0) self.row1 = QHBoxLayout() self.row1.setSpacing(6) self.enabled_checkbox = QCheckBox(self) self.enabled_checkbox.setChecked(enabled) self.enabled_checkbox.stateChanged.connect(self._on_enabled_checkbox) self.enabled_checkbox.setToolTip(trans._("enable/disable")) sizePolicy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth( self.enabled_checkbox.sizePolicy().hasHeightForWidth() ) self.enabled_checkbox.setSizePolicy(sizePolicy) self.enabled_checkbox.setMinimumSize(QSize(20, 0)) self.enabled_checkbox.setText("") self.row1.addWidget(self.enabled_checkbox) self.plugin_name = QLabel(self) sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth( self.plugin_name.sizePolicy().hasHeightForWidth() ) self.plugin_name.setSizePolicy(sizePolicy) font15 = QFont() font15.setPointSize(15) self.plugin_name.setFont(font15) self.row1.addWidget(self.plugin_name) self.item_status = QLabel(self) self.item_status.setObjectName("small_italic_text") self.item_status.setSizePolicy(sizePolicy) self.row1.addWidget(self.item_status) self.row1.addStretch() self.package_name = QLabel(self) self.package_name.setAlignment( Qt.AlignRight | Qt.AlignTrailing | Qt.AlignVCenter ) self.row1.addWidget(self.package_name) self.cancel_btn = QPushButton("cancel", self) self.cancel_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.cancel_btn.setObjectName("remove_button") self.row1.addWidget(self.cancel_btn) self.update_btn = QPushButton(self) self.update_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.update_btn.setObjectName("install_button") self.row1.addWidget(self.update_btn) self.update_btn.setVisible(False) self.help_button = QPushButton(self) self.action_button = QPushButton(self) sizePolicy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth( self.action_button.sizePolicy().hasHeightForWidth() ) self.help_button.setSizePolicy(sizePolicy) self.action_button.setSizePolicy(sizePolicy) self.row1.addWidget(self.help_button) self.row1.addWidget(self.action_button) self.v_lay.addLayout(self.row1) self.row2 = QHBoxLayout() self.error_indicator = QPushButton() self.error_indicator.setObjectName("warning_icon") self.error_indicator.setCursor(Qt.PointingHandCursor) self.error_indicator.hide() self.row2.addWidget(self.error_indicator) self.row2.setContentsMargins(-1, 4, 0, -1) self.summary = QElidingLabel(parent=self) sizePolicy = QSizePolicy( QSizePolicy.MinimumExpanding, QSizePolicy.Preferred ) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth( self.summary.sizePolicy().hasHeightForWidth() ) self.summary.setSizePolicy(sizePolicy) self.summary.setObjectName("small_text") self.row2.addWidget(self.summary) self.package_author = QLabel(self) sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth( self.package_author.sizePolicy().hasHeightForWidth() ) self.package_author.setSizePolicy(sizePolicy) self.package_author.setObjectName("small_text") self.row2.addWidget(self.package_author) self.v_lay.addLayout(self.row2) def _on_enabled_checkbox(self, state: int): """Called with `state` when checkbox is clicked.""" enabled = bool(state) current_distname = self.plugin_name.text() for plugin_name, _, distname in plugin_manager.iter_available(): if distname and distname == current_distname: plugin_manager.set_blocked(plugin_name, not enabled)
def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) selectProjectButton = QPushButton() selectProjectButton.setCursor(Qt.PointingHandCursor) selectProjectButton.setObjectName("selectProjectButton") selectProjectButton.setText("Select project") self.selectProjectButton = selectProjectButton pathLabel = QLabel() pathLabel.setText("Path:") pathLabel.setToolTip("Project path") pathLabel.setWordWrap(False) pathLabel.setAlignment(Qt.AlignRight) pathValueLabel = QElidedLabel() pathValueLabel.setText("none") pathValueLabel.setToolTip("none") pathValueLabel.setWordWrap(False) self.pathValueLabel = pathValueLabel nameLabel = QLabel() nameLabel.setText("Name:") nameLabel.setToolTip("Project name") nameLabel.setWordWrap(False) nameLabel.setAlignment(Qt.AlignRight) nameValueLabel = QElidedLabel() nameValueLabel.setText("none") nameValueLabel.setToolTip("none") nameValueLabel.setWordWrap(False) self.nameValueLabel = nameValueLabel projectStatusGrid = QGridLayout() projectStatusGrid.setContentsMargins(2, 2, 2, 2) projectStatusGrid.setColumnStretch(0, 0) projectStatusGrid.setColumnStretch(1, 1) projectStatusGrid.addWidget(pathLabel, 0, 0) projectStatusGrid.addWidget(nameLabel, 1, 0) projectStatusGrid.addWidget(pathValueLabel, 0, 1) projectStatusGrid.addWidget(nameValueLabel, 1, 1) projectGroupBox = QGroupBox() projectGroupBox.setTitle("Current project") projectGroupBox.setLayout(projectStatusGrid) statusLayout = QVBoxLayout() statusLayout.addWidget(selectProjectButton, stretch=0) statusLayout.addSpacing(10) statusLayout.addWidget(projectGroupBox, stretch=0) statusLayout.addStretch(1) statusLayout.setContentsMargins(0, 0, 0, 0) statusLayout.setSpacing(0) statusWidget = QWidget() statusWidget.setLayout(statusLayout) statusWidget.setContentsMargins(0, 0, 0, 0) statusWidget.setFixedWidth(250) noProjectSetLabel = QLabel() noProjectSetLabel.setObjectName("noProjectSetLabel") noProjectSetLabel.setText("No project set") noProjectSetLabel.setAlignment(Qt.AlignCenter) layout = QHBoxLayout() layout.setContentsMargins(6, 6, 6, 6) layout.addWidget(statusWidget, stretch=0) layout.addSpacing(3) layout.addWidget(noProjectSetLabel, stretch=1) self.setLayout(layout)
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()