Skip to content

Commit

Permalink
feat: Launch PartSeg GUI from napari (#581)
Browse files Browse the repository at this point in the history
  • Loading branch information
Czaki authored Apr 14, 2022
1 parent e31d28d commit 0f31f06
Show file tree
Hide file tree
Showing 8 changed files with 86 additions and 56 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test_napari_widgets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
82 changes: 46 additions & 36 deletions package/PartSeg/_launcher/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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"))
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -108,28 +90,56 @@ 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(
"During load saved state some of data could not be load properly\n"
"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))
5 changes: 3 additions & 2 deletions package/PartSeg/common_gui/algorithms_description.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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]:
Expand All @@ -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:
Expand Down
6 changes: 6 additions & 0 deletions package/PartSeg/plugins/napari_widgets/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
3 changes: 3 additions & 0 deletions package/PartSegCore/algorithm_describe_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def __init__(
value_type=None,
help_text="",
per_dimension=False,
mgi_options=None,
**kwargs,
):
if "property_type" in kwargs:
Expand All @@ -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}).")

Expand Down Expand Up @@ -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", {}),
)


Expand Down
27 changes: 18 additions & 9 deletions package/tests/test_PartSeg/test_main_windows.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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")
Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
9 changes: 1 addition & 8 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down

0 comments on commit 0f31f06

Please sign in to comment.