From a5f7ebbb092e01912a5e8339b531c596a7d7352e Mon Sep 17 00:00:00 2001 From: Doralitze Date: Sat, 10 Jan 2026 17:44:28 +0100 Subject: [PATCH 1/5] add: option to remove assets --- src/model/media_assets/asset.py | 6 +++++- src/model/media_assets/registry.py | 19 +++++++++++++++++++ src/view/dialogs/asset_mgmt_dialog.py | 17 ++++++++++++++++- .../utility_widgets/asset_selection_widget.py | 5 ++++- 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/model/media_assets/asset.py b/src/model/media_assets/asset.py index 02dbc984..3c08ebc1 100644 --- a/src/model/media_assets/asset.py +++ b/src/model/media_assets/asset.py @@ -8,7 +8,7 @@ from typing import TYPE_CHECKING from uuid import uuid4 -from model.media_assets.registry import register +from model.media_assets.registry import register, unregister if TYPE_CHECKING: from PySide6.QtGui import QPixmap @@ -102,3 +102,7 @@ def is_local_resource(self) -> bool: Non-local resources need to be provided and copied together with the show file. """ return False + + def unregister(self) -> None: + """Unregister this asset.""" + unregister(self) diff --git a/src/model/media_assets/registry.py b/src/model/media_assets/registry.py index dacc2cc5..4abaf857 100644 --- a/src/model/media_assets/registry.py +++ b/src/model/media_assets/registry.py @@ -30,6 +30,25 @@ def register(asset: MediaAsset, uuid: str) -> bool: d[uuid] = asset return True +def unregister(asset: MediaAsset) -> bool: + """Method unregisters a media asset. + + Args: + asset: the media asset to unregister + + Returns: + True if the asset was successfully unregistered, False otherwise. + + """ + reg = _asset_library.get(asset.get_type()) + if reg is None: + return False + try: + reg.pop(asset.id) + return True + except KeyError: + return False + def get_asset_by_uuid(uuid: str) -> MediaAsset | None: """Get a media asset by its UUID. diff --git a/src/view/dialogs/asset_mgmt_dialog.py b/src/view/dialogs/asset_mgmt_dialog.py index 5c288d02..68aba5ae 100644 --- a/src/view/dialogs/asset_mgmt_dialog.py +++ b/src/view/dialogs/asset_mgmt_dialog.py @@ -36,10 +36,16 @@ def __init__(self, parent: QWidget | None = None, show_file_path: str | None = N self._load_asset_file.setText("Load asset from file") self._load_asset_file.triggered.connect(self._open_file) self._action_button_group.addAction(self._load_asset_file) + self._delete_selected_asset_action = QAction() + self._delete_selected_asset_action.setIcon(QIcon.fromTheme("edit-delete")) + self._delete_selected_asset_action.setText("Delete selected asset") + self._delete_selected_asset_action.triggered.connect(self._delete_selected_asset) + self._delete_selected_asset_action.setEnabled(False) + self._action_button_group.addAction(self._delete_selected_asset_action) layout.addWidget(self._action_button_group) - self._asset_display = AssetSelectionWidget(self) + self._asset_display = AssetSelectionWidget(self, multiselection_allowed=True) layout.addWidget(self._asset_display) self._close_button_group = QDialogButtonBox(QDialogButtonBox.StandardButton.Close) @@ -51,6 +57,7 @@ def __init__(self, parent: QWidget | None = None, show_file_path: str | None = N self._dialog: QFileDialog | None = None self._show_file_path = show_file_path if show_file_path is not None else "" self.setMinimumWidth(800) + self._asset_display.asset_selection_changed.connect(self._selected_asset_changed) def _open_file(self) -> None: self._dialog = QFileDialog(self, "Open file") @@ -90,3 +97,11 @@ def _process_loading_files(self, list_of_files: list[str]) -> None: detailedText=accumulated_errors) msg_box.show() self._dialog = msg_box + + def _selected_asset_changed(self) -> None: + self._delete_selected_asset_action.setEnabled(len(self._asset_display.selected_asset) > 0) + + def _delete_selected_asset(self) -> None: + for asset in self._asset_display.selected_asset: + asset.unregister() + self._asset_display.reload_model() diff --git a/src/view/utility_widgets/asset_selection_widget.py b/src/view/utility_widgets/asset_selection_widget.py index fd157ab7..2d267e11 100644 --- a/src/view/utility_widgets/asset_selection_widget.py +++ b/src/view/utility_widgets/asset_selection_widget.py @@ -5,7 +5,7 @@ import os from typing import TYPE_CHECKING, Any, override -from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt +from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt, Signal from PySide6.QtGui import QAction, QIcon from PySide6.QtWidgets import QAbstractItemView, QLineEdit, QTableView, QToolBar, QVBoxLayout, QWidget @@ -148,6 +148,8 @@ def get_row_indicies(self, assets: list[MediaAsset]) -> list[int]: class AssetSelectionWidget(QWidget): """Provide a sortable and searchable selection widget for assets.""" + asset_selection_changed: Signal = Signal() + def __init__(self, parent: QWidget | None = None, allowed_types: list[MediaType] | None = None, multiselection_allowed: bool = True) -> None: """Initialize the asset selection widget. @@ -199,6 +201,7 @@ def __init__(self, parent: QWidget | None = None, allowed_types: list[MediaType] self.setLayout(layout) self._update_filter() + self._asset_view.selectionModel().selectionChanged.connect(lambda: self.asset_selection_changed.emit()) def _update_filter(self, force: bool = False) -> None: selected_types: set[MediaType] = set() From 63f36dd7ef53a1777c49ca8ead11b25533d3d3c9 Mon Sep 17 00:00:00 2001 From: Doralitze Date: Sat, 10 Jan 2026 18:32:05 +0100 Subject: [PATCH 2/5] add: option to edit icons of macro buttons --- src/view/dialogs/asset_selection_dialog.py | 51 +++++++++++++++++++ .../macro_buttons_ui_widget.py | 42 ++++++++++++--- 2 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 src/view/dialogs/asset_selection_dialog.py diff --git a/src/view/dialogs/asset_selection_dialog.py b/src/view/dialogs/asset_selection_dialog.py new file mode 100644 index 00000000..1b722f0b --- /dev/null +++ b/src/view/dialogs/asset_selection_dialog.py @@ -0,0 +1,51 @@ +"""Provide a dialog for selecting assets.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, override + +from PySide6.QtCore import Qt, Signal +from PySide6.QtWidgets import QDialog, QWidget, QVBoxLayout, QDialogButtonBox +from qasync import QApplication + +from model.media_assets.asset import MediaAsset +from view.utility_widgets.asset_selection_widget import AssetSelectionWidget + +if TYPE_CHECKING: + from model.media_assets.media_type import MediaType + + +class AssetSelectionDialog(QDialog): + """A dialog for selecting assets.""" + + asset_selected: Signal = Signal(MediaAsset) + + def __init__(self, parent: QWidget | None = None, allowed_types: list[MediaType] | None = None, multiselection_allowed: bool = False) -> None: + """Initialize the dialog.""" + super().__init__(parent) + layout = QVBoxLayout() + + self._asset_view = AssetSelectionWidget(self, allowed_types, multiselection_allowed) + layout.addWidget(self._asset_view) + + button_box = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, Qt.Orientation.Horizontal, self + ) + layout.addWidget(button_box) + button_box.accepted.connect(self.accept) + button_box.rejected.connect(self.reject) + self.setMinimumWidth(800) + self.setMinimumHeight(600) + self.setLayout(layout) + + @override + def accept(self) -> None: + self.asset_selected.emit(self._asset_view.selected_asset) + QApplication.processEvents() + super().accept() + self.close() + + @override + def reject(self) -> None: + super().reject() + self.close() diff --git a/src/view/show_mode/show_ui_widgets/macro_buttons_ui_widget.py b/src/view/show_mode/show_ui_widgets/macro_buttons_ui_widget.py index cae5ab09..d926117e 100644 --- a/src/view/show_mode/show_ui_widgets/macro_buttons_ui_widget.py +++ b/src/view/show_mode/show_ui_widgets/macro_buttons_ui_widget.py @@ -28,6 +28,7 @@ from model.media_assets.registry import get_asset_by_uuid from utility import resource_path from view.action_setup_view._command_insertion_dialog import escape_argument +from view.dialogs.asset_selection_dialog import AssetSelectionDialog from view.show_mode.editor.editor_tab_widgets.ui_widget_editor._widget_holder import UIWidgetHolder from view.show_mode.editor.show_browser.annotated_item import AnnotatedListWidgetItem from view.utility_widgets.asset_selection_widget import AssetSelectionWidget @@ -35,6 +36,7 @@ if TYPE_CHECKING: from model import UIPage + from model.media_assets.asset import MediaAsset class _AddMacroActionDialog(QDialog): @@ -108,12 +110,9 @@ def __init__(self, parent: QListWidget, item_def: dict[str, str], index: int, up self._icon_bt.setIcon(self._NO_ICON) layout.addWidget(self._icon_bt) layout.addStretch() - icon_label = QLabel(self) - if self._item_def.get("icon", "") != "": - asset = get_asset_by_uuid(self._item_def["icon"]) - if asset is not None: - icon_label.setPixmap(asset.get_thumbnail()) - layout.addWidget(icon_label) + self.icon_label = QLabel(self) + self._update_displayed_icon() + layout.addWidget(self.icon_label) layout.addStretch() self._text_tb = QLineEdit(self) self._text_tb.setText(item_def["text"]) @@ -125,7 +124,18 @@ def __init__(self, parent: QListWidget, item_def: dict[str, str], index: int, up self._command_tb.textChanged.connect(self._command_changed) layout.addWidget(self._command_tb) self.setLayout(layout) - # TODO implement icon changing functionality + self._dialog: QDialog | None = None + self._icon_bt.pressed.connect(self._change_icon_of_button) + + def _update_displayed_icon(self): + found_icon = False + if self._item_def.get("icon", "") != "": + asset = get_asset_by_uuid(self._item_def["icon"]) + if asset is not None: + self.icon_label.setPixmap(asset.get_thumbnail()) + found_icon = True + if not found_icon: + self.icon_label.setPixmap(self._NO_ICON.pixmap(64, 64)) def _text_changed(self, text: str) -> None: self._item_def["text"] = text @@ -135,6 +145,24 @@ def _command_changed(self, text: str) -> None: self._item_def["command"] = text self._update_button.setEnabled(True) + def _change_icon_of_button(self) -> None: + self._dialog = AssetSelectionDialog(self, allowed_types=[MediaType.IMAGE], multiselection_allowed=False) + self._dialog.setModal(True) + self._dialog.asset_selected.connect(self._icon_changed) + self._dialog.open() + + def _icon_changed(self, asset: list[MediaAsset]) -> None: + self._dialog = None + if len(asset) == 0: + asset_id = "" + else: + asset = asset[-1] + asset_id = asset.id if asset is not None else "" + if asset_id != self._item_def.get("icon", ""): + self._update_button.setEnabled(True) + self._item_def["icon"] = asset_id + self._update_displayed_icon() + @property def item_def(self) -> dict[str, str]: return self._item_def From 66caaf51d5fc418eda8c8017196f4e93b6ac81d4 Mon Sep 17 00:00:00 2001 From: Doralitze Date: Sat, 10 Jan 2026 21:44:40 +0100 Subject: [PATCH 3/5] add: dialog to ask for exit --- src/view/dialogs/asset_selection_dialog.py | 3 ++- src/view/main_window.py | 16 ++++++++++++++++ .../show_ui_widgets/macro_buttons_ui_widget.py | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/view/dialogs/asset_selection_dialog.py b/src/view/dialogs/asset_selection_dialog.py index 1b722f0b..f529b62b 100644 --- a/src/view/dialogs/asset_selection_dialog.py +++ b/src/view/dialogs/asset_selection_dialog.py @@ -20,7 +20,8 @@ class AssetSelectionDialog(QDialog): asset_selected: Signal = Signal(MediaAsset) - def __init__(self, parent: QWidget | None = None, allowed_types: list[MediaType] | None = None, multiselection_allowed: bool = False) -> None: + def __init__(self, parent: QWidget | None = None, + allowed_types: list[MediaType] | None = None, multiselection_allowed: bool = False) -> None: """Initialize the dialog.""" super().__init__(parent) layout = QVBoxLayout() diff --git a/src/view/main_window.py b/src/view/main_window.py index 62c32acd..f4c2a79c 100644 --- a/src/view/main_window.py +++ b/src/view/main_window.py @@ -28,6 +28,7 @@ from view.misc.console_dock_widget import ConsoleDockWidget from view.misc.settings.settings_dialog import SettingsDialog from view.patch_view.patch_mode import PatchMode +from view.show_mode.editor.node_editor_widgets.cue_editor.yes_no_dialog import YesNoDialog from view.show_mode.editor.showmanager import ShowEditorWidget from view.show_mode.player.showplayer import ShowPlayerWidget from view.utility_widgets.wizzards.patch_plan_export import PatchPlanExportWizard @@ -136,6 +137,7 @@ def __init__(self, parent: QWidget = None) -> None: self._terminal_widget: ConsoleDockWidget | None = None self.setWindowIcon(QPixmap(resource_path(os.path.join("resources", "logo.png")))) + self._close_now = False @property def fish_connector(self) -> NetworkManager: @@ -405,3 +407,17 @@ def _toggle_terminal(self) -> None: def _open_asset_mgmt_dialog(self) -> None: self._settings_dialog = AssetManagementDialog(self, self._board_configuration.file_path) self._settings_dialog.show() + + @override + def closeEvent(self, event: QCloseEvent) -> None: + if self._close_now: + super().closeEvent(event) + else: + event.ignore() + self._settings_dialog = YesNoDialog(self, "Close Editor", "Do you really want to close this window? Any unsaved changes will be lost.", self.close_callback) + self._settings_dialog.setModal(True) + self._settings_dialog.show() + + def close_callback(self) -> None: + self._close_now = True + self.close() diff --git a/src/view/show_mode/show_ui_widgets/macro_buttons_ui_widget.py b/src/view/show_mode/show_ui_widgets/macro_buttons_ui_widget.py index d926117e..33bcbe79 100644 --- a/src/view/show_mode/show_ui_widgets/macro_buttons_ui_widget.py +++ b/src/view/show_mode/show_ui_widgets/macro_buttons_ui_widget.py @@ -127,7 +127,7 @@ def __init__(self, parent: QListWidget, item_def: dict[str, str], index: int, up self._dialog: QDialog | None = None self._icon_bt.pressed.connect(self._change_icon_of_button) - def _update_displayed_icon(self): + def _update_displayed_icon(self) -> None: found_icon = False if self._item_def.get("icon", "") != "": asset = get_asset_by_uuid(self._item_def["icon"]) From f1c056ea51d2f9535df880ad41c91182e8b20b44 Mon Sep 17 00:00:00 2001 From: Leon Dietrich Date: Mon, 12 Jan 2026 18:37:25 +0100 Subject: [PATCH 4/5] fix: duplicate declaration of closing event in main --- src/view/dialogs/asset_selection_dialog.py | 2 +- src/view/main_window.py | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/view/dialogs/asset_selection_dialog.py b/src/view/dialogs/asset_selection_dialog.py index f529b62b..21669fd8 100644 --- a/src/view/dialogs/asset_selection_dialog.py +++ b/src/view/dialogs/asset_selection_dialog.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, override from PySide6.QtCore import Qt, Signal -from PySide6.QtWidgets import QDialog, QWidget, QVBoxLayout, QDialogButtonBox +from PySide6.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QWidget from qasync import QApplication from model.media_assets.asset import MediaAsset diff --git a/src/view/main_window.py b/src/view/main_window.py index f4c2a79c..22936de1 100644 --- a/src/view/main_window.py +++ b/src/view/main_window.py @@ -218,14 +218,6 @@ def _setup_menubar(self) -> None: self._add_entries_to_menu(menu, entries) self.menuBar().addAction(menu.menuAction()) - @override - def closeEvent(self, event: QCloseEvent, /) -> None: - # TODO use event.ignore() here is there's still stuff to do - super().closeEvent(event) - QApplication.processEvents() - self._broadcaster.application_closing.emit() - QApplication.processEvents() - def _start_connection(self) -> None: # TODO rework to signals self._fish_connector.start(True) @@ -412,9 +404,17 @@ def _open_asset_mgmt_dialog(self) -> None: def closeEvent(self, event: QCloseEvent) -> None: if self._close_now: super().closeEvent(event) + QApplication.processEvents() + self._broadcaster.application_closing.emit() + QApplication.processEvents() else: event.ignore() - self._settings_dialog = YesNoDialog(self, "Close Editor", "Do you really want to close this window? Any unsaved changes will be lost.", self.close_callback) + self._settings_dialog = YesNoDialog( + self, + "Close Editor", + "Do you really want to close this window? Any unsaved changes will be lost.", + self.close_callback + ) self._settings_dialog.setModal(True) self._settings_dialog.show() From 25f4180ffbbd6ad165b44dca3e474ca3711e11c0 Mon Sep 17 00:00:00 2001 From: Leon Dietrich Date: Thu, 15 Jan 2026 16:41:03 +0100 Subject: [PATCH 5/5] fix: main window close callback to be private --- src/view/main_window.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/view/main_window.py b/src/view/main_window.py index 22936de1..888c1f21 100644 --- a/src/view/main_window.py +++ b/src/view/main_window.py @@ -413,11 +413,11 @@ def closeEvent(self, event: QCloseEvent) -> None: self, "Close Editor", "Do you really want to close this window? Any unsaved changes will be lost.", - self.close_callback + self._close_callback ) self._settings_dialog.setModal(True) self._settings_dialog.show() - def close_callback(self) -> None: + def _close_callback(self) -> None: self._close_now = True self.close()