diff --git a/src/controller/file/deserialization/migrations.py b/src/controller/file/deserialization/migrations.py index d07eb137..e1a92d9f 100644 --- a/src/controller/file/deserialization/migrations.py +++ b/src/controller/file/deserialization/migrations.py @@ -8,7 +8,7 @@ logger = getLogger(__name__) -def replace_old_filter_configurations(f: Filter) -> Filter: +def replace_old_filter_configurations(f: Filter) -> tuple[Filter, bool]: """Replace outdated filter representations if necessary. Some filter representations may have been replaced with their vFilter @@ -22,10 +22,24 @@ def replace_old_filter_configurations(f: Filter) -> Filter: Returns: The original filter if no modification was required, or a new - updated representation. + updated representation. Also True if a migration happened. """ + migration_happened: bool = False if f.filter_type == FilterTypeEnumeration.FILTER_TYPE_CUES: f.filter_type = int(FilterTypeEnumeration.VFILTER_CUES) - logger.info("Replaced filter type of filter %s to become virtual.", f.filter_id) - return f + logger.info("Replaced filter type of filter %s to become virtual on next load. Reloading is advised.", + f.filter_id) + migration_happened = True + if f.filter_type in [ + FilterTypeEnumeration.FILTER_FADER_RAW, + FilterTypeEnumeration.FILTER_FADER_HSI, + FilterTypeEnumeration.FILTER_FADER_HSIA, + FilterTypeEnumeration.FILTER_FADER_HSIU, + FilterTypeEnumeration.FILTER_FADER_HSIAU, + ]: + f.filter_type = int(int(f.filter_type) * -1) + logger.info("Replaced filter type of filter %s to become virtual on next load. Reloading is advised", + f.filter_id) + migration_happened = True + return f, migration_happened diff --git a/src/controller/file/read.py b/src/controller/file/read.py index c27dda19..2b0b56da 100644 --- a/src/controller/file/read.py +++ b/src/controller/file/read.py @@ -126,6 +126,7 @@ def read_document(file_name: str, board_configuration: BoardConfiguration) -> bo _clean_tags(root, prefix) scene_defs_to_be_parsed = [] + migration_happened = False loaded_banksets: dict[str, BankSet] = {} pn.total_step_count += len(root) @@ -149,7 +150,7 @@ def read_document(file_name: str, board_configuration: BoardConfiguration) -> bo pn.total_step_count += len(scene_defs_to_be_parsed) for scene_def in scene_defs_to_be_parsed: - _parse_scene(scene_def, board_configuration, loaded_banksets) + migration_happened |= _parse_scene(scene_def, board_configuration, loaded_banksets) pn.current_step_number += 1 pn.current_step_number += 1 @@ -169,6 +170,9 @@ def read_document(file_name: str, board_configuration: BoardConfiguration) -> bo board_configuration.broadcaster.end_show_file_parsing.emit() board_configuration.broadcaster.show_file_loaded.emit() pn.close() + if migration_happened: + board_configuration.broadcaster.message_box_required.emit("A filter migration was performed. Please save and " + "reload the show file.") return True @@ -260,7 +264,7 @@ def _parse_filter_page(element: ET.Element, parent_scene: Scene, instantiated_pa def _parse_scene( scene_element: ET.Element, board_configuration: BoardConfiguration, loaded_banksets: dict[str, BankSet] -) -> None: +) -> bool: """Load a scene from the show file data structure. Args: @@ -284,12 +288,17 @@ def _parse_scene( scene = Scene(scene_id=scene_id, human_readable_name=human_readable_name, board_configuration=board_configuration) + if scene_element.attrib.get("linkedBankset") in loaded_banksets: + scene.linked_bankset = loaded_banksets[scene_element.attrib["linkedBankset"]] + filter_pages = [] ui_page_elements = [] + migration_happened = False + for child in scene_element: match child.tag: case "filter": - _parse_filter(child, scene) + migration_happened |= _parse_filter(child, scene) case "filterpage": filter_pages.append(child) case "uipage": @@ -309,13 +318,11 @@ def _parse_scene( logger.error("No suitable parent found while parsing filter pages") break - if scene_element.attrib.get("linkedBankset") in loaded_banksets: - scene.linked_bankset = loaded_banksets[scene_element.attrib["linkedBankset"]] - for ui_page_element in ui_page_elements: _append_ui_page(ui_page_element, scene) board_configuration.broadcaster.scene_created.emit(scene) + return migration_happened def _append_ui_page(page_def: ET.Element, scene: Scene) -> None: @@ -387,13 +394,16 @@ def _append_ui_page(page_def: ET.Element, scene: Scene) -> None: scene.ui_pages.append(page) -def _parse_filter(filter_element: ET.Element, scene: Scene) -> None: +def _parse_filter(filter_element: ET.Element, scene: Scene) -> bool: """Load a filter from the XML definition. Args: filter_element: The XML data to load the filter from. scene: The scene to append the filter to. + Returns: + bool: True if a migration happened and the show file should be saved and reloaded. + """ filter_id = "" filter_type = 0 @@ -427,10 +437,11 @@ def _parse_filter(filter_element: ET.Element, scene: Scene) -> None: case _: logger.warning("Filter %s contains unknown element: %s", filter_id, child.tag) - filter_ = replace_old_filter_configurations(filter_) + filter_, migration_happened = replace_old_filter_configurations(filter_) if isinstance(filter_, VirtualFilter): filter_.deserialize() scene.append_filter(filter_) + return migration_happened def _parse_channel_link(initial_parameters_element: ET.Element, filter_: Filter) -> None: diff --git a/src/model/broadcaster.py b/src/model/broadcaster.py index 648b2232..b0f697e1 100644 --- a/src/model/broadcaster.py +++ b/src/model/broadcaster.py @@ -93,6 +93,7 @@ class Broadcaster(QtCore.QObject, metaclass=QObjectSingletonMeta): view_to_action_config: QtCore.Signal = QtCore.Signal() view_leave_action_config: QtCore.Signal = QtCore.Signal() application_closing: QtCore.Signal = QtCore.Signal() + message_box_required: QtCore.Signal = QtCore.Signal(str) ################################################################ save_button_pressed: QtCore.Signal = QtCore.Signal() commit_button_pressed: QtCore.Signal = QtCore.Signal() diff --git a/src/model/filter.py b/src/model/filter.py index 07337668..f5b9f602 100644 --- a/src/model/filter.py +++ b/src/model/filter.py @@ -77,6 +77,12 @@ class FilterTypeEnumeration(IntFlag): Negative values indicate virtual filters. """ + VFILTER_FADER_RAW = -39 + VFILTER_FADER_HSI = -40 + VFILTER_FADER_HSIA = -41 + VFILTER_FADER_HSIU = -42 + VFILTER_FADER_HSIAU = -43 + VFILTER_SEQUENCER = -12 VFILTER_COLOR_MIXER = -11 VFILTER_IMPORT = -10 diff --git a/src/model/virtual_filters/fader_vfilter.py b/src/model/virtual_filters/fader_vfilter.py new file mode 100644 index 00000000..4e63e2fa --- /dev/null +++ b/src/model/virtual_filters/fader_vfilter.py @@ -0,0 +1,94 @@ +"""Module containing vFilter wrapper for fader filters.""" +from __future__ import annotations + +from logging import getLogger +from typing import TYPE_CHECKING, override + +from model import Filter +from model.control_desk import BankSet, BanksetIDUpdateListener +from model.filter import FilterTypeEnumeration, VirtualFilter + +if TYPE_CHECKING: + from model.scene import Scene + +logger = getLogger(__name__) + +class FaderUpdatingVFilter(VirtualFilter, BanksetIDUpdateListener): + """VFilter wrapper to automatically register bank set updates on filter load.""" + + def __init__( + self, + scene: Scene, + filter_id: str, + filter_type: FilterTypeEnumeration, + pos: tuple[int] | None = None, + ) -> None: + """Initialize the filter. + + As this virtual filter simply adds the required callbacks to bank set handling, it behaves much like the + original filter. + + Args: + scene: The scene of the filter. + filter_id: The id of the filter. + filter_type: The type of the filter. Must be one of the fader ones. + pos: The position of the filter inside the filter page. + + """ + super().__init__(scene, filter_id, filter_type=int(filter_type), pos=pos) + self.bankset_model: BankSet | None = None + self.update_bankset_listener() + + @override + def resolve_output_port_id(self, virtual_port_id: str) -> str | None: + return f"{self.filter_id}:{virtual_port_id}" + + @override + def instantiate_filters(self, filter_list: list[Filter]) -> None: + f = Filter(self.scene, self.filter_id, FilterTypeEnumeration(self.filter_type * -1), + pos=self.pos, filter_configurations=self.filter_configurations.copy(), + initial_parameters=self.initial_parameters.copy()) + f.channel_links.update(self.channel_links) + f.gui_update_keys.update(self.gui_update_keys) + f.initial_parameters.update(self.initial_parameters) + f.in_data_types.update(self.in_data_types) + f.default_values.update(self.default_values) + filter_list.append(f) + + def update_bankset_listener(self) -> None: + """Update the attached bank set UUID change listener of this fader.""" + if not "set_id" in self.filter_configurations.keys() or self.scene.linked_bankset is None: + return + set_id = self.filter_configurations["set_id"] + + if self.scene.linked_bankset.id == set_id: + self._bankset_model = self.scene.linked_bankset + else: + for bs in BankSet.linked_bank_sets(): + if bs.id == set_id: + self._bankset_model = bs + break + if self._bankset_model is None: + column_candidate = self.scene.linked_bankset.get_column( + self.filter_configurations.get("column_id")) + if column_candidate: + self.filter_configurations["set_id"] = self.scene.linked_bankset.id + self._bankset_model = self.scene.linked_bankset + + if self._bankset_model is not None: + self._bankset_model.id_update_listeners.append(self) + + @override + def notify_on_new_id(self, new_id: str) -> None: + logger.debug("New bankset ID: %s", new_id) + self.filter.filter_configurations["set_id"] = new_id + + @override + def deserialize(self) -> None: + super().deserialize() + self.update_bankset_listener() + + def __del__(self) -> None: + """Deregister the bank set update listener.""" + if self._bankset_model is not None: + self._bankset_model.id_update_listeners.remove(self) diff --git a/src/model/virtual_filters/vfilter_factory.py b/src/model/virtual_filters/vfilter_factory.py index b8494eb0..97346a3c 100644 --- a/src/model/virtual_filters/vfilter_factory.py +++ b/src/model/virtual_filters/vfilter_factory.py @@ -13,6 +13,7 @@ from model.virtual_filters.color_mixer_vfilter import ColorMixerVFilter from model.virtual_filters.cue_vfilter import CueFilter from model.virtual_filters.effects_stacks.vfilter import EffectsStack +from model.virtual_filters.fader_vfilter import FaderUpdatingVFilter from model.virtual_filters.import_vfilter import ImportVFilter from model.virtual_filters.pan_tilt_constant import PanTiltConstantFilter from model.virtual_filters.range_adapters import ( @@ -46,6 +47,10 @@ def construct_virtual_filter_instance( if not filter_type < 0: raise ValueError("The provided filter is not a virtual description.") match filter_type: + case (FilterTypeEnumeration.VFILTER_FADER_RAW | FilterTypeEnumeration.VFILTER_FADER_HSI | + FilterTypeEnumeration.VFILTER_FADER_HSIA | FilterTypeEnumeration.FILTER_FADER_HSIU | + FilterTypeEnumeration.FILTER_FADER_HSIAU): + return FaderUpdatingVFilter(scene, filter_id, filter_type, pos=pos) case FilterTypeEnumeration.VFILTER_COMBINED_FILTER_PRESET: # TODO return virtual filter that instantiates a preset (as described in issue #48) return None diff --git a/src/view/main_window.py b/src/view/main_window.py index aff68034..37aab983 100644 --- a/src/view/main_window.py +++ b/src/view/main_window.py @@ -8,7 +8,7 @@ from PySide6 import QtGui, QtWidgets from PySide6.QtGui import QCloseEvent, QIcon, QKeySequence, QPixmap -from PySide6.QtWidgets import QApplication, QProgressBar, QWidget +from PySide6.QtWidgets import QApplication, QProgressBar, QWidget, QMessageBox import proto.RealTimeControl_pb2 import style @@ -132,6 +132,7 @@ def __init__(self, parent: QWidget = None) -> None: self._utility_wizard: QWizard | None = None self.setWindowIcon(QPixmap(resource_path(os.path.join("resources", "logo.png")))) + self._broadcaster.message_box_required.connect(self._open_message_box) @property def fish_connector(self) -> NetworkManager: @@ -379,3 +380,9 @@ def _cleanup_wizard(self) -> None: if self._utility_wizard is not None: self._utility_wizard.deleteLater() self._utility_wizard = None + + def _open_message_box(self, message: str) -> None: + self._settings_dialog = QMessageBox(self) + self._settings_dialog.setText(message) + self._settings_dialog.setModal(True) + self._settings_dialog.show() diff --git a/src/view/show_mode/editor/filter_settings_item.py b/src/view/show_mode/editor/filter_settings_item.py index 916bb305..5faabfec 100644 --- a/src/view/show_mode/editor/filter_settings_item.py +++ b/src/view/show_mode/editor/filter_settings_item.py @@ -115,7 +115,7 @@ def check_if_filter_has_special_widget(filter_: Filter) -> NodeEditorFilterConfi The instantiated settings widget or None. """ - if 39 <= filter_.filter_type <= 43: + if 39 <= filter_.filter_type <= 43 or -43 <= filter_.filter_type <= -39: return ColumnSelect(filter_) if filter_.filter_type in [FilterTypeEnumeration.FILTER_TYPE_CUES, FilterTypeEnumeration.VFILTER_CUES]: return CueEditor(f=filter_) diff --git a/src/view/show_mode/editor/nodes/impl/faders.py b/src/view/show_mode/editor/nodes/impl/faders.py index 64a89461..8bbdb586 100644 --- a/src/view/show_mode/editor/nodes/impl/faders.py +++ b/src/view/show_mode/editor/nodes/impl/faders.py @@ -1,16 +1,25 @@ -"""Column fader filter nodes""" +"""Column fader filter nodes.""" + +from logging import getLogger +from typing import TYPE_CHECKING from model import DataType, Filter, Scene -from model.control_desk import BankSet from model.filter import FilterTypeEnumeration +from model.virtual_filters.fader_vfilter import FaderUpdatingVFilter from view.show_mode.editor.nodes.base.filternode import FilterNode +logger = getLogger(__name__) + +if TYPE_CHECKING: + from model.control_desk import BankSet + class _FaderNode(FilterNode): def __init__(self, model: Filter | Scene, filter_type: FilterTypeEnumeration, name: str, terminals: dict[str, dict[str, str]]) -> None: - self._bankset_model: BankSet | None = None super().__init__(model=model, filter_type=filter_type, name=name, terminals=terminals) + self._bankset_model: BankSet | None = self.filter.bankset_model if ( + isinstance(self.filter, FaderUpdatingVFilter)) else None try: self.filter.filter_configurations["set_id"] = model.filter_configurations["set_id"] @@ -20,48 +29,24 @@ def __init__(self, model: Filter | Scene, filter_type: FilterTypeEnumeration, na self.filter.filter_configurations["column_id"] = model.filter_configurations["column_id"] except AttributeError: self.filter.filter_configurations["column_id"] = "" - self._update_bankset_listener() - - def _update_bankset_listener(self) -> None: - set_id = self.filter.filter_configurations["set_id"] - - if self.filter.scene.linked_bankset.id == set_id: - self._bankset_model = self.filter.scene.linked_bankset - else: - for bs in BankSet.linked_bank_sets(): - if bs.id == set_id: - self._bankset_model = bs - break - if self._bankset_model is None: - column_candidate = self.filter.scene.linked_bankset.get_column( - self.filter.filter_configurations.get("column_id")) - if column_candidate: - self.filter.filter_configurations["set_id"] = self.filter.scene.linked_bankset.id - self._bankset_model = self.filter.scene.linked_bankset - - if self._bankset_model is not None: - self._bankset_model.id_update_listeners.append(self) - - def notify_on_new_id(self, new_id: str) -> None: - self.filter.filter_configurations["set_id"] = new_id def update_node_after_settings_changed(self) -> None: if self._bankset_model is not None: self._bankset_model.id_update_listeners.remove(self) - self._update_bankset_listener() - - def __del__(self) -> None: - if self._bankset_model is not None: - self._bankset_model.id_update_listeners.remove(self) + if isinstance(self.filter, FaderUpdatingVFilter): + self.filter.update_bankset_listener() + else: + logger.error("This fader filter has not been updated yet. Please save the show file and reload it NOW!") class FaderRawNode(_FaderNode): - """Filter to represent any filter fader""" + """Filter to represent any filter fader.""" nodeName = "Raw" # noqa: N815 def __init__(self, model: Filter, name: str) -> None: - super().__init__(model=model, filter_type=FilterTypeEnumeration.FILTER_FADER_RAW, name=name, terminals={ + """Initialize node.""" + super().__init__(model=model, filter_type=FilterTypeEnumeration.VFILTER_FADER_RAW, name=name, terminals={ "primary": {"io": "out"}, "secondary": {"io": "out"}, }) @@ -71,11 +56,13 @@ def __init__(self, model: Filter, name: str) -> None: class FaderHSINode(_FaderNode): - """Filter to represent a hsi filter fader""" + """Filter to represent a hsi filter fader.""" + nodeName = "HSI" # noqa: N815 def __init__(self, model: Filter, name: str) -> None: - super().__init__(model=model, filter_type=FilterTypeEnumeration.FILTER_FADER_HSI, name=name, terminals={ + """Initialize node.""" + super().__init__(model=model, filter_type=FilterTypeEnumeration.VFILTER_FADER_HSI, name=name, terminals={ "color": {"io": "out"}, }) @@ -89,11 +76,13 @@ def __init__(self, model: Filter, name: str) -> None: class FaderHSIANode(_FaderNode): - """Filter to represent a hsia filter fader""" + """Filter to represent a hsia filter fader.""" + nodeName = "HSI-A" # noqa: N815 def __init__(self, model: Filter, name: str) -> None: - super().__init__(model=model, filter_type=FilterTypeEnumeration.FILTER_FADER_HSIA, name=name, terminals={ + """Initialize node.""" + super().__init__(model=model, filter_type=FilterTypeEnumeration.VFILTER_FADER_HSIA, name=name, terminals={ "color": {"io": "out"}, "amber": {"io": "out"}, }) @@ -109,11 +98,13 @@ def __init__(self, model: Filter, name: str) -> None: class FaderHSIUNode(_FaderNode): - """Filter to represent a hsiu filter fader""" + """Filter to represent a hsiu filter fader.""" + nodeName = "HSI_U" # noqa: N815 def __init__(self, model: Filter, name: str) -> None: - super().__init__(model=model, filter_type=FilterTypeEnumeration.FILTER_FADER_HSIU, name=name, terminals={ + """Initialize node.""" + super().__init__(model=model, filter_type=FilterTypeEnumeration.VFILTER_FADER_HSIU, name=name, terminals={ "color": {"io": "out"}, "uv": {"io": "out"}, }) @@ -129,11 +120,13 @@ def __init__(self, model: Filter, name: str) -> None: class FaderHSIAUNode(_FaderNode): - """Filter to represent a hasiau filter fader""" + """Filter to represent a hasiau filter fader.""" + nodeName = "HSI-AU" # noqa: N815 def __init__(self, model: Filter, name: str) -> None: - super().__init__(model=model, filter_type=FilterTypeEnumeration.FILTER_FADER_HSIAU, name=name, terminals={ + """Initialize node.""" + super().__init__(model=model, filter_type=FilterTypeEnumeration.VFILTER_FADER_HSIAU, name=name, terminals={ "color": {"io": "out"}, "amber": {"io": "out"}, "uv": {"io": "out"}, @@ -150,10 +143,12 @@ def __init__(self, model: Filter, name: str) -> None: class FaderMainBrightness(FilterNode): - """Filter to the main brightness fader""" + """Filter to the main brightness fader.""" + nodeName = "global-ilumination" # noqa: N815 def __init__(self, model: Filter, name: str) -> None: + """Initialize node.""" super().__init__(model=model, filter_type=FilterTypeEnumeration.FILTER_TYPE_MAIN_BRIGHTNESS, name=name, terminals={"brightness": {"io": "out"}}) diff --git a/src/view/show_mode/editor/nodes/type_to_node_dict.py b/src/view/show_mode/editor/nodes/type_to_node_dict.py index 5114ac21..80b8dc9a 100644 --- a/src/view/show_mode/editor/nodes/type_to_node_dict.py +++ b/src/view/show_mode/editor/nodes/type_to_node_dict.py @@ -98,6 +98,11 @@ from view.show_mode.editor.nodes.import_node import ImportNode type_to_node: dict[int, str] = { + FilterTypeEnumeration.VFILTER_FADER_RAW: FaderRawNode.nodeName, + FilterTypeEnumeration.VFILTER_FADER_HSI: FaderHSINode.nodeName, + FilterTypeEnumeration.VFILTER_FADER_HSIA: FaderHSIANode.nodeName, + FilterTypeEnumeration.VFILTER_FADER_HSIU: FaderHSIUNode.nodeName, + FilterTypeEnumeration.VFILTER_FADER_HSIAU: FaderHSIAUNode.nodeName, FilterTypeEnumeration.VFILTER_SEQUENCER: SequencerNode.nodeName, FilterTypeEnumeration.VFILTER_COLOR_MIXER: ColorMixerVFilterNode.nodeName, FilterTypeEnumeration.VFILTER_IMPORT: ImportNode.nodeName,