.. _Starting a new project from scratch: Starting a New Project from Scratch =================================== `Orange `_ widgets can be written and linked to Ewoks tasks defined within an Ewoks project. To enable this, the Ewoks project must be set up as an *Orange add-on*. You can easily bootstrap such a project using the `Ewoks cookiecutter template `_. A minimal working example is available `here `_ and is used as an example in the following sections. Python Distribution Layout -------------------------- An Orange add-on project for Ewoks typically contains **two Python packages**: - **Main package** – Implements Ewoks tasks and *Qt* components (e.g. ``ewoks_orange_example_addon``). - **Namespace package** – Provides Orange *Qt* widgets (under ``orangecontrib``, usually with the same subpackage name). When using an `src-layout `_, the project structure typically looks like this: .. code-block:: bash . ├── pyproject.toml ├── README.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── doc │ └── ... └── src ├── ewoks_orange_example_addon │ ├── __init__.py │ ├── tasks │ │ ├── __init__.py │ │ └── exampletask.py │ └── tests │ ├── __init__.py │ ├── conftest.py │ └── test_exampletask.py └── orangecontrib └── ewoks_orange_example_addon ├── __init__.py └── exampletask.py Orange Add-on Metadata ---------------------- The primary dependencies of an Ewoks-Orange project are: - ``ewokscore`` (core Ewoks functionality) - ``ewoksorange`` (Ewoks-Orange integration) Example ``pyproject.toml``: .. code-block:: toml dependencies = [ "ewokscore", "ewoksorange", ] Orange Metadata for GUI Display ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The add-on metadata displayed in the Orange Canvas GUI is defined in: .. code-block:: python # src/orangecontrib/ewoks_orange_example_addon/__init__.py NAME = "Ewoks Orange Example" DESCRIPTION = "An Ewoks project" The ``NAME`` value determines how the add-on appears in the Orange widget panel. Entry Points for Orange Discovery ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Orange uses entry points to auto-discover add-ons and their widgets: .. code-block:: toml [project.entry-points."orange3.addon"] "ewoks_orange_example_addon" = "orangecontrib.ewoks_orange_example_addon" [project.entry-points."orange.widgets"] "Examples" = "orangecontrib.ewoks_orange_example_addon" Optional: Ewoks Task Discovery ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can optionally declare Ewoks task entry points for auto-discovery: .. code-block:: toml [project.entry-points."ewoks.tasks.class"] "ewoks_orange_example_addon.tasks.*" = "ewoks_orange_example_addon" Optional: Distribution Metadata for PyPI ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To make the project discoverable on PyPI, include: .. code-block:: toml [project] name = "ewoks-orange-example-addon" version = "0.0.1-alpha" keywords = [ "orange3 add-on", "ewoks" ] Defining an Ewoks Task ---------------------- An Ewoks task is a standard computational unit without any *Qt* or interactive components. Example: .. code-block:: python # src/ewoks_orange_example_addon/tasks/exampletask.py from ewokscore import Task class ExampleTask(Task, input_names=["number"], output_names=["result"]): """Add +1 to a number""" def run(self): self.outputs.result = self.inputs.number + 1 For more information please see `Task implementation `_ and `Define Task class inputs `_. .. note:: Please consider using `Input models `_ and `Output models` for your tasks. It will allow creating 'typed' Orange Inputs and Outputs as well. Creating an Ewoks-Orange Widget ------------------------------- An Ewoks-Orange widget provides the *Qt* user interface for the corresponding Ewoks task. It typically includes: - Widgets for user-defined Ewoks task inputs - Widgets for displaying results A typical widget could implement the following interactions: - **Load Workflow File → Initialize Display:** Populate widgets with saved Ewoks inputs. - **User Input → Store:** Store user-defined inputs at runtime. - **Upstream Outputs → Update Inputs:** Receive inputs from upstream workflow nodes. - **Execution → Update Display:** Display the Ewoks task outputs after execution. To create a widget, subclass one of the Ewoks-Orange base widget classes, such as ``OWEwoksWidgetOneThread`` — which executes the task in a separate thread to keep the GUI responsive. .. hint:: see :ref:`ewoks widgets and execution` for more details about existing widgets. Use the ``ewokstaskclass`` argument to link the widget to its Ewoks task, and define the ``name`` attribute for the widget panel display. .. warning:: If the ``name`` is not defined, Orange will not be able to process the widget. Example: .. code-block:: python from AnyQt import QtWidgets from ewoksorange.gui.owwidgets.threaded import OWEwoksWidgetOneThread from ewoks_orange_example_addon.tasks.exampletask import ExampleTask class OWExampleTask(OWEwoksWidgetOneThread, ewokstaskclass=ExampleTask): """Orange widget that delegates computation to an Ewoks task. This class manages two types of inputs: - Default inputs: user inputs saved in the workflow file - Dynamic inputs: values received from upstream nodes (not saved) The class implements Qt widgets for displaying task inputs and outputs. """ name = "Example Ewoks Task Widget" description = "Add +1 to a number" def __init__(self) -> None: super().__init__() self._init_control_area() self._init_main_area() def _init_control_area(self) -> None: """Create widgets for user inputs.""" super()._init_control_area() layout = self._get_control_layout() label = QtWidgets.QLabel("Number") self._widgetNumber_value = QtWidgets.QSpinBox() self._widgetNumber_value.editingFinished.connect(self._default_inputs_changed) layout.addWidget(label) layout.addWidget(self._widgetNumber_value) layout.addStretch() def _init_main_area(self) -> None: """Create widgets for task outputs.""" super()._init_main_area() layout = self._get_main_layout() label = QtWidgets.QLabel("Result") self._widgetResult_value = QtWidgets.QLineEdit() self._widgetResult_value.setReadOnly(True) layout.addWidget(label) layout.addWidget(self._widgetResult_value) layout.addStretch() def _initialize_widgets(self) -> None: """Initialize widgets with saved workflow inputs.""" number = self.get_default_input_value("number", None) if number is not None: self._widgetNumber_value.setValue(number) def _default_inputs_changed(self) -> None: """Handle user input changes.""" self.update_default_inputs(number=self._widgetNumber_value.value()) def handleNewSignals(self) -> None: """Handle upstream outputs.""" number = self.get_dynamic_input_value("number", None) if number is not None: self._widgetNumber_value.setValue(number) self._widgetNumber_value.setReadOnly(True) else: self._widgetNumber_value.setReadOnly(False) super().handleNewSignals() def task_output_changed(self) -> None: """Display Ewoks task outputs.""" result = self.get_task_output_value("result", None) if result is not None: self._widgetResult_value.setText(str(result)) super().task_output_changed() .. note:: ``super()._init_control_area`` adds two execution buttons: - **Execute:** runs only the current node (``execute_ewoks_task_without_propagation``) - **Trigger:** runs the current node and all downstream nodes (``execute_ewoks_task``) Accessing and Modifying Widget Input Values ------------------------------------------- Each Ewoks-Orange widget provides instance methods to manage **input values**, grouped into three categories depending on their origin and usage. Default Input Values ~~~~~~~~~~~~~~~~~~~~ Default inputs are user-defined values that are **saved in the workflow file**. .. list-table:: :header-rows: 1 :widths: 40 60 * - Method - Description * - ``set_default_input(name, value)`` - Set a single default input value. * - ``update_default_inputs(**inputs)`` - Update one or more default input values at once. * - ``get_default_input_value(name, default=None)`` - Retrieve a single default input value. * - ``get_default_input_values()`` - Return all inputs that have default values. Dynamic Input Values ~~~~~~~~~~~~~~~~~~~~ Dynamic inputs are values received from **upstream workflow nodes** during execution. .. list-table:: :header-rows: 1 :widths: 40 60 * - Method - Description * - ``set_dynamic_input(name, value)`` - Set a single dynamic input value. * - ``update_dynamic_inputs(**inputs)`` - Update one or more dynamic input values at once. * - ``get_dynamic_input_value(name, default=None)`` - Retrieve a single dynamic input value. * - ``get_dynamic_input_values()`` - Return all inputs that have dynamic values. Task Input Values ~~~~~~~~~~~~~~~~~ Task inputs are the values used when executing the **underlying Ewoks task**. They are resolved according to the following precedence: 1. Dynamic input (if present) 2. Default input (if defined) 3. Explicit fallback argument (if provided) .. list-table:: :header-rows: 1 :widths: 40 60 * - Method - Description * - ``get_task_input_value(name, default=None)`` - Retrieve a single task input value, preferring dynamic values if present. * - ``get_task_input_values()`` - Return all task input values (dynamic when present, default otherwise). Notes ~~~~~ - Use *default inputs* for values configured by the user and stored in the workflow file. - Use *dynamic inputs* for runtime values propagated from upstream tasks. - *Task input* getters automatically combine both, and are typically used when instantiating an Ewoks ``Task``. Ewoks-Orange Execution Call Stack --------------------------------- When the user executes an Ewoks-Orange widget from the canvas, the following sequence occurs: .. mermaid:: sequenceDiagram participant User participant Widget participant Task as Ewoks Task participant Channel as Output Channel(s) participant SignalManager participant DownstreamWidget as Downstream Widget(s) User->>Widget: Click "Trigger" Widget->>Task: execute_ewoks_task() Task->>Widget: propagate_downstream() loop For each output channel Widget->>Channel: send() Channel->>SignalManager: send() end Task->>Widget: 🔧 task_output_changed() 🔧 loop For each connected output channel SignalManager->>DownstreamWidget: set_dynamic_input(output_name, value) end SignalManager->>DownstreamWidget: 🔧 handleNewSignals() 🔧 Note over SignalManager,DownstreamWidget: One handleNewSignals() call per downstream widget Note over Widget,Task: Concurrent Task Execution 🔧 : to be implemented like in the ``OWExampleTask`` widget above.