diff --git a/.github/workflows/test_napari_widgets.yml b/.github/workflows/test_napari_widgets.yml index a08c64091..ae56371e5 100644 --- a/.github/workflows/test_napari_widgets.yml +++ b/.github/workflows/test_napari_widgets.yml @@ -55,7 +55,7 @@ jobs: uses: GabrielBB/xvfb-action@v1 timeout-minutes: 60 with: - run: tox + run: python -m tox env: PLATFORM: ${{ matrix.platform }} NAPARI: ${{ matrix.napari_version }} diff --git a/package/PartSeg/_launcher/main_window.py b/package/PartSeg/_launcher/main_window.py index b32e69e0c..474bbb660 100644 --- a/package/PartSeg/_launcher/main_window.py +++ b/package/PartSeg/_launcher/main_window.py @@ -3,7 +3,7 @@ import warnings from functools import partial -from qtpy.QtCore import QSize, Qt, QThread +from qtpy.QtCore import QSize, Qt, QThread, Signal from qtpy.QtGui import QIcon from qtpy.QtWidgets import QGridLayout, QMainWindow, QMessageBox, QProgressBar, QToolButton, QWidget @@ -39,10 +39,11 @@ def run(self): self.result = partial(main_window, settings=settings, initial_image=im) -class MainWindow(QMainWindow): - def __init__(self, title): - super().__init__() - self.setWindowTitle(title) +class PartSegGUILauncher(QWidget): + launched = Signal() + + def __init__(self, parent=None): + super().__init__(parent) self.lib_path = "" self.final_title = "" analysis_icon = QIcon(os.path.join(icons_dir, "icon.png")) @@ -66,28 +67,10 @@ def __init__(self, title): layout.addWidget(self.progress, 0, 0, 1, 2) layout.addWidget(self.analysis_button, 1, 1) layout.addWidget(self.mask_button, 1, 0) - widget = QWidget() - widget.setLayout(layout) - self.setCentralWidget(widget) - self.setWindowIcon(analysis_icon) - self.prepare = None - self.wind = None - self._update_theme() - - def _update_theme(self): - napari_settings = napari_get_settings(state_store.save_folder) - with warnings.catch_warnings(): - warnings.simplefilter("ignore", FutureWarning) - theme = get_theme(napari_settings.appearance.theme) - # TODO understand qss overwrite mechanism - self.setStyleSheet(napari_template(get_stylesheet(), **theme)) - - def _launch_begin(self): - self.progress.setVisible(True) - self.progress.setRange(0, 0) - self.analysis_button.setDisabled(True) - self.mask_button.setDisabled(True) - import_config() + self.setLayout(layout) + self.prepare = Prepare("") + self.prepare.finished.connect(self.launch) + self.wind = [] def launch_analysis(self): self._launch_begin() @@ -97,8 +80,7 @@ def launch_analysis(self): def _launch_analysis(self): self.lib_path = "PartSeg._roi_analysis.main_window" self.final_title = f"{APP_NAME} {ANALYSIS_NAME}" - self.prepare = Prepare(self.lib_path) - self.prepare.finished.connect(self.launch) + self.prepare.module = self.lib_path def launch_mask(self): self._launch_begin() @@ -108,17 +90,26 @@ def launch_mask(self): def _launch_mask(self): self.lib_path = "PartSeg._roi_mask.main_window" self.final_title = f"{APP_NAME} {MASK_NAME}" - self.prepare = Prepare(self.lib_path) - self.prepare.finished.connect(self.launch) + self.prepare.module = self.lib_path + + def _launch_begin(self): + self.progress.setVisible(True) + self.progress.setRange(0, 0) + self.analysis_button.setDisabled(True) + self.mask_button.setDisabled(True) + import_config() def window_shown(self): - self.close() + self.progress.setHidden(True) + self.analysis_button.setEnabled(True) + self.mask_button.setEnabled(True) + self.launched.emit() def launch(self): - if self.prepare.result is None: + if self.prepare.result is None: # pragma: no cover self.close() return - if self.prepare.errors: + if self.prepare.errors: # pragma: no cover errors_message = QMessageBox() errors_message.setText("There are errors during start") errors_message.setInformativeText( @@ -126,10 +117,29 @@ def launch(self): "The files has prepared backup copies in state directory (Help > State directory)" ) errors_message.setStandardButtons(QMessageBox.Ok) - text = "\n".join("File: " + x[0] + "\n" + str(x[1]) for x in self.prepare.errors) + text = "\n".join(f"File: {x[0]}" + "\n" + str(x[1]) for x in self.prepare.errors) errors_message.setDetailedText(text) errors_message.exec_() wind = self.prepare.result(title=self.final_title, signal_fun=self.window_shown) wind.show() - self.wind = wind + self.wind.append(wind) + + +class MainWindow(QMainWindow): + def __init__(self, title): + super().__init__() + self.setWindowTitle(title) + widget = PartSegGUILauncher(self) + widget.launched.connect(self.close) + self.setCentralWidget(widget) + self.setWindowIcon(QIcon(os.path.join(icons_dir, "icon.png"))) + self._update_theme() + + def _update_theme(self): + napari_settings = napari_get_settings(state_store.save_folder) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", FutureWarning) + theme = get_theme(napari_settings.appearance.theme) + # TODO understand qss overwrite mechanism + self.setStyleSheet(napari_template(get_stylesheet(), **theme)) diff --git a/package/PartSeg/common_gui/algorithms_description.py b/package/PartSeg/common_gui/algorithms_description.py index d5e1e5e18..5593e8f1d 100644 --- a/package/PartSeg/common_gui/algorithms_description.py +++ b/package/PartSeg/common_gui/algorithms_description.py @@ -140,6 +140,7 @@ def from_algorithm_property(cls, ob): possible_values=ob.possible_values, help_text=ob.help_text, per_dimension=ob.per_dimension, + mgi_options=ob.mgi_options, ) if isinstance(ob, str): return QLabel(ob) @@ -189,7 +190,7 @@ def _get_field_from_value_type(cls, ap: AlgorithmProperty): elif issubclass(ap.value_type, BaseModel): res = FieldsList([cls.from_algorithm_property(x) for x in base_model_to_algorithm_property(ap.value_type)]) else: - res = create_widget(value=ap.default_value, annotation=ap.value_type, options={}) + res = create_widget(value=ap.default_value, annotation=ap.value_type, options=ap.mgi_options) return res def _get_field(self) -> typing.Union[QWidget, Widget]: @@ -202,7 +203,7 @@ def _get_field(self) -> typing.Union[QWidget, Widget]: self.per_dimension = True res = ListInput(prop, 3) elif not inspect.isclass(self.value_type): - res = create_widget(value=self.default_value, annotation=self.value_type, options={}) + res = create_widget(value=self.default_value, annotation=self.value_type, options=self.mgi_options) elif hasattr(self.value_type, "get_object"): res = self.value_type.get_object() else: diff --git a/package/PartSeg/plugins/napari_widgets/__init__.py b/package/PartSeg/plugins/napari_widgets/__init__.py index 9df869825..e2d1bb2de 100644 --- a/package/PartSeg/plugins/napari_widgets/__init__.py +++ b/package/PartSeg/plugins/napari_widgets/__init__.py @@ -1,5 +1,6 @@ from napari_plugin_engine import napari_hook_implementation +from PartSeg._launcher.main_window import PartSegGUILauncher from PartSeg.plugins.napari_widgets.mask_create_widget import MaskCreate from PartSeg.plugins.napari_widgets.roi_extraction_algorithms import ROIAnalysisExtraction, ROIMaskExtraction from PartSeg.plugins.napari_widgets.search_label_widget import SearchLabel @@ -37,3 +38,8 @@ def napari_experimental_provide_dock_widget4(): @napari_hook_implementation(specname="napari_experimental_provide_dock_widget") def napari_experimental_provide_dock_widget5(): return SearchLabel + + +@napari_hook_implementation(specname="napari_experimental_provide_dock_widget") +def napari_experimental_provide_dock_widget6(): + return PartSegGUILauncher diff --git a/package/PartSegCore/algorithm_describe_base.py b/package/PartSegCore/algorithm_describe_base.py index f9b957734..f8d52f86f 100644 --- a/package/PartSegCore/algorithm_describe_base.py +++ b/package/PartSegCore/algorithm_describe_base.py @@ -46,6 +46,7 @@ def __init__( value_type=None, help_text="", per_dimension=False, + mgi_options=None, **kwargs, ): if "property_type" in kwargs: @@ -68,6 +69,7 @@ def __init__( self.possible_values = possible_values self.help_text = help_text self.per_dimension = per_dimension + self.mgi_options = mgi_options if mgi_options is not None else {} if self.value_type is list and default_value not in possible_values: raise ValueError(f"default_value ({default_value}) should be one of possible values ({possible_values}).") @@ -583,6 +585,7 @@ def _field_to_algorithm_property(name: str, field: "ModelField"): value_type=value_type, possible_values=possible_values, help_text=help_text, + mgi_options=field.field_info.extra.get("options", {}), ) diff --git a/package/tests/test_PartSeg/test_main_windows.py b/package/tests/test_PartSeg/test_main_windows.py index 2fb5a0123..bd54a7b6c 100644 --- a/package/tests/test_PartSeg/test_main_windows.py +++ b/package/tests/test_PartSeg/test_main_windows.py @@ -1,11 +1,15 @@ +# pylint: disable=R0201 import platform import sys +from functools import partial import pytest import qtpy from qtpy.QtCore import QCoreApplication from PartSeg._launcher.main_window import MainWindow as LauncherMainWindow +from PartSeg._launcher.main_window import PartSegGUILauncher +from PartSeg._launcher.main_window import Prepare as LauncherPrepare from PartSeg._roi_analysis import main_window as analysis_main_window from PartSeg._roi_mask import main_window as mask_main_window @@ -57,13 +61,13 @@ def test_open_mask(self, qtbot, monkeypatch, tmp_path): monkeypatch.setattr(mask_main_window, "CONFIG_FOLDER", str(tmp_path)) if platform.system() == "Linux" and (GITHUB_ACTIONS or TRAVIS): monkeypatch.setattr(mask_main_window.MainWindow, "show", empty) - main_window = LauncherMainWindow("Launcher") + main_window = PartSegGUILauncher() qtbot.addWidget(main_window) - main_window._launch_mask() with qtbot.waitSignal(main_window.prepare.finished, timeout=10**4): - main_window.prepare.start() + main_window.launch_mask() QCoreApplication.processEvents() - main_window.wind.hide() + qtbot.add_widget(main_window.wind[0]) + main_window.wind[0].hide() qtbot.wait(50) # @pytest.mark.skipif((platform.system() == "Linux") and CI_BUILD, reason="vispy problem") @@ -74,13 +78,18 @@ def test_open_analysis(self, qtbot, monkeypatch, tmp_path): monkeypatch.setattr(analysis_main_window, "CONFIG_FOLDER", str(tmp_path)) if platform.system() in {"Darwin", "Linux"} and (GITHUB_ACTIONS or TRAVIS): monkeypatch.setattr(analysis_main_window.MainWindow, "show", empty) - main_window = LauncherMainWindow("Launcher") + main_window = PartSegGUILauncher() qtbot.addWidget(main_window) - main_window._launch_analysis() with qtbot.waitSignal(main_window.prepare.finished): - main_window.prepare.start() + main_window.launch_analysis() QCoreApplication.processEvents() qtbot.wait(50) - qtbot.addWidget(main_window.wind) - main_window.wind.hide() + qtbot.add_widget(main_window.wind[0]) + main_window.wind[0].hide() qtbot.wait(50) + + def test_prepare(self): + prepare = LauncherPrepare("PartSeg._roi_analysis.main_window") + prepare.run() + assert isinstance(prepare.result, partial) + assert prepare.result.func is analysis_main_window.MainWindow diff --git a/package/tests/test_PartSegCore/test_algorithm_describe_base.py b/package/tests/test_PartSegCore/test_algorithm_describe_base.py index fa3f7e4a1..5341336f5 100644 --- a/package/tests/test_PartSegCore/test_algorithm_describe_base.py +++ b/package/tests/test_PartSegCore/test_algorithm_describe_base.py @@ -247,6 +247,14 @@ class ModelWithPosition(BBaseModel): assert property_list[2].name == "field2" +def test_base_model_to_algorithm_property_magicgui_parameters(): + class BBaseModel(BaseModel): + field1: int = Field(1, options={"a": 1, "b": 2}) + + prop = base_model_to_algorithm_property(BBaseModel)[0] + assert prop.mgi_options == {"a": 1, "b": 2} + + class TestAlgorithmDescribeBase: def test_old_style_algorithm(self): class SampleAlgorithm(AlgorithmDescribeBase): diff --git a/tox.ini b/tox.ini index b48e397b5..7ba56ac10 100644 --- a/tox.ini +++ b/tox.ini @@ -25,14 +25,7 @@ NAPARI = napari414: 414 napari415: 415 repo: repo -PLATFORM = - ubuntu-latest: linux - ubuntu-16.04: linux - ubuntu-18.04: linux - ubuntu-20.04: linux - windows-latest: windows - macos-latest: macos - macos-11: macos + [base] deps =