Source code for radioviz.tools.base_controller
# Copyright 2025–2026 European Union
# Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
# SPDX-License-Identifier: EUPL-1.2
"""
Base controller module for radioviz tools.
This module provides the foundational controller classes that manage the interaction
between tools and their graphical interfaces. It defines the base controller
abstraction that all tool controllers must implement, along with the necessary
infrastructure for managing tool sessions, docks, and event handling.
The primary class in this module is :class:`ToolController`, which serves as the
base class for all tool controllers in the application. It handles the lifecycle
of tool sessions, manages dock widgets, and provides mechanisms for communication
between tools and their views.
"""
from __future__ import annotations
from abc import abstractmethod
from typing import TYPE_CHECKING, Any, Generic, List, Optional, TypeVar, Union, cast
from PySide6.QtCore import QObject, Signal
from radioviz.controllers.sub_window_controller import SubWindowEnum, T_SubWindowController, compare_flags
from radioviz.models.menu_spec import ToolMenuSpec
from radioviz.services.action_descriptor import ActionDescriptor
from radioviz.tools.base_tool import ToolEvent
from radioviz.tools.tool_api import T_Session, T_Tool_Controller, ToolContext
if TYPE_CHECKING:
from PySide6.QtGui import QAction
from PySide6.QtWidgets import QWidget
from radioviz.controllers.sub_window_controller import SubWindowController
from radioviz.models.mouse_state import MouseEvent
from radioviz.tools.base_dock import ToolDockWidget
from radioviz.tools.base_tool import Tool
T_ToolControllerSelf = TypeVar('T_ToolControllerSelf', bound='ToolController[Any, Any]')
T_ToolControllerSelf.__doc__ = 'Concrete tool controller type for dock return typing.'
[docs]
class ToolController(QObject, Generic[T_SubWindowController, T_Session]):
"""
Base controller class for radioviz tools.
This abstract base class defines the interface and common behavior for all
tool controllers in the radioviz application. It manages the tool's session
lifecycle, handles communication between the tool and its UI components,
and provides infrastructure for dock widgets and event handling.
:ivar dock_event: Signal emitted when a dock event occurs
:vartype dock_event: Signal
:ivar controller_event: Signal emitted when a controller event occurs
:vartype controller_event: Signal
:ivar request_new_window: Signal emitted when a new window is requested
:vartype request_new_window: Signal
:ivar enable_for_window_type: Window types where this tool's menus are available
:vartype enable_for_window_type: SubWindowEnum
"""
dock_event = Signal(object) # inbound from dock
controller_event = Signal(object) # outbound to dock
request_new_window = Signal(object) # emits a WindowRequest
enable_for_window_type = SubWindowEnum.ImageWindow
def __init__(self, tool_ctx: 'ToolContext', tool: 'Tool[T_Tool_Controller]') -> None:
"""
Initialize the ToolController.
:param tool_ctx: The tool context containing application state
:type tool_ctx: ToolContext
:param tool: The tool instance associated with this controller
:type tool: Tool
"""
super().__init__()
self.context = tool_ctx
self.context.on_active_image_changed(self._on_active_image_changed)
self.active_session: Optional[T_Session] = None
self.tool = tool
self.dock: Optional['ToolDockWidget[Any]'] = None # optional
self.has_dock = False
self._dock_visibility_action: Optional['QAction'] = None
[docs]
def _on_active_image_changed(self, new_window_controller: 'SubWindowController[Any] | None') -> None:
"""
Handle active image change event.
This internal method is called when the active image window changes.
If there's an active session and it belongs to a different window,
the session will be deactivated.
:param new_window_controller: The new active window controller
:type new_window_controller: SubWindowController[Any] | None
"""
if self.active_session and new_window_controller is not self.active_session.window_controller:
self.deactivate()
[docs]
def activate(self) -> None:
"""
Activate the tool.
This method activates the tool by creating a new session for the currently
active image window. If there's no active image, the activation is skipped.
"""
window_controller = cast(T_SubWindowController, self.context.active_image())
if window_controller is None:
return
self.context.request_tool_activation(self)
self.active_session = self.create_session(window_controller)
window_controller.active_tool = self
self.active_session.start()
[docs]
def deactivate(self) -> None:
"""
Deactivate the tool.
This method cancels the active session if one exists, effectively
deactivating the tool. The session is properly cleaned up during this process.
"""
if self.active_session:
window_controller = self.active_session.window_controller
self.active_session.cancel('tool deactivation')
self.active_session = None
if window_controller is not None and getattr(window_controller, 'active_tool', None) is self:
window_controller.active_tool = None
self.context.clear_active_tool(self)
[docs]
@abstractmethod
def create_session(self, window_controller: 'T_SubWindowController') -> T_Session:
"""
Create a new tool session for the given window controller.
This abstract method must be implemented by subclasses to create
appropriate tool sessions for specific tools.
:param window_controller: The window controller to create session for
:type window_controller: SubWindowController[Any]
:return: A new tool session instance
:rtype: BaseToolSession
"""
@property
def tool_id(self) -> str:
"""
Get the unique identifier for this tool.
:return: The tool identifier
:rtype: str
"""
return self.tool.tool_id
# Called once the view is available
[docs]
def attach_view(self, view: 'QWidget') -> None:
"""
Attach a view to this controller.
This method is called when the view becomes available to associate
it with the controller.
:param view: The view to attach
:type view: object
"""
self.view = view
# Called once the main window is available to create the dock
[docs]
def create_dock(
self: T_ToolControllerSelf, parent_window: 'QWidget'
) -> Union['ToolDockWidget[T_ToolControllerSelf]', None]:
"""
Create a dock widget for this tool.
Override this method in subclasses to provide custom dock widget creation
functionality. By default, returns None indicating no dock widget is created.
:param parent_window: The parent window for the dock widget
:type parent_window: QWidget
:return: A new dock widget instance or None
:rtype: ToolDockWidget or None
"""
return None
[docs]
def toggle_dock_visibility(self, visible: bool) -> None:
"""
Toggle the visibility of the dock widget.
:param visible: Whether to make the dock visible
:type visible: bool
"""
if self.dock:
self.dock.setVisible(visible)
# ---- Mouse events from image view -----------------------------------
[docs]
def on_mouse_press(self, event: MouseEvent) -> None:
"""
Handle mouse press events from the image view.
This method is called when a mouse press event occurs in the image view.
Subclasses can override this to handle specific mouse interactions.
:param event: The mouse press event
:type event: MouseEvent
"""
[docs]
def on_mouse_move(self, event: MouseEvent) -> None:
"""
Handle mouse move events from the image view.
This method is called when a mouse move event occurs in the image view.
Subclasses can override this to handle specific mouse interactions.
:param event: The mouse move event
:type event: MouseEvent
"""
# ---- Optional lifecycle hooks --------------------------------------
[docs]
def on_activate(self) -> None:
"""
Called when the tool becomes active.
This method is invoked when the tool transitions to an active state.
Subclasses can override this to perform initialization or setup tasks
when the tool becomes active.
"""
[docs]
def on_deactivate(self) -> None:
"""
Called when the tool becomes inactive.
This method is invoked when the tool transitions to an inactive state.
Subclasses can override this to perform cleanup or teardown tasks
when the tool becomes inactive.
"""
[docs]
def cleanup(self) -> None:
"""
Clean up resources when the image window closes.
This method is called when the associated image window is closed.
Subclasses can override this to perform final cleanup operations.
"""
[docs]
def on_dock_event(self, event: ToolEvent) -> None:
"""
Handle dock events.
This method processes events coming from the dock widget. Subclasses
can override this to handle custom dock-related events.
:param event: The dock event to process
:type event: ToolEvent
"""
[docs]
def emit_event(self, event: str, payload: Optional[Any] = None) -> None:
"""
Emit a controller event.
This method emits a controller event that can be listened to by other
components in the application.
:param event: The event name
:type event: str
:param payload: Optional event payload data
:type payload: Any
"""
self.controller_event.emit(ToolEvent(self.tool_id, event, payload))
[docs]
def count_dependencies_for_image(self, window_controller: 'SubWindowController[Any]') -> int:
"""
Count items managed by this tool that depend on a given image.
Subclasses can override this to expose in-place mutation impact.
:param window_controller: Image controller used as dependency root
:type window_controller: SubWindowController[Any]
:return: Number of dependent items
:rtype: int
"""
return 0
[docs]
def invalidate_dependencies_for_image(self, window_controller: 'SubWindowController[Any]') -> int:
"""
Invalidate items managed by this tool that depend on a given image.
Subclasses can override this to safely remove stale dependent items when
in-place image data changes make them invalid.
:param window_controller: Image controller used as dependency root
:type window_controller: SubWindowController[Any]
:return: Number of invalidated items
:rtype: int
"""
return 0