Extending RadioViz ================== RadioViz is designed to be extended with new tools. There are two supported paths: * Built-in tools contributed via merge requests to the main repository. * External plugins distributed as separate Python packages and discovered through entry points. Built-in vs external tools -------------------------- Built-in tools live in the :mod:`.tools` package and are loaded automatically at startup. If you want your tool to ship with RadioViz, submit it as a merge request with the new tool module and any supporting UI or model changes. External tools can be shipped as independent packages. RadioViz discovers them via the ``radioviz.tools`` entry-point group. Your package should declare an entry point that returns a :class:`.Tool` subclass or a factory that returns a :class:`.Tool` instance. Example ``pyproject.toml`` for a plugin package:: [project.entry-points."radioviz.tools"] my_tool = "my_radioviz_plugin.my_tool:MyTool" The entry-point name should match your tool's :attr:`~.Tool.tool_id`. Tool contract ------------- Tools follow a small, consistent contract. The core classes are: * :class:`~radioviz.tools.base_tool.Tool` for metadata and registration. * :class:`~radioviz.tools.base_controller.ToolController` for runtime behavior. * :class:`~radioviz.tools.base_dock.ToolDockWidget` for dock UI. * :class:`~radioviz.tools.tool_api.BaseToolSession` for per-image sessions. * :class:`~radioviz.tools.tool_api.ToolContext` for access to app state. The typical structure is: * :class:`~radioviz.tools.base_tool.Tool` owns tool metadata and registers overlays and windows. * :meth:`~radioviz.tools.base_tool.Tool.create_controller` constructs a :class:`.ToolController`. * :meth:`.ToolController.create_session` creates a :class:`.BaseToolSession` for the active window. * :meth:`.ToolController.create_dock` creates a :class:`.ToolDockWidget` if the tool needs a dock UI. Tool API and interaction ------------------------ :class:`.ToolContext` provides access to application state and the active image controller. Tools use it to react to active window changes and to request messages in the UI. :class:`.BaseToolSession` defines the lifecycle for operations tied to a single window. Sessions start when a tool is activated and end when the tool is deactivated or the active window changes. Sessions allow to have an isolated *sandbox* where you can perform all your calculations without interfering with the underlying data model. When the session is finished, a :class:`.ToolSessionResult` is returned containing the results of the session so that it can be included in the data model. Menu entries are provided by the controller via :attr:`.ToolController.menu_specs` returning :class:`.ToolMenuSpec` and :class:`.ToolActionSpec` objects. The main window renders these entries into the ``Tools`` (context) menu. Tools can also contribute nested menu categories by returning nested :class:`.ToolMenuSpec` objects; menus with the same title are merged. Ordering can be controlled by setting ``order`` on :class:`.ToolMenuSpec` and :class:`.ToolActionSpec`. Lower values appear first. When any entry uses ``order`` at a given menu level, items without an explicit order are placed after the ordered items and ties fall back to alphabetical order. Creating new windows -------------------- Some tools need to open a new sub-window. These windows are created through the window factory. A tool can register new window types using :attr:`~.Tool.windows_to_be_registered` with :class:`~radioviz.services.window_factory.WindowSpec` objects. When a tool needs to open a new window, it emits a :class:`.WindowRequest` from its controller. The main controller handles the request and delegates creation to the window factory :class:`.WindowFactoryRegistry`. Example from existing tools --------------------------- The Profile tool is a good reference implementation: * Tool class: :class:`~.radioviz.tools.profile_tool.ProfileTool` * Controller: :class:`~.radioviz.tools.profile_tool.ProfileToolController` * Dock widget: :class:`~.radioviz.tools.profile_tool.ProfileToolDock` * Sessions: :class:`~.radioviz.tools.profile_tool.ProfileToolSession` Look at: * :meth:`~.radioviz.tools.profile_tool.ProfileToolController.menu_specs` for how menu actions are exposed. * :attr:`~.radioviz.tools.profile_tool.ProfileTool.windows_to_be_registered` and :attr:`~.radioviz.tools.profile_tool.ProfileTool.overlays_to_be_registered` for window and overlay registration. * :meth:`~.radioviz.tools.profile_tool.ProfileToolController.create_session` for session creation. * :meth:`~.radioviz.tools.profile_tool.ProfileToolController.create_dock` for dock creation and wiring. Implementation checklist ------------------------ * Define a :class:`.Tool` subclass with a unique :attr:`.Tool.tool_id`. * Provide a :class:`.ToolController` with sessions and optional dock UI. * Register overlays and window specs as needed. * Add menu actions through :meth:`~.ToolController.menu_specs`. * If external, expose the tool via the ``radioviz.tools`` entry-point group. * If internal (proposed as merge request), extend the :data:`._BUILTIN_TOOL_PATHS` list with your tool. Quickstart: a minimal tool -------------------------- Below is a minimal tool that adds a menu entry and a dock. It omits overlays and sessions for brevity. Tool module: .. code-block:: python from PySide6.QtWidgets import QLabel from radioviz.models.menu_spec import ToolActionSpec, ToolMenuSpec from radioviz.tools.base_controller import ToolController from radioviz.tools.base_dock import ToolDockWidget from radioviz.tools.base_tool import Tool from radioviz.tools.tool_api import BaseToolSession class HelloSession(BaseToolSession): def start(self): pass def cancel(self, reason: str): pass class HelloDock(ToolDockWidget): dock_position = ToolDockWidget.DockWidgetArea.RightDockWidgetArea initial_visibility = True def __init__(self, parent, controller): super().__init__('Hello', parent, controller) self.setWidget(QLabel('Hello from a plugin!')) class HelloController(ToolController): def create_session(self, window_controller): return HelloSession(window_controller, self) def create_dock(self, parent_window): return HelloDock(parent_window, self) def menu_specs(self): return [ ToolMenuSpec( title='Analysis', order=10, entries=[ ToolMenuSpec( title='Hello', order=10, entries=[ ToolActionSpec( text='Say Hello', triggered=self._say_hello, ), ], ) ], ) ] def _say_hello(self): self.context.request_message('Hello from RadioViz!', level='info') class HelloTool(Tool): tool_id = 'hello' name = 'Hello Tool' description = 'Minimal example tool' def create_controller(self, ctx): self._controller = HelloController(ctx, self) return self._controller External plugin entry point: .. code-block:: toml [project.entry-points."radioviz.tools"] hello = "my_radioviz_plugin.hello:HelloTool" Plugin author typing guide -------------------------- RadioViz accepts plugin tools that are either strictly typed or loosely typed. Built-in tools are expected to use strict typing. External plugins may choose their own level of strictness. Recommended strict typing (plugin authors) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ If you want full static typing, follow the generic pattern used in built-in tools. This keeps the relationship between Tool → ToolController → BaseToolSession fully typed. Minimal example with strict typing:: from __future__ import annotations from radioviz.controllers.image_window_controller import ImageWindowController from radioviz.tools.base_controller import ToolController from radioviz.tools.base_tool import Tool from radioviz.tools.tool_api import BaseToolSession, ToolContext class HelloSession(BaseToolSession["HelloController", ImageWindowController]): def on_start(self) -> None: pass def on_cancel(self, reason: str) -> None: pass def on_finish(self) -> None: pass class HelloController(ToolController[ImageWindowController, HelloSession]): def create_session(self, window_controller: ImageWindowController) -> HelloSession: return HelloSession(self, window_controller) class HelloTool(Tool[HelloController]): tool_id = "hello" name = "Hello Tool" description = "Minimal strictly typed tool" def create_controller(self, ctx: ToolContext) -> HelloController: self._controller = HelloController(ctx, self) return self._controller Lenient typing (allowed for plugins) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ If strict typing is inconvenient, you can still ship a plugin as long as it returns a valid :class:`~radioviz.tools.base_tool.Tool` instance and implements the expected runtime interface. RadioViz does not enforce runtime type checks for plugins.