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 menu_specs(self) -> List[ToolMenuSpec]: """ Get menu specifications for this tool. Returns a list of menu specifications that define how this tool should appear in menus. By default, returns an empty list. :return: List of menu specifications :rtype: List[ToolMenuSpec] """ return []
[docs] def sync_menu_state(self) -> None: """ Emit current action states for menu enablement. This is useful when menus are built on demand (e.g., context menus) so actions start in the correct enabled/disabled state. """ descriptors: dict[str, ActionDescriptor] = {} for cls in type(self).mro(): for name, attr in cls.__dict__.items(): if isinstance(attr, ActionDescriptor): descriptors[name] = attr for name, descriptor in descriptors.items(): value = bool(getattr(self, name)) signal = getattr(self, descriptor.signal_name, None) if signal is not None: signal.emit(value)
[docs] def sync_menu_state_for(self, window_controller: 'SubWindowController[Any]') -> None: """ Sync menu state for a specific window controller. Subclasses can override this to emit additional state signals that depend on the active window (e.g., distance-calibration availability). By default this defers to :meth:`sync_menu_state`. :param window_controller: Active window controller :type window_controller: SubWindowController[Any] """ if window_controller is None: return if not window_controller: return if not compare_flags(window_controller.sub_window_type, self.enable_for_window_type): descriptors: dict[str, ActionDescriptor] = {} for cls in type(self).mro(): for name, attr in cls.__dict__.items(): if isinstance(attr, ActionDescriptor): descriptors[name] = attr for descriptor in descriptors.values(): signal = getattr(self, descriptor.signal_name, None) if signal is not None: signal.emit(False) return self.sync_menu_state()
[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