Skip to content

Commit 0f31f06

Browse files
authored
feat: Launch PartSeg GUI from napari (#581)
1 parent e31d28d commit 0f31f06

8 files changed

Lines changed: 86 additions & 56 deletions

File tree

.github/workflows/test_napari_widgets.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ jobs:
5555
uses: GabrielBB/xvfb-action@v1
5656
timeout-minutes: 60
5757
with:
58-
run: tox
58+
run: python -m tox
5959
env:
6060
PLATFORM: ${{ matrix.platform }}
6161
NAPARI: ${{ matrix.napari_version }}

package/PartSeg/_launcher/main_window.py

Lines changed: 46 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import warnings
44
from functools import partial
55

6-
from qtpy.QtCore import QSize, Qt, QThread
6+
from qtpy.QtCore import QSize, Qt, QThread, Signal
77
from qtpy.QtGui import QIcon
88
from qtpy.QtWidgets import QGridLayout, QMainWindow, QMessageBox, QProgressBar, QToolButton, QWidget
99

@@ -39,10 +39,11 @@ def run(self):
3939
self.result = partial(main_window, settings=settings, initial_image=im)
4040

4141

42-
class MainWindow(QMainWindow):
43-
def __init__(self, title):
44-
super().__init__()
45-
self.setWindowTitle(title)
42+
class PartSegGUILauncher(QWidget):
43+
launched = Signal()
44+
45+
def __init__(self, parent=None):
46+
super().__init__(parent)
4647
self.lib_path = ""
4748
self.final_title = ""
4849
analysis_icon = QIcon(os.path.join(icons_dir, "icon.png"))
@@ -66,28 +67,10 @@ def __init__(self, title):
6667
layout.addWidget(self.progress, 0, 0, 1, 2)
6768
layout.addWidget(self.analysis_button, 1, 1)
6869
layout.addWidget(self.mask_button, 1, 0)
69-
widget = QWidget()
70-
widget.setLayout(layout)
71-
self.setCentralWidget(widget)
72-
self.setWindowIcon(analysis_icon)
73-
self.prepare = None
74-
self.wind = None
75-
self._update_theme()
76-
77-
def _update_theme(self):
78-
napari_settings = napari_get_settings(state_store.save_folder)
79-
with warnings.catch_warnings():
80-
warnings.simplefilter("ignore", FutureWarning)
81-
theme = get_theme(napari_settings.appearance.theme)
82-
# TODO understand qss overwrite mechanism
83-
self.setStyleSheet(napari_template(get_stylesheet(), **theme))
84-
85-
def _launch_begin(self):
86-
self.progress.setVisible(True)
87-
self.progress.setRange(0, 0)
88-
self.analysis_button.setDisabled(True)
89-
self.mask_button.setDisabled(True)
90-
import_config()
70+
self.setLayout(layout)
71+
self.prepare = Prepare("")
72+
self.prepare.finished.connect(self.launch)
73+
self.wind = []
9174

9275
def launch_analysis(self):
9376
self._launch_begin()
@@ -97,8 +80,7 @@ def launch_analysis(self):
9780
def _launch_analysis(self):
9881
self.lib_path = "PartSeg._roi_analysis.main_window"
9982
self.final_title = f"{APP_NAME} {ANALYSIS_NAME}"
100-
self.prepare = Prepare(self.lib_path)
101-
self.prepare.finished.connect(self.launch)
83+
self.prepare.module = self.lib_path
10284

10385
def launch_mask(self):
10486
self._launch_begin()
@@ -108,28 +90,56 @@ def launch_mask(self):
10890
def _launch_mask(self):
10991
self.lib_path = "PartSeg._roi_mask.main_window"
11092
self.final_title = f"{APP_NAME} {MASK_NAME}"
111-
self.prepare = Prepare(self.lib_path)
112-
self.prepare.finished.connect(self.launch)
93+
self.prepare.module = self.lib_path
94+
95+
def _launch_begin(self):
96+
self.progress.setVisible(True)
97+
self.progress.setRange(0, 0)
98+
self.analysis_button.setDisabled(True)
99+
self.mask_button.setDisabled(True)
100+
import_config()
113101

114102
def window_shown(self):
115-
self.close()
103+
self.progress.setHidden(True)
104+
self.analysis_button.setEnabled(True)
105+
self.mask_button.setEnabled(True)
106+
self.launched.emit()
116107

117108
def launch(self):
118-
if self.prepare.result is None:
109+
if self.prepare.result is None: # pragma: no cover
119110
self.close()
120111
return
121-
if self.prepare.errors:
112+
if self.prepare.errors: # pragma: no cover
122113
errors_message = QMessageBox()
123114
errors_message.setText("There are errors during start")
124115
errors_message.setInformativeText(
125116
"During load saved state some of data could not be load properly\n"
126117
"The files has prepared backup copies in state directory (Help > State directory)"
127118
)
128119
errors_message.setStandardButtons(QMessageBox.Ok)
129-
text = "\n".join("File: " + x[0] + "\n" + str(x[1]) for x in self.prepare.errors)
120+
text = "\n".join(f"File: {x[0]}" + "\n" + str(x[1]) for x in self.prepare.errors)
130121

131122
errors_message.setDetailedText(text)
132123
errors_message.exec_()
133124
wind = self.prepare.result(title=self.final_title, signal_fun=self.window_shown)
134125
wind.show()
135-
self.wind = wind
126+
self.wind.append(wind)
127+
128+
129+
class MainWindow(QMainWindow):
130+
def __init__(self, title):
131+
super().__init__()
132+
self.setWindowTitle(title)
133+
widget = PartSegGUILauncher(self)
134+
widget.launched.connect(self.close)
135+
self.setCentralWidget(widget)
136+
self.setWindowIcon(QIcon(os.path.join(icons_dir, "icon.png")))
137+
self._update_theme()
138+
139+
def _update_theme(self):
140+
napari_settings = napari_get_settings(state_store.save_folder)
141+
with warnings.catch_warnings():
142+
warnings.simplefilter("ignore", FutureWarning)
143+
theme = get_theme(napari_settings.appearance.theme)
144+
# TODO understand qss overwrite mechanism
145+
self.setStyleSheet(napari_template(get_stylesheet(), **theme))

package/PartSeg/common_gui/algorithms_description.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ def from_algorithm_property(cls, ob):
140140
possible_values=ob.possible_values,
141141
help_text=ob.help_text,
142142
per_dimension=ob.per_dimension,
143+
mgi_options=ob.mgi_options,
143144
)
144145
if isinstance(ob, str):
145146
return QLabel(ob)
@@ -189,7 +190,7 @@ def _get_field_from_value_type(cls, ap: AlgorithmProperty):
189190
elif issubclass(ap.value_type, BaseModel):
190191
res = FieldsList([cls.from_algorithm_property(x) for x in base_model_to_algorithm_property(ap.value_type)])
191192
else:
192-
res = create_widget(value=ap.default_value, annotation=ap.value_type, options={})
193+
res = create_widget(value=ap.default_value, annotation=ap.value_type, options=ap.mgi_options)
193194
return res
194195

195196
def _get_field(self) -> typing.Union[QWidget, Widget]:
@@ -202,7 +203,7 @@ def _get_field(self) -> typing.Union[QWidget, Widget]:
202203
self.per_dimension = True
203204
res = ListInput(prop, 3)
204205
elif not inspect.isclass(self.value_type):
205-
res = create_widget(value=self.default_value, annotation=self.value_type, options={})
206+
res = create_widget(value=self.default_value, annotation=self.value_type, options=self.mgi_options)
206207
elif hasattr(self.value_type, "get_object"):
207208
res = self.value_type.get_object()
208209
else:

package/PartSeg/plugins/napari_widgets/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from napari_plugin_engine import napari_hook_implementation
22

3+
from PartSeg._launcher.main_window import PartSegGUILauncher
34
from PartSeg.plugins.napari_widgets.mask_create_widget import MaskCreate
45
from PartSeg.plugins.napari_widgets.roi_extraction_algorithms import ROIAnalysisExtraction, ROIMaskExtraction
56
from PartSeg.plugins.napari_widgets.search_label_widget import SearchLabel
@@ -37,3 +38,8 @@ def napari_experimental_provide_dock_widget4():
3738
@napari_hook_implementation(specname="napari_experimental_provide_dock_widget")
3839
def napari_experimental_provide_dock_widget5():
3940
return SearchLabel
41+
42+
43+
@napari_hook_implementation(specname="napari_experimental_provide_dock_widget")
44+
def napari_experimental_provide_dock_widget6():
45+
return PartSegGUILauncher

package/PartSegCore/algorithm_describe_base.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ def __init__(
4646
value_type=None,
4747
help_text="",
4848
per_dimension=False,
49+
mgi_options=None,
4950
**kwargs,
5051
):
5152
if "property_type" in kwargs:
@@ -68,6 +69,7 @@ def __init__(
6869
self.possible_values = possible_values
6970
self.help_text = help_text
7071
self.per_dimension = per_dimension
72+
self.mgi_options = mgi_options if mgi_options is not None else {}
7173
if self.value_type is list and default_value not in possible_values:
7274
raise ValueError(f"default_value ({default_value}) should be one of possible values ({possible_values}).")
7375

@@ -583,6 +585,7 @@ def _field_to_algorithm_property(name: str, field: "ModelField"):
583585
value_type=value_type,
584586
possible_values=possible_values,
585587
help_text=help_text,
588+
mgi_options=field.field_info.extra.get("options", {}),
586589
)
587590

588591

package/tests/test_PartSeg/test_main_windows.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
# pylint: disable=R0201
12
import platform
23
import sys
4+
from functools import partial
35

46
import pytest
57
import qtpy
68
from qtpy.QtCore import QCoreApplication
79

810
from PartSeg._launcher.main_window import MainWindow as LauncherMainWindow
11+
from PartSeg._launcher.main_window import PartSegGUILauncher
12+
from PartSeg._launcher.main_window import Prepare as LauncherPrepare
913
from PartSeg._roi_analysis import main_window as analysis_main_window
1014
from PartSeg._roi_mask import main_window as mask_main_window
1115

@@ -57,13 +61,13 @@ def test_open_mask(self, qtbot, monkeypatch, tmp_path):
5761
monkeypatch.setattr(mask_main_window, "CONFIG_FOLDER", str(tmp_path))
5862
if platform.system() == "Linux" and (GITHUB_ACTIONS or TRAVIS):
5963
monkeypatch.setattr(mask_main_window.MainWindow, "show", empty)
60-
main_window = LauncherMainWindow("Launcher")
64+
main_window = PartSegGUILauncher()
6165
qtbot.addWidget(main_window)
62-
main_window._launch_mask()
6366
with qtbot.waitSignal(main_window.prepare.finished, timeout=10**4):
64-
main_window.prepare.start()
67+
main_window.launch_mask()
6568
QCoreApplication.processEvents()
66-
main_window.wind.hide()
69+
qtbot.add_widget(main_window.wind[0])
70+
main_window.wind[0].hide()
6771
qtbot.wait(50)
6872

6973
# @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):
7478
monkeypatch.setattr(analysis_main_window, "CONFIG_FOLDER", str(tmp_path))
7579
if platform.system() in {"Darwin", "Linux"} and (GITHUB_ACTIONS or TRAVIS):
7680
monkeypatch.setattr(analysis_main_window.MainWindow, "show", empty)
77-
main_window = LauncherMainWindow("Launcher")
81+
main_window = PartSegGUILauncher()
7882
qtbot.addWidget(main_window)
79-
main_window._launch_analysis()
8083
with qtbot.waitSignal(main_window.prepare.finished):
81-
main_window.prepare.start()
84+
main_window.launch_analysis()
8285
QCoreApplication.processEvents()
8386
qtbot.wait(50)
84-
qtbot.addWidget(main_window.wind)
85-
main_window.wind.hide()
87+
qtbot.add_widget(main_window.wind[0])
88+
main_window.wind[0].hide()
8689
qtbot.wait(50)
90+
91+
def test_prepare(self):
92+
prepare = LauncherPrepare("PartSeg._roi_analysis.main_window")
93+
prepare.run()
94+
assert isinstance(prepare.result, partial)
95+
assert prepare.result.func is analysis_main_window.MainWindow

package/tests/test_PartSegCore/test_algorithm_describe_base.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,14 @@ class ModelWithPosition(BBaseModel):
247247
assert property_list[2].name == "field2"
248248

249249

250+
def test_base_model_to_algorithm_property_magicgui_parameters():
251+
class BBaseModel(BaseModel):
252+
field1: int = Field(1, options={"a": 1, "b": 2})
253+
254+
prop = base_model_to_algorithm_property(BBaseModel)[0]
255+
assert prop.mgi_options == {"a": 1, "b": 2}
256+
257+
250258
class TestAlgorithmDescribeBase:
251259
def test_old_style_algorithm(self):
252260
class SampleAlgorithm(AlgorithmDescribeBase):

tox.ini

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,7 @@ NAPARI =
2525
napari414: 414
2626
napari415: 415
2727
repo: repo
28-
PLATFORM =
29-
ubuntu-latest: linux
30-
ubuntu-16.04: linux
31-
ubuntu-18.04: linux
32-
ubuntu-20.04: linux
33-
windows-latest: windows
34-
macos-latest: macos
35-
macos-11: macos
28+
3629

3730
[base]
3831
deps =

0 commit comments

Comments
 (0)