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 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 Tool subclass or a factory that returns a 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 tool_id.

Tool contract

Tools follow a small, consistent contract. The core classes are:

The typical structure is:

Tool API and interaction

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.

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 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 ToolController.menu_specs returning ToolMenuSpec and ToolActionSpec objects. The main window renders these entries into the Tools (context) menu. Tools can also contribute nested menu categories by returning nested ToolMenuSpec objects; menus with the same title are merged.

Ordering can be controlled by setting order on ToolMenuSpec and 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 windows_to_be_registered with WindowSpec objects.

When a tool needs to open a new window, it emits a WindowRequest from its controller. The main controller handles the request and delegates creation to the window factory WindowFactoryRegistry.

Example from existing tools

The Profile tool is a good reference implementation:

Look at:

Implementation checklist

  • Define a Tool subclass with a unique Tool.tool_id.

  • Provide a ToolController with sessions and optional dock UI.

  • Register overlays and window specs as needed.

  • Add menu actions through menu_specs().

  • If external, expose the tool via the radioviz.tools entry-point group.

  • If internal (proposed as merge request), extend the _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:

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:

[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.

Lenient typing (allowed for plugins)

If strict typing is inconvenient, you can still ship a plugin as long as it returns a valid Tool instance and implements the expected runtime interface. RadioViz does not enforce runtime type checks for plugins.