Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Two new "Import Ewoks workflow..." and "Export Ewoks workflow..." menus in the orange main canvas.
- Ewoks workflow file filter (`*.json *.yml *.yaml`) in the Open and Save As dialogs of the orange main canvas.
- New property `auto_execute` in `ewoksorange.gui.owwidgets.OWEwoksBaseWidget`. Allow to propagate outputs downstream without triggering execution.
- Support OASYS2 (`pip install ewoksorange[oasys2]`) next to the standard
`orange-canvas-core` + `orange-widget-base` and the optional Orange3.
Expand Down
4 changes: 2 additions & 2 deletions src/ewoksorange/gui/canvas/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from contextlib import contextmanager

from ...orange_version import ORANGE_VERSION
from .menu import add_ewoks_import_export_actions
from .menu import add_ewoks_file_filters

if ORANGE_VERSION == ORANGE_VERSION.latest_oasys:
from oasys2.canvas import config as _oasys_config_module
Expand Down Expand Up @@ -99,7 +99,7 @@ def _temporary_log_handlers(log_level):


def main(argv=None):
add_ewoks_import_export_actions()
add_ewoks_file_filters()
parser = arg_parser()

parser.add_argument(
Expand Down
152 changes: 105 additions & 47 deletions src/ewoksorange/gui/canvas/menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,29 @@
import tempfile
from pathlib import Path

from AnyQt.QtWidgets import QAction
from AnyQt.QtCore import QUrl
from AnyQt.QtWidgets import QDialog
from AnyQt.QtWidgets import QFileDialog
from AnyQt.QtWidgets import QMenu
from orangecanvas.application import canvasmain
from orangecanvas.application.canvasmain import CanvasMainWindow

from ...orange_version import ORANGE_VERSION
from ..workflows.owscheme import ewoks_to_ows
from ..workflows.owscheme import ows_to_ewoks

_logger = logging.getLogger(__name__)
_SUFFIXES = (".json", ".yml", ".yaml")
_FILTER = "Ewoks workflow (*.json *.yml *.yaml)"
_patched = False


def _import_ewoks_workflow(window: CanvasMainWindow) -> None:
def _is_ewoks_file(filename: str) -> bool:
return Path(filename).suffix.lower() in _SUFFIXES


def _import_ewoks_workflow(window: CanvasMainWindow, filename: str) -> None:
from ...bindings import load_graph # Lazy import to avoid circular import

filename, _ = QFileDialog.getOpenFileName(
window, "Import Ewoks workflow", "", _FILTER
)
if not filename:
return
try:
graph = load_graph(filename)
with tempfile.TemporaryDirectory() as tmpdir:
Expand All @@ -39,15 +40,9 @@ def _import_ewoks_workflow(window: CanvasMainWindow) -> None:
window.output_view().write(f"Ewoks import failed: {e}\n")


def _export_ewoks_workflow(window: CanvasMainWindow) -> None:

def _export_ewoks_workflow(window: CanvasMainWindow, filename: str) -> None:
from ...bindings import save_graph # Lazy import to avoid circular import

filename, _ = QFileDialog.getSaveFileName(
window, "Export Ewoks workflow", "", _FILTER
)
if not filename:
return
if not Path(filename).suffix:
filename += ".json"
try:
Expand All @@ -62,43 +57,106 @@ def _export_ewoks_workflow(window: CanvasMainWindow) -> None:
window.output_view().write(f"Ewoks export failed: {e}\n")


def _add_actions(window: CanvasMainWindow) -> None:
menu = window.menuBar().findChild(QMenu, "file-menu")
if menu is None:
return
import_action = QAction(
"Import Ewoks workflow...", window, objectName="action-import-ewoks"
)
import_action.triggered.connect(lambda: _import_ewoks_workflow(window))
export_action = QAction(
"Export Ewoks workflow...", window, objectName="action-export-ewoks"
)
export_action.triggered.connect(lambda: _export_ewoks_workflow(window))

before = menu.findChild(QAction, "open-actions-separator")
for action in (import_action, export_action):
if before is not None:
menu.insertAction(before, action)
else:
menu.addAction(action)
class _EwoksSaveDialog(QFileDialog):
"""Save As dialog with an extra Ewoks filter.

Choosing an Ewoks file exports the workflow and rejects the dialog, so that
the document path and modified state (and thus Ctrl+S) keep targeting `.ows`.
"""

def add_ewoks_import_export_actions() -> None:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._window = args[0] if args else kwargs.get("parent")

def setNameFilter(self, name_filter):
self.setNameFilters([name_filter, _FILTER])

def exec(self):
accepted = super().exec()
if not accepted:
return accepted
filename = self.selectedFiles()[0]
if not _is_ewoks_file(filename):
return accepted
_export_ewoks_workflow(self._window, filename)
# Reject so any further saving is skipped
return 0

exec_ = exec


def _patch_open_save() -> None:
original_open_dialog = CanvasMainWindow._open_workflow_dialog
original_open_file = CanvasMainWindow.open_scheme_file
original_save_as = CanvasMainWindow.save_scheme_as

def _open_workflow_dialog(self):
dialog = original_open_dialog(self)
dialog.setNameFilters([*dialog.nameFilters(), _FILTER])
return dialog

def open_scheme_file(self, filename, **kwargs):
path = filename.toLocalFile() if isinstance(filename, QUrl) else filename
if _is_ewoks_file(path):
_import_ewoks_workflow(self, path)
else:
original_open_file(self, filename, **kwargs)

def save_scheme_as(self):
original = canvasmain.QFileDialog
canvasmain.QFileDialog = _EwoksSaveDialog
try:
return original_save_as(self)
finally:
canvasmain.QFileDialog = original

CanvasMainWindow._open_workflow_dialog = _open_workflow_dialog
CanvasMainWindow.open_scheme_file = open_scheme_file
CanvasMainWindow.save_scheme_as = save_scheme_as


def _patch_oasys_open() -> None:
"""OASYS2 has its own Open dialog (a static ``QFileDialog.getOpenFileName``)."""
# Imported late: it must come after the OasysConfig patch of `canvas.main`.
from oasys2.canvas.application import canvasmain as oasys_canvasmain

window_class = oasys_canvasmain.OASYSMainWindow
original_open = window_class._open_scheme
original_load = window_class._load_scheme

class _OasysOpenDialog(QFileDialog):
@staticmethod
def getOpenFileName(parent=None, caption="", directory="", filter="", *args):
return QFileDialog.getOpenFileName(
parent, caption, directory, f"{filter};;{_FILTER}", *args
)

def _open_scheme(self):
original = oasys_canvasmain.QFileDialog
oasys_canvasmain.QFileDialog = _OasysOpenDialog
try:
return original_open(self)
finally:
oasys_canvasmain.QFileDialog = original

def _load_scheme(self, filename):
if _is_ewoks_file(str(filename)):
_import_ewoks_workflow(self, str(filename))
return QDialog.Accepted
return original_load(self, filename)

window_class._open_scheme = _open_scheme
window_class._load_scheme = _load_scheme


def add_ewoks_file_filters() -> None:
"""
Add Ewoks import/export to the File menu of every canvas window.
Add the Ewoks workflow filter to the Open and Save As dialogs of the canvas.
"""
global _patched
if _patched:
return
_patched = True

# NOTE: There is no other way than a patch here because
# - We need a solution that works also with Oasys2 fork
# - Main code where the CanvasMainWindow is instantiated is in Orange3 or in Oasys2 and there is no public interface given.
original_init = CanvasMainWindow.__init__

def __init__(self, *args, **kwargs):
original_init(self, *args, **kwargs)
_add_actions(self)

CanvasMainWindow.__init__ = __init__
_patch_open_save()
if ORANGE_VERSION == ORANGE_VERSION.latest_oasys:
_patch_oasys_open()
51 changes: 34 additions & 17 deletions src/ewoksorange/tests/test_canvas_io.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import json

import pytest

from ewoksorange.gui.canvas import menu
from ewoksorange.gui.canvas.handler import OrangeCanvasHandler
from ewoksorange.orange_version import ORANGE_VERSION


def test_exported_json_matches_manual_workflow(
tmp_path, monkeypatch, ewoksorange_qtapp
):
def test_exported_json_matches_manual_workflow(tmp_path, ewoksorange_qtapp):
destination_path = tmp_path / "exported_workflow.json"
graph = {
"nodes": [
Expand Down Expand Up @@ -38,24 +39,40 @@ def test_exported_json_matches_manual_workflow(
# Here i import to the canvas then re export it and verify the exported json.

with OrangeCanvasHandler() as handler:
monkeypatch.setattr(
menu.QFileDialog,
"getOpenFileName",
lambda *args, **kwargs: (str(source_path), ""),
)

menu._import_ewoks_workflow(handler.canvas)

monkeypatch.setattr(
menu.QFileDialog,
"getSaveFileName",
lambda *_args, **_kwargs: (str(destination_path), ""),
)
menu._export_ewoks_workflow(handler.canvas)
menu._import_ewoks_workflow(handler.canvas, str(source_path))
menu._export_ewoks_workflow(handler.canvas, str(destination_path))

with open(destination_path) as f:
exported_workflow = json.load(f)

assert exported_workflow["nodes"][0]["id"] == "source"
assert exported_workflow["nodes"][1]["id"] == "sink"
assert exported_workflow["links"] == graph["links"]


@pytest.mark.skipif(
ORANGE_VERSION == ORANGE_VERSION.latest_oasys, reason="OASYS2 has its own dialog"
)
def test_open_dialog_has_ewoks_filter(ewoksorange_qtapp):
menu.add_ewoks_file_filters()
with OrangeCanvasHandler() as handler:
dialog = handler.canvas._open_workflow_dialog()
assert menu._FILTER in dialog.nameFilters()


@pytest.mark.skipif(
ORANGE_VERSION != ORANGE_VERSION.latest_oasys, reason="OASYS2 specific"
)
def test_oasys_open_dialog_has_ewoks_filter(monkeypatch, ewoksorange_qtapp):
from oasys2.canvas.application.canvasmain import OASYSMainWindow

menu.add_ewoks_file_filters()
filters = []

def get_open_file_name(parent, caption, directory, name_filter, *args):
filters.append(name_filter)
return "", ""

monkeypatch.setattr(menu.QFileDialog, "getOpenFileName", get_open_file_name)
OASYSMainWindow()._open_scheme()
assert menu._FILTER in filters[0]
Loading