From 7d7e1e4dc13316acbfe9044b69f16f1779115aea Mon Sep 17 00:00:00 2001 From: dalthviz <16781833+dalthviz@users.noreply.github.com> Date: Thu, 8 Aug 2024 12:35:35 -0500 Subject: [PATCH 1/5] Add an optional disclaimer message related with 3rd party nature of the plugins --- napari_plugin_manager/qt_plugin_dialog.py | 16 +++++++++++++++- napari_plugin_manager/styles.qss | 6 ++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/napari_plugin_manager/qt_plugin_dialog.py b/napari_plugin_manager/qt_plugin_dialog.py index 58faf0a0..8afde1b3 100644 --- a/napari_plugin_manager/qt_plugin_dialog.py +++ b/napari_plugin_manager/qt_plugin_dialog.py @@ -848,7 +848,9 @@ def filter(self, text: str, starts_with_chars: int = 1): class QtPluginDialog(QDialog): - def __init__(self, parent=None, prefix=None) -> None: + def __init__( + self, parent=None, prefix=None, show_disclaimer=False + ) -> None: super().__init__(parent) self._parent = parent @@ -862,6 +864,7 @@ def __init__(self, parent=None, prefix=None) -> None: self.available_set = set() self._prefix = prefix self._first_open = True + self._show_disclaimer = show_disclaimer self._plugin_queue = [] # Store plugin data to be added self._plugin_data = [] # Store all plugin data self._filter_texts = [] @@ -1113,6 +1116,13 @@ def _setup_ui(self): installed = QWidget(self.v_splitter) lay = QVBoxLayout(installed) lay.setContentsMargins(0, 2, 0, 2) + self.disclaimer_label = QLabel( + trans._( + "DISCLAIMER: Available plugin packages are user produced content. Any use of the provided files is at your own risk." + ) + ) + self.disclaimer_label.setObjectName("small_bold_text") + self.disclaimer_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.installed_label = QLabel(trans._("Installed Plugins")) self.packages_filter = QLineEdit() self.packages_filter.setPlaceholderText(trans._("filter...")) @@ -1134,6 +1144,7 @@ def _setup_ui(self): horizontal_mid_layout.addWidget(self.packages_filter) horizontal_mid_layout.addStretch() horizontal_mid_layout.addWidget(self.refresh_button) + mid_layout.addWidget(self.disclaimer_label) mid_layout.addLayout(horizontal_mid_layout) # mid_layout.addWidget(self.packages_filter) mid_layout.addWidget(self.installed_label) @@ -1217,6 +1228,8 @@ def _setup_ui(self): self.show_status_btn.setChecked(False) self.show_status_btn.toggled.connect(self.toggle_status) + self.disclaimer_label.setVisible(self._show_disclaimer) + self.v_splitter.setStretchFactor(1, 2) self.h_splitter.setStretchFactor(0, 2) @@ -1400,6 +1413,7 @@ def exec_(self): if plugin_dialog != self: self.close() + plugin_dialog.disclaimer_label.setVisible(self._show_disclaimer) plugin_dialog.setModal(True) plugin_dialog.show() diff --git a/napari_plugin_manager/styles.qss b/napari_plugin_manager/styles.qss index 6ec934f9..d3488ba4 100644 --- a/napari_plugin_manager/styles.qss +++ b/napari_plugin_manager/styles.qss @@ -145,6 +145,12 @@ QPushButton#refresh_button:disabled { font-style: italic; } +#small_bold_text { + color: {{ opacity(text, 150) }}; + font-size: {{ font_size }}; + font-weight: bold; +} + #plugin_manager_process_status{ background: {{ background }}; color: {{ opacity(text, 200) }}; From f27b75bc09cccbc702efaabc7efbbfd70dfa9d5c Mon Sep 17 00:00:00 2001 From: dalthviz <16781833+dalthviz@users.noreply.github.com> Date: Tue, 13 Aug 2024 11:43:54 -0500 Subject: [PATCH 2/5] Dismissible label/custom widget option implementation --- napari_plugin_manager/qt_plugin_dialog.py | 21 +++++------ napari_plugin_manager/qt_widgets.py | 45 +++++++++++++++++++++-- napari_plugin_manager/styles.qss | 6 ++- 3 files changed, 56 insertions(+), 16 deletions(-) diff --git a/napari_plugin_manager/qt_plugin_dialog.py b/napari_plugin_manager/qt_plugin_dialog.py index 8afde1b3..51584a18 100644 --- a/napari_plugin_manager/qt_plugin_dialog.py +++ b/napari_plugin_manager/qt_plugin_dialog.py @@ -54,7 +54,7 @@ InstallerTools, ProcessFinishedData, ) -from napari_plugin_manager.qt_widgets import ClickableLabel +from napari_plugin_manager.qt_widgets import ClickableLabel, DisclaimerWidget from napari_plugin_manager.utils import is_conda_package # Scaling factor for each list widget item when expanding. @@ -1116,13 +1116,6 @@ def _setup_ui(self): installed = QWidget(self.v_splitter) lay = QVBoxLayout(installed) lay.setContentsMargins(0, 2, 0, 2) - self.disclaimer_label = QLabel( - trans._( - "DISCLAIMER: Available plugin packages are user produced content. Any use of the provided files is at your own risk." - ) - ) - self.disclaimer_label.setObjectName("small_bold_text") - self.disclaimer_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.installed_label = QLabel(trans._("Installed Plugins")) self.packages_filter = QLineEdit() self.packages_filter.setPlaceholderText(trans._("filter...")) @@ -1144,9 +1137,7 @@ def _setup_ui(self): horizontal_mid_layout.addWidget(self.packages_filter) horizontal_mid_layout.addStretch() horizontal_mid_layout.addWidget(self.refresh_button) - mid_layout.addWidget(self.disclaimer_label) mid_layout.addLayout(horizontal_mid_layout) - # mid_layout.addWidget(self.packages_filter) mid_layout.addWidget(self.installed_label) lay.addLayout(mid_layout) @@ -1161,6 +1152,12 @@ def _setup_ui(self): mid_layout.addWidget(self.avail_label) mid_layout.addStretch() lay.addLayout(mid_layout) + self.disclaimer_widget = DisclaimerWidget( + trans._( + "DISCLAIMER: Available plugin packages are user produced content. Any use of the provided files is at your own risk." + ) + ) + lay.addWidget(self.disclaimer_widget) self.available_list = QPluginList(uninstalled, self.installer) lay.addWidget(self.available_list) @@ -1228,7 +1225,7 @@ def _setup_ui(self): self.show_status_btn.setChecked(False) self.show_status_btn.toggled.connect(self.toggle_status) - self.disclaimer_label.setVisible(self._show_disclaimer) + self.disclaimer_widget.setVisible(self._show_disclaimer) self.v_splitter.setStretchFactor(1, 2) self.h_splitter.setStretchFactor(0, 2) @@ -1413,7 +1410,7 @@ def exec_(self): if plugin_dialog != self: self.close() - plugin_dialog.disclaimer_label.setVisible(self._show_disclaimer) + plugin_dialog.disclaimer_widget.setVisible(self._show_disclaimer) plugin_dialog.setModal(True) plugin_dialog.show() diff --git a/napari_plugin_manager/qt_widgets.py b/napari_plugin_manager/qt_widgets.py index f1150713..f14c6016 100644 --- a/napari_plugin_manager/qt_widgets.py +++ b/napari_plugin_manager/qt_widgets.py @@ -1,6 +1,13 @@ -from qtpy.QtCore import Signal -from qtpy.QtGui import QMouseEvent -from qtpy.QtWidgets import QLabel +from qtpy.QtCore import Qt, Signal +from qtpy.QtGui import QMouseEvent, QPainter +from qtpy.QtWidgets import ( + QHBoxLayout, + QLabel, + QPushButton, + QStyle, + QStyleOption, + QWidget, +) class ClickableLabel(QLabel): @@ -12,3 +19,35 @@ def __init__(self, parent=None): def mouseReleaseEvent(self, event: QMouseEvent): super().mouseReleaseEvent(event) self.clicked.emit() + + +class DisclaimerWidget(QWidget): + def __init__(self, text, parent=None): + super().__init__(parent=parent) + + # Setup widgets + disclaimer_label = QLabel(text) + disclaimer_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + + disclaimer_button = QPushButton("x") + disclaimer_button.setFixedSize(20, 20) + disclaimer_button.clicked.connect(self.hide) + + # Setup layout + disclaimer_layout = QHBoxLayout() + disclaimer_layout.addWidget(disclaimer_label) + disclaimer_layout.addWidget(disclaimer_button) + self.setLayout(disclaimer_layout) + + def paintEvent(self, paint_event): + """ + Override so `QWidget` subclass can be affect by the stylesheet. + + For details you can check: https://doc.qt.io/qt-5/stylesheet-reference.html#list-of-stylable-widgets + """ + style_option = QStyleOption() + style_option.initFrom(self) + painter = QPainter(self) + self.style().drawPrimitive( + QStyle.PE_Widget, style_option, painter, self + ) diff --git a/napari_plugin_manager/styles.qss b/napari_plugin_manager/styles.qss index d3488ba4..eda10492 100644 --- a/napari_plugin_manager/styles.qss +++ b/napari_plugin_manager/styles.qss @@ -145,10 +145,14 @@ QPushButton#refresh_button:disabled { font-style: italic; } -#small_bold_text { +DisclaimerWidget { color: {{ opacity(text, 150) }}; font-size: {{ font_size }}; font-weight: bold; + background-color: {{ foreground }}; + border: 1px solid {{ foreground }}; + padding: 5px; + border-radius: 3px; } #plugin_manager_process_status{ From d8095158d1f574f97880d888b4f2e077d9279a7f Mon Sep 17 00:00:00 2001 From: dalthviz <16781833+dalthviz@users.noreply.github.com> Date: Tue, 13 Aug 2024 12:00:46 -0500 Subject: [PATCH 3/5] Add basic test for the disclaimer widget visibility --- napari_plugin_manager/_tests/test_qt_plugin_dialog.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/napari_plugin_manager/_tests/test_qt_plugin_dialog.py b/napari_plugin_manager/_tests/test_qt_plugin_dialog.py index b773281a..9142848f 100644 --- a/napari_plugin_manager/_tests/test_qt_plugin_dialog.py +++ b/napari_plugin_manager/_tests/test_qt_plugin_dialog.py @@ -561,3 +561,10 @@ def test_shortcut_quit(plugin_dialog, qtbot): ) qtbot.wait(200) assert not plugin_dialog.isVisible() + + +def test_disclaimer_widget(plugin_dialog, qtbot): + assert not plugin_dialog.disclaimer_widget.isVisible() + plugin_dialog._show_disclaimer = True + plugin_dialog.exec_() + assert plugin_dialog.disclaimer_widget.isVisible() From 14b4f4f4c9ccf46cb2f03baba7fdaf03d8e5afb8 Mon Sep 17 00:00:00 2001 From: dalthviz <16781833+dalthviz@users.noreply.github.com> Date: Fri, 16 Aug 2024 13:25:38 -0500 Subject: [PATCH 4/5] Basic attempt to handle show_disclaimer value without relaying on napari settings --- napari_plugin_manager/config.py | 34 +++++++++++++++++++++++ napari_plugin_manager/qt_plugin_dialog.py | 9 +++--- 2 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 napari_plugin_manager/config.py diff --git a/napari_plugin_manager/config.py b/napari_plugin_manager/config.py new file mode 100644 index 00000000..0a8687c2 --- /dev/null +++ b/napari_plugin_manager/config.py @@ -0,0 +1,34 @@ +import configparser +from pathlib import Path + +DEFAULT_CONFIG_PATH = Path.home() / ".napari-plugin-manager" +DEFAULT_CONFIG_FILE_PATH = DEFAULT_CONFIG_PATH / "napari-plugin-manager.ini" + + +def get_configuration(): + """ + Get plugin manager configuration. + + Currently only used to store need to show an initial disclaimer message: + * `['general']['show_disclaimer']` -> bool + """ + DEFAULT_CONFIG_PATH.mkdir(exist_ok=True) + config = configparser.ConfigParser() + + if DEFAULT_CONFIG_FILE_PATH.exists(): + config.read(DEFAULT_CONFIG_FILE_PATH) + # Since the config was stored ensure the disclamer config is now `False` + # an update save config for the next time + if config.getboolean("general", "show_disclaimer"): + config.set("general", "show_disclaimer", "False") + with open(DEFAULT_CONFIG_FILE_PATH, "w") as configfile: + config.write(configfile) + else: + # Set default config + config["general"] = {"show_disclaimer": True} + + # Write the configuration to a file + with open(DEFAULT_CONFIG_FILE_PATH, "w") as configfile: + config.write(configfile) + + return config diff --git a/napari_plugin_manager/qt_plugin_dialog.py b/napari_plugin_manager/qt_plugin_dialog.py index 51584a18..9f130c9b 100644 --- a/napari_plugin_manager/qt_plugin_dialog.py +++ b/napari_plugin_manager/qt_plugin_dialog.py @@ -44,6 +44,7 @@ ) from superqt import QCollapsible, QElidingLabel +from napari_plugin_manager.config import get_configuration from napari_plugin_manager.npe2api import ( cache_clear, iter_napari_plugin_info, @@ -848,9 +849,7 @@ def filter(self, text: str, starts_with_chars: int = 1): class QtPluginDialog(QDialog): - def __init__( - self, parent=None, prefix=None, show_disclaimer=False - ) -> None: + def __init__(self, parent=None, prefix=None) -> None: super().__init__(parent) self._parent = parent @@ -864,7 +863,9 @@ def __init__( self.available_set = set() self._prefix = prefix self._first_open = True - self._show_disclaimer = show_disclaimer + self._show_disclaimer = get_configuration().getboolean( + 'general', 'show_disclaimer' + ) self._plugin_queue = [] # Store plugin data to be added self._plugin_data = [] # Store all plugin data self._filter_texts = [] From 0021064bb77bcac97c2fe73b5a75278d706e6cec Mon Sep 17 00:00:00 2001 From: dalthviz <16781833+dalthviz@users.noreply.github.com> Date: Mon, 19 Aug 2024 13:31:40 -0500 Subject: [PATCH 5/5] Add initial test for config logic --- napari_plugin_manager/_tests/test_config.py | 28 +++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 napari_plugin_manager/_tests/test_config.py diff --git a/napari_plugin_manager/_tests/test_config.py b/napari_plugin_manager/_tests/test_config.py new file mode 100644 index 00000000..087c5678 --- /dev/null +++ b/napari_plugin_manager/_tests/test_config.py @@ -0,0 +1,28 @@ +from unittest.mock import patch + +from napari_plugin_manager import config + + +def test_config_file(tmp_path): + TMP_DEFAULT_CONFIG_PATH = tmp_path / ".napari-plugin-manager" + TMP_DEFAULT_CONFIG_FILE_PATH = ( + TMP_DEFAULT_CONFIG_PATH / "napari-plugin-manager.ini" + ) + + assert not TMP_DEFAULT_CONFIG_PATH.exists() + assert not TMP_DEFAULT_CONFIG_FILE_PATH.exists() + + with ( + patch.object(config, "DEFAULT_CONFIG_PATH", TMP_DEFAULT_CONFIG_PATH), + patch.object( + config, "DEFAULT_CONFIG_FILE_PATH", TMP_DEFAULT_CONFIG_FILE_PATH + ), + ): + initial_config = config.get_configuration() + assert TMP_DEFAULT_CONFIG_PATH.exists() + assert TMP_DEFAULT_CONFIG_FILE_PATH.exists() + assert initial_config.getboolean("general", "show_disclaimer") + second_config = config.get_configuration() + assert TMP_DEFAULT_CONFIG_PATH.exists() + assert TMP_DEFAULT_CONFIG_FILE_PATH.exists() + assert not second_config.getboolean("general", "show_disclaimer")