# Copyright 2025–2026 European Union
# Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
# SPDX-License-Identifier: EUPL-1.2
"""
Dialog for selecting unsaved images before workspace save.
This module provides a modal dialog that allows users to select which
unsaved images should be included when saving a workspace to JSON format.
The dialog displays a tree view with checkboxes for each unsaved image,
showing its name and any dependencies that would also be affected.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Iterable
from uuid import UUID
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QDialog,
QDialogButtonBox,
QHBoxLayout,
QHeaderView,
QLabel,
QPushButton,
QTreeWidget,
QTreeWidgetItem,
QVBoxLayout,
QWidget,
)
from radioviz.services.workspace_manager import enforce_uuid
[docs]
@dataclass(frozen=True)
class UnsavedImageItem:
"""
Data class representing an unsaved image item in the dialog.
This class holds the information needed to display an unsaved image
in the selection dialog, including its unique identifier, display name,
and information about any dependencies.
:param image_id: The unique identifier of the image.
:type image_id: UUID
:param name: The display name of the image.
:type name: str
:param dependency_text: Text describing the profiles or regions that
depend on this image.
:type dependency_text: str
"""
image_id: UUID
name: str
dependency_text: str
[docs]
class UnsavedImagesDialog(QDialog):
"""
Modal dialog for selecting unsaved images before workspace save.
This dialog displays a tree view with checkboxes for each unsaved image,
allowing users to select which images should be included when saving
the workspace to JSON format. Each row shows the image name and any
dependent profiles or regions.
The dialog provides "Select All" and "Select None" buttons for quick
selection management. When the dialog is accepted, the
:meth:`checked_ids` method can be used to retrieve the UUIDs of
the selected images.
"""
def __init__(self, items: Iterable[UnsavedImageItem], parent: QWidget | None = None) -> None:
"""
Initialize the unsaved images selection dialog.
Creates a modal dialog with a tree view displaying all unsaved images
as checkable items. Each item shows the image name and its dependencies.
By default, all images are checked for inclusion.
:param items: Collection of unsaved image items to display in the dialog.
:type items: Iterable[UnsavedImageItem]
:param parent: The parent widget for the dialog, or None for no parent.
:type parent: QWidget | None
"""
super().__init__(parent=parent)
self.setWindowTitle('Unsaved images')
self.setModal(True)
layout = QVBoxLayout(self)
info = QLabel(
'Unchecked images will be excluded from the JSON workspace. '
'Dependent profiles/regions will also be excluded.'
)
info.setWordWrap(True)
layout.addWidget(info)
control_row = QHBoxLayout()
self._select_all_button = QPushButton('Select All')
self._select_none_button = QPushButton('Select None')
control_row.addWidget(self._select_all_button)
control_row.addWidget(self._select_none_button)
control_row.addStretch(1)
layout.addLayout(control_row)
self._tree = QTreeWidget()
self._tree.setHeaderLabels(['Image', 'Dependencies'])
self._tree.setRootIsDecorated(False)
self._tree.setUniformRowHeights(True)
layout.addWidget(self._tree)
for item in items:
tree_item = QTreeWidgetItem([item.name, item.dependency_text])
tree_item.setFlags(tree_item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
tree_item.setCheckState(0, Qt.CheckState.Checked)
tree_item.setData(0, Qt.ItemDataRole.UserRole, str(item.image_id))
self._tree.addTopLevelItem(tree_item)
header = self._tree.header()
header.setStretchLastSection(True)
header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
self._tree.resizeColumnToContents(0)
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout.addWidget(buttons)
self._select_all_button.clicked.connect(self._select_all)
self._select_none_button.clicked.connect(self._select_none)
self.resize(640, 420)
[docs]
def _select_all(self) -> None:
"""
Select all images in the tree view.
Sets the check state of every top-level item in the tree to checked,
including all images to be saved when the dialog is accepted.
"""
for i in range(self._tree.topLevelItemCount()):
item = self._tree.topLevelItem(i)
if item is None:
continue
item.setCheckState(0, Qt.CheckState.Checked)
[docs]
def _select_none(self) -> None:
"""
Deselect all images in the tree view.
Sets the check state of every top-level item in the tree to unchecked,
excluding all images from being saved when the dialog is accepted.
"""
for i in range(self._tree.topLevelItemCount()):
item = self._tree.topLevelItem(i)
if item is None:
continue
item.setCheckState(0, Qt.CheckState.Unchecked)
[docs]
def checked_ids(self) -> list[UUID]:
"""
Get the UUIDs of all checked images.
Iterates through all top-level items in the tree and returns a list
of UUIDs for those items that have a checked check state.
:return: List of UUIDs corresponding to the selected images.
:rtype: list[UUID]
"""
selected: list[UUID] = []
for i in range(self._tree.topLevelItemCount()):
item = self._tree.topLevelItem(i)
if item is None:
continue
if item.checkState(0) == Qt.CheckState.Checked:
raw_id = item.data(0, Qt.ItemDataRole.UserRole)
if raw_id is None:
continue
selected.append(enforce_uuid(raw_id))
return selected