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") diff --git a/napari_plugin_manager/_tests/test_qt_plugin_dialog.py b/napari_plugin_manager/_tests/test_qt_plugin_dialog.py index 2a7b8bbf..83a3cd6b 100644 --- a/napari_plugin_manager/_tests/test_qt_plugin_dialog.py +++ b/napari_plugin_manager/_tests/test_qt_plugin_dialog.py @@ -583,3 +583,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() 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 096e80f9..2409a3b4 100644 --- a/napari_plugin_manager/qt_plugin_dialog.py +++ b/napari_plugin_manager/qt_plugin_dialog.py @@ -53,6 +53,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, @@ -63,7 +64,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. @@ -871,6 +872,9 @@ def __init__(self, parent=None, prefix=None) -> None: self.available_set = set() self._prefix = prefix self._first_open = True + 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 = [] @@ -1152,7 +1156,6 @@ def _setup_ui(self): horizontal_mid_layout.addStretch() horizontal_mid_layout.addWidget(self.refresh_button) mid_layout.addLayout(horizontal_mid_layout) - # mid_layout.addWidget(self.packages_filter) mid_layout.addWidget(self.installed_label) lay.addLayout(mid_layout) @@ -1167,6 +1170,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) @@ -1255,6 +1264,8 @@ def _setup_ui(self): self.show_status_btn.setChecked(False) self.show_status_btn.toggled.connect(self.toggle_status) + self.disclaimer_widget.setVisible(self._show_disclaimer) + self.v_splitter.setStretchFactor(1, 2) self.h_splitter.setStretchFactor(0, 2) @@ -1456,6 +1467,7 @@ def exec_(self): if plugin_dialog != self: self.close() + 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 6f6b2306..8e0a6509 100644 --- a/napari_plugin_manager/styles.qss +++ b/napari_plugin_manager/styles.qss @@ -145,6 +145,16 @@ QPushButton#refresh_button:disabled { font-style: italic; } +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{ background: {{ background }}; color: {{ opacity(text, 200) }};