Source code for radioviz.views.main_window

#  Copyright 2025–2026 European Union
#  Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
#  SPDX-License-Identifier: EUPL-1.2
"""
Main window implementation for the RadioViz application.

This module contains the main window class that serves as the central hub
for the application's GUI, managing sub-windows, menus, tool docks, and
overall application layout. It handles file operations, window management,
and integrates with the application's controller layer.
"""

from __future__ import annotations

import subprocess
import sys
from functools import partial
from pathlib import Path
from typing import TYPE_CHECKING, Any, Optional, cast

from PySide6.QtCore import QEvent, QPoint, QSettings, QUrl, Signal
from PySide6.QtGui import QAction, QCloseEvent, QDesktopServices, QDropEvent, QIcon, QKeySequence, Qt
from PySide6.QtWidgets import (
    QApplication,
    QDialog,
    QDockWidget,
    QFileDialog,
    QMainWindow,
    QMdiArea,
    QMenu,
    QMenuBar,
    QMessageBox,
    QProgressDialog,
)
from superqt import QIconifyIcon

from radioviz.__about__ import __doc_url__
from radioviz.controllers.image_window_controller import ImageWindowController
from radioviz.controllers.main_controller import MainController
from radioviz.controllers.sub_window_controller import SubWindowController, SubWindowEnum, compare_flags
from radioviz.services.application_services import ApplicationServices
from radioviz.services.export_services import Exportable
from radioviz.services.menu_builder import build_context_menu, build_tool_menus
from radioviz.services.save_services import Savable
from radioviz.services.window_manager import WindowRequest
from radioviz.services.workspace_manager import (
    SerializerType,
)
from radioviz.views.about_dialog import AboutDialog
from radioviz.views.sub_window import SubWindow
from radioviz.views.unsaved_images_dialog import UnsavedImageItem, UnsavedImagesDialog


[docs] class MainWindow(QMainWindow): """ Main application window for RadioViz. This class implements the primary window of the RadioViz application, handling the main GUI components including MDI area for sub-windows, menu bars, tool docks, and overall application layout management. :ivar controller: The main application controller instance :vartype controller: MainController """ nonpersistent_images_selected = Signal(bool, list) def __init__( self, main_controller: Optional[MainController] = None, ) -> None: """ Initialize the main window. Creates the main application window with all required UI components including menus, tool docks, and MDI area for sub-windows. :param main_controller: Optional main controller instance, defaults to None :type main_controller: Optional[MainController] """ super().__init__() self.controller = main_controller or MainController() self.application_services = ApplicationServices() self.setWindowTitle('RadioViz') icon_path = Path(__file__).parent.parent / 'resources' / 'radioviz-logo-trasp.ico' if icon_path.exists(): self.setWindowIcon(QIcon(str(icon_path))) self.resize(1200, 800) self.mdi = QMdiArea() self.mdi.setAcceptDrops(True) self.mdi.installEventFilter(self) self.mdi.subWindowActivated.connect(self.on_active_window_changed) self.setCentralWidget(self.mdi) # Wire controller signals self.controller.image_load_failed.connect(self._on_load_failed) self.controller.context_menu_requested.connect(self.show_context_menu) self.controller.request_new_window_view.connect(self.create_subwindow_view) self.controller.image_loader.progress.connect(self._on_load_progress) self.controller.windows_manager.request_update_app_state.connect(self.controller.app_state.on_window_change) self.controller.app_state.changed.connect(self._synch_ui_state) self.controller.recent_files.changed.connect(self._rebuild_recent_menu) self.controller.windows_manager.active_changed.connect(self._rebuild_window_list) self.controller.windows_manager.active_changed.connect(self.activate_window) self.controller.windows_manager.list_changed.connect(self._rebuild_window_list) self.controller.tool_context.message_requested.connect(self._show_message) self.controller.message_requested.connect(self._show_message) self.controller.request_nonpersistent_image_selection.connect(self._on_nonpersistent_image_selection_requested) self.controller.request_xyz_metadata.connect(self._on_xyz_metadata_requested) self.nonpersistent_images_selected.connect(self.controller.on_nonpersistent_image_selection) self._create_menus() self._tool_global_docks() self._init_default_layout() self.restore_layout() # a dictionary of progress dialogs self._progress_dialogs: dict[Path, QProgressDialog] = {} # the last thing is to manually synch the ui status for the first (and last time) self._synch_ui_state() self.statusBar().showMessage('Welcome to RadioViz', timeout=3000)
[docs] def _create_menus(self) -> None: """ Create all application menus. Initializes and sets up all menu bars including File, View, Tools, Window, and Help menus with their respective actions and sub-menus. """ bar = self.menuBar() self._create_file_menu(bar) self._create_view_menu(bar) self._create_tool_menu(bar) self._create_window_menu(bar) self._create_help_menu(bar)
[docs] def _tool_global_docks(self) -> None: """ Create and configure global tool docks. Sets up all tool docks defined in the controller, adding them to the main window with appropriate docking positions and tabification. """ first_dock_per_area: dict[Qt.DockWidgetArea, QDockWidget] = {} for tool_controller in self.controller.tool_controllers: tool_dock = tool_controller.create_dock(self) if not tool_dock: continue # Menu toggle self.tool_dock_view.addAction(tool_dock.toggleViewAction()) area = tool_dock.dock_position if area not in first_dock_per_area: # First dock in this area self.addDockWidget(area, tool_dock) first_dock_per_area[area] = tool_dock else: # Tabify with the first dock in this area self.addDockWidget(area, tool_dock) self.tabifyDockWidget(first_dock_per_area[area], tool_dock) # Ensure the first dock remains the visible one first_dock_per_area[area].raise_() # Register in controller self.controller.register_global_dock(tool_controller.tool.tool_id, tool_dock) # Initial visibility if not tool_dock.initial_visibility: tool_dock.hide()
[docs] def create_subwindow_view(self, window_request: WindowRequest) -> 'SubWindow[Any]': """ Create a new sub-window view. Creates and initializes a new sub-window based on the provided window request. :param window_request: Request specifying the window to create :type window_request: WindowRequest :return: The created sub-window instance :rtype: SubWindow[Any] """ view = cast(SubWindow[Any], self.application_services.window_factory.create(window_request)) view.application_services = self.application_services self.mdi.addSubWindow(view) view.show() view.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True) view.about_to_close.connect(self._on_window_closed) view.about_to_close.connect(view.controller.on_view_closed) view.controller.on_view_created() return view
[docs] def _show_message(self, text: str, level: str, timeout_ms: int = 3000) -> None: """ Display a message in the status bar or dialog. Shows messages in the status bar or as dialog boxes depending on the message level. :param text: Message text to display :type text: str :param level: Message level ('info', 'warning', or 'error') :type level: str :param timeout_ms: Timeout in milliseconds for status bar messages, defaults to 3000 :type timeout_ms: int """ if level == 'info': self.statusBar().showMessage(text, timeout_ms) else: { 'warning': QMessageBox.warning, 'error': QMessageBox.critical, }[level](self, level.capitalize(), text)
[docs] def _on_xyz_metadata_requested(self, payload: dict[str, Any]) -> None: """ Prompt the user to provide a DAT sidecar for a XYZ file. :param payload: Payload containing the XYZ path and expected DAT path. :type payload: dict """ xyz_path = Path(payload['xyz_path']) expected_dat_path = Path(payload['expected_dat_path']) message = ( f'Metadata sidecar not found for:\n{xyz_path}\n\n' f'Expected DAT file:\n{expected_dat_path}\n\n' 'You can select a DAT file, continue without metadata, or cancel.' ) msg_box = QMessageBox(self) msg_box.setWindowTitle('Missing DAT metadata') msg_box.setIcon(QMessageBox.Icon.Warning) msg_box.setText(message) select_button = msg_box.addButton('Select DAT...', QMessageBox.ButtonRole.AcceptRole) continue_button = msg_box.addButton('Continue without metadata', QMessageBox.ButtonRole.DestructiveRole) msg_box.addButton(QMessageBox.StandardButton.Cancel) msg_box.exec() clicked = msg_box.clickedButton() if clicked == select_button: dat_file, _ = QFileDialog.getOpenFileName( self, 'Select DAT file', str(expected_dat_path.parent), 'DAT files (*.dat)', ) if dat_file: self.controller.open_xyz_with_dat(xyz_path, Path(dat_file)) return if clicked == continue_button: self.controller.open_xyz_with_dat(xyz_path, None)
[docs] def _on_nonpersistent_image_selection_requested(self, items: list[dict[str, Any]]) -> None: """ Process a request to select non‑persistent images. The method builds a list of :class:`UnsavedImageItem` objects from the supplied ``items`` dictionaries, presents them in an :class:`UnsavedImagesDialog`, and reacts to the user's choice: * If the dialog is cancelled, the :py:attr:`nonpersistent_images_selected` signal is emitted with ``False`` and an empty exclusion list. * If the user confirms, the method ensures that each selected image has a saved file (prompting a *SaveAs* operation when necessary). Should any save operation fail, the signal is emitted with ``False``. * Finally, it emits the signal with ``True`` and a list of image IDs that the user chose to exclude. :param items: List of dictionaries describing the images. Each dictionary must contain an ``'image_id'`` key and may optionally include ``'name'`` and ``'dependency_counts'`` entries. :type items: list[dict] :return: ``None`` – the outcome is communicated via the ``nonpersistent_images_selected`` signal. :rtype: None """ dialog_items: list[UnsavedImageItem] = [] for item in items: image_id = item['image_id'] name = item.get('name', 'unnamed') dependency_counts = item.get('dependency_counts', {}) dependency_text = self._format_dependency_text(dependency_counts) dialog_items.append(UnsavedImageItem(image_id=image_id, name=name, dependency_text=dependency_text)) dialog = UnsavedImagesDialog(dialog_items, parent=self) if dialog.exec() != QDialog.DialogCode.Accepted: self.nonpersistent_images_selected.emit(False, []) return selected_ids = set(dialog.checked_ids()) all_ids = {item.image_id for item in dialog_items} for image_id in selected_ids: controller = self.controller.windows_manager.get_windows_by_id(image_id) if controller is None: self.nonpersistent_images_selected.emit(False, []) return if controller.storage.path is None: if not isinstance(controller, Savable): self.nonpersistent_images_selected.emit(False, []) return self.application_services.save_coordinator.save_as(controller, self) if controller.storage.path is None: self.nonpersistent_images_selected.emit(False, []) return excluded_ids = list(all_ids - selected_ids) self.nonpersistent_images_selected.emit(True, excluded_ids)
[docs] def _format_dependency_text(self, counts: dict[str, int]) -> str: """ Create a human‑readable description of tool dependencies. The function receives a mapping where keys are tool identifiers and values are the number of items that depend on the image. It returns a concise string summarising the dependencies, or a default message when no dependencies are present. :param counts: Mapping of tool IDs to dependency counts. :type counts: dict :return: Description of the dependencies, e.g. ``'Excluding will also remove: 2 toolA item(s), 1 toolB item(s)'``. Returns ``'No dependent tools'`` when the mapping is empty or all counts are zero. :rtype: str """ if not counts: return 'No dependent tools' parts = [] for tool_id in sorted(counts.keys()): count = counts[tool_id] if count: parts.append(f'{count} {tool_id} item(s)') if not parts: return 'No dependent tools' return 'Excluding will also remove: ' + ', '.join(parts)
[docs] def _create_file_menu(self, bar: QMenuBar) -> None: """ Create the File menu. Sets up the File menu with actions for opening files, viewing recent files, and exiting the application. :param bar: The menu bar to add the menu to :type bar: QMenuBar """ # add file menu self.file_menu = bar.addMenu('&File') # open open_action = self.file_menu.addAction('Open...') open_action.setIcon(QIconifyIcon('material-symbols-light:file-open-outline')) open_action.setShortcut(QKeySequence.StandardKey.Open) open_action.triggered.connect(self._open_file_dialog) # recent files self.recent_menu = QMenu('Open &Recent', self) self.file_menu.addMenu(self.recent_menu) self._rebuild_recent_menu() self.close_action = self.file_menu.addAction('&Close') self.close_action.triggered.connect(self._close_active_window) self.close_all_action = self.file_menu.addAction('Close &all') self.close_all_action.triggered.connect(self._close_all_windows) self.file_menu.addSeparator() self.load_workspace_action = self.file_menu.addAction('&Load workspace...') self.load_workspace_action.triggered.connect(self.load_workspace) self.save_workspace_action = self.file_menu.addAction('Save &workspace...') self.save_workspace_action.triggered.connect(self.save_workspace) self.file_menu.addSeparator() self.file_save = self.file_menu.addAction('&Save') self.file_save.triggered.connect(self.save) self.file_save.setShortcut(QKeySequence.StandardKey.Save) self.file_save.setIcon(QIconifyIcon('material-symbols-light:save-outline')) self.file_save_as = self.file_menu.addAction('Save &As...') self.file_save_as.setShortcut(QKeySequence.StandardKey.SaveAs) self.file_save_as.setIcon(QIconifyIcon('material-symbols-light:save-as-outline')) self.file_save_as.triggered.connect(self.save_as) self.export_action = self.file_menu.addAction('&Export...') self.export_action.triggered.connect(self.export) self.file_menu.addSeparator() # exit exit_action = QAction('E&xit', self) exit_action.setShortcut(QKeySequence.StandardKey.Quit) exit_action.setIcon(QIcon.fromTheme('application-exit')) exit_action.setMenuRole(QAction.MenuRole.QuitRole) exit_action.triggered.connect(self.close) self.file_menu.addAction(exit_action)
[docs] def _create_view_menu(self, bar: QMenuBar) -> None: """ Create the View menu. Sets up the View menu with options for tool view management and layout restoration. :param bar: The menu bar to add the menu to :type bar: QMenuBar """ self.view_menu = bar.addMenu('&View') self.tool_dock_view = QMenu('&Tool View', self) self.view_menu.addMenu(self.tool_dock_view) self.view_menu.addSeparator() self.view_image_metadata_action = QAction('Display image metadata', self) self.view_image_metadata_action.triggered.connect(self.show_image_metadata) self.view_image_metadata_action.setShortcut(QKeySequence('Ctrl + I')) self.view_menu.addAction(self.view_image_metadata_action) restore_default_layout_action = QAction('Restore default layout', self) restore_default_layout_action.triggered.connect(self.restore_default_layout) self.view_menu.addSeparator() self.view_menu.addAction(restore_default_layout_action)
[docs] def _create_tool_menu(self, bar: QMenuBar) -> None: """ Create the Tools menu. Sets up the Tools menu with dynamically built tool menus based on available tools. :param bar: The menu bar to add the menu to :type bar: QMenuBar """ self.tool_menu = bar.addMenu('&Tools') specs = self.controller.tool_menu_specs() menus = build_tool_menus(self, specs) for menu in menus: self.tool_menu.addMenu(menu)
[docs] def show_context_menu(self, pos: QPoint, window_controller: SubWindowController[Any]) -> None: """ Show the context menu. Displays a context menu built from tool menus compatible with the active window plus any window-specific menus. :param pos: Position where to show the menu :type pos: QPoint :param window_controller: The window controller from where the context menu is shown :type window_controller: SubWindowController[Any] """ specs = self.controller.context_menu_specs_for(window_controller) menu = build_context_menu(self, specs) self.controller.sync_tool_menu_state(window_controller) if window_controller is not None: window_controller.sync_context_menu_state() menu.exec(pos)
[docs] def _create_window_menu(self, bar: QMenuBar) -> None: """ Create the Window menu. Sets up the Window menu with actions for window management including cascading, tiling, and switching between windows. :param bar: The menu bar to add the menu to :type bar: QMenuBar """ self.window_menu = bar.addMenu('&Window') self.cascade_action = QAction('Cascade Windows', self) self.cascade_action.triggered.connect(self.mdi.cascadeSubWindows) self.window_menu.addAction(self.cascade_action) self.tile_action = QAction('Tile Windows', self) self.tile_action.triggered.connect(self.mdi.tileSubWindows) self.window_menu.addAction(self.tile_action) self.window_menu.addSeparator() self.next_window_action = QAction('Ne&xt Window', self) self.next_window_action.setShortcut('Ctrl+Tab') self.next_window_action.triggered.connect(self.activate_next_window) self.window_menu.addAction(self.next_window_action) self.previous_window_action = QAction('&Previous Window', self) self.previous_window_action.setShortcut('Shift+Ctrl+Tab') self.previous_window_action.triggered.connect(self.activate_previous_window) self.window_menu.addAction(self.previous_window_action) self.window_menu.addSeparator() self._rebuild_window_list()
[docs] def _create_help_menu(self, bar: QMenuBar) -> None: """ Create the Help menu. Sets up the Help menu with About and Qt information actions. :param bar: The menu bar to add the menu to :type bar: QMenuBar """ # Add Help menu help_menu = bar.addMenu('&Help') online_help_action = QAction('Online &help', self) online_help_action.setStatusTip('Open the online documentation in a web browser') online_help_action.setIcon(QIconifyIcon('material-symbols-light:help-outline')) online_help_action.setShortcut(QKeySequence.StandardKey.HelpContents) online_help_action.triggered.connect(self._open_online_help) help_menu.addAction(online_help_action) help_menu.addSeparator() action = QAction('About &Qt', self) action.setStatusTip("Show the Qt library's version and copyright") action.triggered.connect(QApplication.aboutQt) help_menu.addAction(action) about_action = QAction('&About RadioViz', self) about_action.setMenuRole(QAction.MenuRole.AboutRole) about_action.triggered.connect(self.show_about_dialog) help_menu.addAction(about_action)
[docs] @staticmethod def _open_online_help() -> None: """ Open the online documentation in a web browser. Uses subprocess on Linux to suppress console output from browser sessions. """ if sys.platform == 'linux': try: subprocess.Popen( ['xdg-open', __doc_url__], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, close_fds=True ) return except Exception: pass QDesktopServices.openUrl(QUrl(__doc_url__))
[docs] def _open_file_dialog(self) -> None: """ Open file dialog for selecting TIFF files. Displays a file dialog to select one or more TIFF and XYZ files for loading into the application. """ paths, _ = QFileDialog.getOpenFileNames( self, 'Open Image', '', 'Compatible files (*.tif *.tiff *.xyz);;TIFF Files (*.tif *.tiff);;XYZ Files (*.xyz)', ) for p in paths: self.controller.open_image(Path(p))
[docs] def _on_load_progress(self, path: Path, progress: int) -> None: """ Handle image loading progress updates. Updates the progress dialog when loading images. :param path: Path of the file being loaded :type path: Path :param progress: Current loading progress percentage :type progress: int """ if path not in self._progress_dialogs: dlg = QProgressDialog(f'Loading file {path.name}...', 'Cancel', 0, 100, self) dlg.setWindowTitle('Opening File') dlg.setAutoClose(True) dlg.setAutoReset(True) dlg.canceled.connect(partial(self._on_cancelled_loading, path)) self._progress_dialogs[path] = dlg self._progress_dialogs[path].setValue(progress)
[docs] def _on_cancelled_loading(self, path: Path) -> None: """ Handle cancelled image loading. Cancels the image loading process and closes the progress dialog. :param path: Path of the file whose loading was cancelled :type path: Path """ self.controller.cancel_loading(path) self._progress_dialogs[path].close()
[docs] def _rebuild_recent_menu(self) -> None: """ Rebuild the recent files menu. Updates the recent files menu with the currently stored recent files. """ self.recent_menu.clear() files = self.controller.get_recent_files() if not files: empty_action = QAction('(No recent files)', self) empty_action.setEnabled(False) self.recent_menu.addAction(empty_action) return for path in files: action = QAction(path.name, self) action.setData(str(path)) action.setToolTip(str(path)) action.setStatusTip(str(path)) action.triggered.connect(partial(self.controller.open_recent, path)) self.recent_menu.addAction(action) self.recent_menu.addSeparator() action = QAction('Clear recent', self) action.triggered.connect(self.controller.clear_recent_files) self.recent_menu.addAction(action)
[docs] def _rebuild_window_list(self) -> None: """ Rebuild the window list menu. Updates the window list menu with currently open windows. """ menu_actions = self.window_menu.actions() for action in menu_actions: if hasattr(action, 'window_id'): self.window_menu.removeAction(action) windows = self.controller.windows_manager.window_list for i, window in enumerate(windows): action = QAction(window.name, self) action.setCheckable(True) action.setChecked(window == self.controller.windows_manager.active_window) setattr(action, 'window_id', i) action.triggered.connect(partial(self.set_window_from_menu, i)) self.window_menu.addAction(action)
[docs] def set_window_from_menu(self, window_id: int) -> None: """ Set active window from menu selection. Activates a window based on its index in the window list menu. :param window_id: Index of the window to activate :type window_id: int """ self.controller.windows_manager.set_window_as_active(window_id)
[docs] def _on_window_closed(self, window: 'SubWindow[Any]') -> None: """ Handle window closing event. Removes the window from the application's window manager when closed. :param window: The window that was closed :type window: SubWindow[Any] """ self.controller.unregister_window(window.controller)
[docs] def _on_load_failed(self, path: Path, exc: Exception) -> None: """ Handle image loading failure. Displays an error message when image loading fails. :param path: Path of the file that failed to load :type path: Path :param exc: Exception that occurred during loading :type exc: Exception """ QMessageBox.critical(self, 'Load failed', f'{path}\n\n{exc}')
[docs] def show_about_dialog(self) -> None: """ Show the application's About dialog. Displays the About dialog containing application information. """ dialog = AboutDialog(self) dialog.exec()
[docs] def on_active_window_changed(self, window: 'SubWindow[Any]') -> None: """ Handle active window change. Updates the application state when the active window changes. :param window: The newly activated window :type window: SubWindow[Any] """ if isinstance(window, SubWindow): self.controller.set_active_window(window.controller)
[docs] def activate_window(self) -> None: """ Activate the currently active window. Sets focus to the currently active window in the MDI area. """ win = self.controller.windows_manager.active_window if win and win.view: # the win.view is still valid, even if already removed from the mdi if len(self.mdi.subWindowList()) != 0: self.mdi.setActiveSubWindow(win.view)
[docs] def activate_next_window(self) -> None: """ Activate the next window in the window list. Switches focus to the next window in the sequence. """ new_active_window = self.controller.windows_manager.next_window() if new_active_window is not None and new_active_window.view is not None: self.mdi.setActiveSubWindow(new_active_window.view)
[docs] def activate_previous_window(self) -> None: """ Activate the previous window in the window list. Switches focus to the previous window in the sequence. """ new_active_window = self.controller.windows_manager.previous_window() if new_active_window is not None and new_active_window.view is not None: self.mdi.setActiveSubWindow(new_active_window.view)
[docs] def _synch_ui_state(self) -> None: """ Synchronize UI state with application state. Updates the UI components based on the current application state, enabling/disabling menus and tool docks appropriately. """ has_images = self.controller.app_state.has_images active_window = self.controller.app_state.active_window self._update_save_status(active_window) self._update_close_status(close_active=bool(active_window), close_all=has_images) self._update_export(active_window) self._update_metadata_action(active_window) if active_window: active_window_type = self.controller.app_state.active_window.sub_window_type else: active_window_type = SubWindowEnum.NONE self.tool_menu.setEnabled(has_images) self.window_menu.setEnabled(has_images) has_tool_docks = len(self.controller.dock_register) > 0 if has_tool_docks: for dock in self.controller.dock_register.values(): dock.setEnabled(compare_flags(active_window_type, dock.enable_for_window_type)) self.tool_dock_view.setEnabled(has_tool_docks) self.controller.sync_tool_menu_state(active_window)
[docs] def _update_metadata_action(self, active_window: SubWindow[Any]) -> None: """ Update the image metadata action enabled state. Enables the metadata action only when the active window is an image window. :param active_window: The currently active sub-window :type active_window: SubWindow[Any] """ self.view_image_metadata_action.setEnabled(isinstance(active_window, ImageWindowController))
[docs] def show_image_metadata(self) -> None: """ Open a metadata window for the active image. """ self.controller.show_image_metadata_window()
[docs] def _update_save_status(self, active_window: SubWindow[Any]) -> None: """ Update the save and save-as menu item enabled states. Enables or disables the save and save-as menu items based on whether the active window implements the Savable interface and can perform save operations. :param active_window: The currently active sub-window :type active_window: SubWindow[Any] """ if isinstance(active_window, Savable): self.file_save.setEnabled(active_window.can_save()) self.file_save_as.setEnabled(active_window.can_save_as()) else: self.file_save.setEnabled(False) self.file_save_as.setEnabled(False)
[docs] def _update_close_status(self, close_active: bool, close_all: bool) -> None: """ Update the close and close-all menu item enabled states. Enables or disables the close and close-all menu items based on whether there is an active window and whether there are any open windows respectively. :param close_active: Whether the close active window action should be enabled :type close_active: bool :param close_all: Whether the close all windows action should be enabled :type close_all: bool """ self.close_action.setEnabled(close_active) self.close_all_action.setEnabled(close_all)
[docs] def _update_export(self, active_window: SubWindow[Any]) -> None: """ Update the export menu item enabled state. Enables or disables the export menu item based on whether the active window implements the Exportable interface and can perform export operations. :param active_window: The currently active sub-window :type active_window: SubWindow[Any] """ if isinstance(active_window, Exportable): self.export_action.setEnabled(active_window.can_export_as()) else: self.export_action.setEnabled(False)
[docs] def eventFilter(self, obj: Any, event: QEvent) -> bool: """ Filter events for the MDI area. Handles drag and drop events for the MDI area. :param obj: Object that received the event :param event: The event to filter :return: True if event was handled, False otherwise :rtype: bool """ if obj is self.mdi: if event.type() == event.Type.DragEnter: self.handle_drag_enter(event) return True elif event.type() == event.Type.Drop: self.handle_drop(event) return True return super().eventFilter(obj, event)
[docs] @staticmethod def handle_drag_enter(event: QEvent) -> None: """ Handle drag enter event. Accepts drag operations that contain URLs (files). :param event: The drag enter event """ # Only accept the drag if there's at least one URL (file) being dragged if hasattr(event, 'mimeData'): if TYPE_CHECKING: assert isinstance(event, QDropEvent) if event.mimeData().hasUrls(): event.acceptProposedAction() else: event.ignore()
[docs] def handle_drop(self, event: QEvent) -> None: """ Handle drop event. Processes dropped files, loading TIFF files and showing warnings for unsupported files. :param event: The drop event """ if hasattr(event, 'mimeData') and event.mimeData().hasUrls() and isinstance(event, QDropEvent): # Process the dropped files for url in event.mimeData().urls(): file_path = url.toLocalFile() if file_path.lower().endswith(('.tiff', '.tif', '.xyz')): self.controller.open_image(Path(file_path)) else: QMessageBox.warning( self, 'Unsupported File Type', f'The file {Path(file_path).name} is not a supported TIFF or XYZ image file.', ) event.acceptProposedAction() else: event.ignore()
[docs] def closeEvent(self, event: QEvent) -> None: """ Handle window close event. Saves the window state and geometry before closing. :param event: The close event """ if TYPE_CHECKING: assert isinstance(event, QCloseEvent) settings = QSettings('JRC', 'RadioViz') settings.setValue('MainWindow/state', self.saveState()) settings.setValue('MainWindow/geometry', self.saveGeometry()) super().closeEvent(event)
[docs] def restore_layout(self) -> None: """ Restore saved window layout. Restores the previously saved window geometry and state from settings. """ settings = QSettings('JRC', 'RadioViz') state = settings.value('MainWindow/state') geometry = settings.value('MainWindow/geometry') if geometry: self.restoreGeometry(geometry) if state: self.restoreState(state)
[docs] def _init_default_layout(self) -> None: """ Initialize default window layout. Saves the initial window state for later restoration. """ # self._default_geometry = self.saveGeometry() self._default_state = self.saveState()
[docs] def restore_default_layout(self) -> None: """ Restore default window layout. Restores the default window layout configuration. """ self.restoreState(self._default_state)
[docs] def save_as(self) -> None: """ Save the active window with a new filename. Opens a file dialog to allow the user to specify a new filename and saves the currently active window's content using the save coordinator. This method checks if the active window implements the Savable interface before attempting to save. If the active window does not support saving, the operation is skipped. """ controller = self.controller.app_state.active_window if isinstance(controller, Savable): self.application_services.save_coordinator.save_as(controller, self)
[docs] def save(self) -> None: """ Save the active window. Saves the currently active window's content using the save coordinator. This method checks if the active window implements the Savable interface before attempting to save. If the active window does not support saving, the operation is skipped. """ controller = self.controller.app_state.active_window if isinstance(controller, Savable): self.application_services.save_coordinator.save(controller, self)
[docs] def export(self) -> None: """ Export the active window content. Opens an export dialog to allow the user to specify export parameters and exports the currently active window's content using the export coordinator. This method checks if the active window implements the Exportable interface before attempting to export. If the active window does not support exporting, the operation is skipped. """ controller = self.controller.app_state.active_window if isinstance(controller, Exportable): self.application_services.export_coordinator.export(controller, self)
[docs] def save_workspace(self) -> None: """ Save the current workspace to a file. Opens a file dialog to allow the user to specify a filename and save the current workspace state in either JSON or HDF5 format. The user can choose between light (*.json) and full (*.h5) workspace formats. The saved workspace includes all open windows and their configurations. """ path, type_ = QFileDialog.getSaveFileName( parent=self, caption='Save Workspace', filter='Light (*.json);;Full (*.h5)', ) if not path: # Dialog was cancelled; nothing to do here without a valid file path. return path_p = Path(path) if type_ == 'Light (*.json)': serializer_type = SerializerType.JSON elif type_ == 'Full (*.h5)': serializer_type = SerializerType.HDF5 else: raise RuntimeError(f'Unknown type {type_}') self.controller.save_workspace(path_p, serializer_type)
[docs] def load_workspace(self) -> None: """ Load a workspace from a file. Opens a file dialog to allow the user to select a workspace file to load and restores the workspace state including all windows and their configurations. The user can choose between light (*.json) and full (*.h5) workspace formats. Previously saved workspace data will be restored in the application. """ path, type_ = QFileDialog.getOpenFileName( parent=self, caption='Load Workspace', filter='All workspaces (*.json *.h5);;Light (*.json);;Full (*.h5)', ) if path is None or path == '': return path_p = Path(path) if type_ == 'Light (*.json)': serializer_type = SerializerType.JSON elif type_ == 'Full (*.h5)': serializer_type = SerializerType.HDF5 elif type_ == 'All workspaces (*.json *.h5)': serializer_type = {'.json': SerializerType.JSON, '.h5': SerializerType.HDF5}[path_p.suffix] else: raise RuntimeError(f'Unknown type {type_}') self.controller.load_workspace(path_p, serializer_type)
[docs] def _close_active_window(self) -> None: """ Close the currently active window. Sends a request to the windows manager to close the currently active window. """ self.controller.windows_manager.close_active_window()
[docs] def _close_all_windows(self) -> None: """ Close all open windows. Sends a request to the windows manager to close all currently open windows. """ self.controller.windows_manager.close_all_windows()