Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +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.
- 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
2 changes: 2 additions & 0 deletions src/ewoksorange/gui/canvas/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from contextlib import contextmanager

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

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


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

parser.add_argument(
Expand Down
102 changes: 102 additions & 0 deletions src/ewoksorange/gui/canvas/menu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import io
import logging
import os
import tempfile
from pathlib import Path

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

from ..workflows.owscheme import ewoks_to_ows
from ..workflows.owscheme import ows_to_ewoks

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


def _import_ewoks_workflow(window: CanvasMainWindow) -> 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:
ows = os.path.join(tmpdir, "imported.ows")
ewoks_to_ows(graph, ows, error_on_duplicates=False)
# Opens in this window if it is empty, else in a new one,
# without associating the temporary path with the document.
window.open_example_scheme(ows)
except Exception as e:
_logger.exception("Ewoks import failed")
window.output_view().write(f"Ewoks import failed: {e}\n")


def _export_ewoks_workflow(window: CanvasMainWindow) -> 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:
scheme = window.current_document().scheme()
buffer = io.BytesIO()
scheme.save_to(buffer, pretty=True, pickle_fallback=True)
buffer.seek(0)
graph = ows_to_ewoks(buffer)
save_graph(graph, filename)
except Exception as e:
_logger.exception("Ewoks export failed")
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)


def add_ewoks_import_export_actions() -> None:
"""
Add Ewoks import/export to the File menu of every canvas window.
"""
global _patched
if _patched:
return
_patched = True

# CanvasMainWindow is instantiated both in Orange3 or in Oasys2 and there is no public interface given.
original_init = CanvasMainWindow.__init__

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have verified in the code and Oasys / Orange looks to both use orangecanvas.application.canvasmaiCanvasMainWindow so it should works

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put this as comment perhaps?


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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding actions is a good idea. Perhaps there are other ways to do this without patching? Not sure.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. One question is: how much different is it from the already existing Save / Save As / Open function that already exists ?
At the end this is only the file format that changes right ?

@maxenceprog maxenceprog Oct 15, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes we save as a json ewoks workflow

equivalent to an ewoks convert .ows <-> .json

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I wasn’t clear. My question is whether we should add new actions or, if possible, reuse the existing one and adapt the current code (QDialog, save/load functions).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can investigate first if this is possible (Without going to deep into monkey patching / changing orange canvas api)

But in my opinion. It looks normal for me to have an "export import" pattern. --> The native way to save load orange workflow is ows.

For me this is the same as word. You save / load as docx but you can export import as open office format

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with you on the technical side. That said, I keep wondering: what is the user actually expecting here? Could it be confusing for them to have to choose between saving and exporting? And are we at risk of ending up with too many actions?

I’m not pushing for one option over the other, but I think we should at least ask ourselves these questions first. The technical side can come second.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we were to ignore completely the technical side, yes I agree that it would be better to have "Ewoks workflow" as a new filter in the open/saving box:

image

I'll try to have Claude provide this as an alternative to compare but no promises.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #504

As expected, four things need patching: CanvasMainWindow._open_workflow_dialog, CanvasMainWindow.open_scheme_file, CanvasMainWindow.save_scheme_as and the QFileDialog for saving.

Also, the opening for OASYS needs to be patched separately since the implementation is slightly different.

What do you think @payno ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard to say. Modifying _open_workflow_dialog and QFileDialog is playing with fire.

But save_scheme_as could be fully rewrite or we could ask the orange team to do some modifications.
_open_workflow_dialog is used at three places where there is public API:

        def open_scheme():
            dlg = self._open_workflow_dialog()
            dlg.setParent(dialog, Qt.Dialog)
            dlg.fileSelected.connect(self.open_scheme_file)
            dlg.accepted.connect(dialog.accept)
            dlg.exec()
        ...
    def open_scheme(self):
        # type: () -> None
        """
        Open a user selected workflow in a new window.
        """
        dlg = self._open_workflow_dialog()
        dlg.fileSelected.connect(self.open_scheme_file)
        dlg.exec()

    def open_and_freeze_scheme(self):
        # type: () -> None
        """
        Open a user selected workflow file in a new window and freeze
        signal propagation.
        """
        dlg = self._open_workflow_dialog()
        dlg.fileSelected.connect(partial(self.open_scheme_file, freeze=True))
        dlg.exec()

So I agree this is more code to maintain. But at a first glance it could be 'clean' enough.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's discuss this in the next workflow meeting to reach a collective agreement then.

In the mean time, I think we can merge this PR


CanvasMainWindow.__init__ = __init__
61 changes: 61 additions & 0 deletions src/ewoksorange/tests/test_canvas_io.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import json

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


def test_exported_json_matches_manual_workflow(
tmp_path, monkeypatch, ewoksorange_qtapp
):
destination_path = tmp_path / "exported_workflow.json"
graph = {
"nodes": [
{
"id": "source",
"task_type": "class",
"task_identifier": "ewokscore.tests.examples.tasks.sumtask.SumTask",
},
{
"id": "sink",
"task_type": "class",
"task_identifier": "ewokscore.tests.examples.tasks.sumtask.SumTask",
},
],
"links": [
{
"source": "source",
"target": "sink",
"data_mapping": [{"source_output": "result", "target_input": "a"}],
}
],
}

source_path = tmp_path / "source_workflow.json"

with open(source_path, "w") as f:
json.dump(graph, f)

# Here i import to the canvas then re export it and verify the exported json.

with OrangeCanvasHandler() as handler:
monkeypatch.setattr(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a fan of monkeypatch but there seem to be no other way

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep me too but that's the better way to bypass the dialog

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)

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"]
Loading