From d83aa91f851e4f7c98929aecf6fc79b192874ae9 Mon Sep 17 00:00:00 2001 From: CorsCodini <45790567+CorsCodini@users.noreply.github.com> Date: Thu, 31 Jul 2025 23:51:15 +0200 Subject: [PATCH 01/29] add: save color and uuid on showfile --- src/controller/file/read.py | 3 + .../serializing/universe_serialization.py | 83 ++++++++++++------- src/model/ofl/fixture.py | 22 +++-- 3 files changed, 69 insertions(+), 39 deletions(-) diff --git a/src/controller/file/read.py b/src/controller/file/read.py index e3597b59..56b7cc3a 100644 --- a/src/controller/file/read.py +++ b/src/controller/file/read.py @@ -3,6 +3,7 @@ import os import xml.etree.ElementTree as ET from logging import getLogger +from uuid import UUID import xmlschema from defusedxml.ElementTree import parse @@ -600,6 +601,8 @@ def _parse_patching(board_configuration: BoardConfiguration, location_element: E int(child.attrib["mode"]), universe_id, int(child.attrib["start"]), + UUID(child.attrib.get("id")) if child.attrib.get("id") else None, + child.attrib.get("color"), ) # TODO load fixture name from file diff --git a/src/controller/file/serializing/universe_serialization.py b/src/controller/file/serializing/universe_serialization.py index a31db710..e950881b 100644 --- a/src/controller/file/serializing/universe_serialization.py +++ b/src/controller/file/serializing/universe_serialization.py @@ -14,11 +14,15 @@ def _create_universe_element(universe: Universe, parent: ET.Element) -> ET.Eleme ... """ - return ET.SubElement(parent, "universe", attrib={ - "id": str(universe.id), - "name": str(universe.name), - "description": str(universe.description), - }) + return ET.SubElement( + parent, + "universe", + attrib={ + "id": str(universe.id), + "name": str(universe.name), + "description": str(universe.description), + }, + ) def _create_physical_location_element(physical: int, parent: ET.Element) -> ET.Element: @@ -31,36 +35,45 @@ def _create_physical_location_element(physical: int, parent: ET.Element) -> ET.E return physical_location -def _create_artnet_location_element(artnet_location: proto.UniverseControl_pb2.Universe.ArtNet, - parent: ET.Element) -> ET.Element: +def _create_artnet_location_element( + artnet_location: proto.UniverseControl_pb2.Universe.ArtNet, parent: ET.Element +) -> ET.Element: """Creates an xml element of type artnet_location. """ - return ET.SubElement(parent, "artnet_location", attrib={ - "ip_address": str(artnet_location.ip_address), - "udp_port": str(artnet_location.port), - "device_universe_id": str(artnet_location.universe_on_device), - }) - - -def _create_ftdi_location_element(ftdi_location: proto.UniverseControl_pb2.Universe.USBConfig, - parent: ET.Element) -> ET.Element: + return ET.SubElement( + parent, + "artnet_location", + attrib={ + "ip_address": str(artnet_location.ip_address), + "udp_port": str(artnet_location.port), + "device_universe_id": str(artnet_location.universe_on_device), + }, + ) + + +def _create_ftdi_location_element( + ftdi_location: proto.UniverseControl_pb2.Universe.USBConfig, parent: ET.Element +) -> ET.Element: """Creates a xml element of type ftdi_location. """ # vendor_id=0x0403, product_id=0x6001 - return ET.SubElement(parent, "ftdi_location", attrib={ - "vendor_id": str(ftdi_location.vendor_id), - "product_id": str(ftdi_location.product_id), - "device_name": str(ftdi_location.device_name), - "serial_identifier": str(ftdi_location.serial), - }) - - -def _create_fixture_element(fixture: UsedFixture, patching_element: ET.Element, - assemble_for_fish: bool) -> None: + return ET.SubElement( + parent, + "ftdi_location", + attrib={ + "vendor_id": str(ftdi_location.vendor_id), + "product_id": str(ftdi_location.product_id), + "device_name": str(ftdi_location.device_name), + "serial_identifier": str(ftdi_location.serial), + }, + ) + + +def _create_fixture_element(fixture: UsedFixture, patching_element: ET.Element, assemble_for_fish: bool) -> None: """ add patching information of a fixture to the show file XML structure. :param fixture: The Fixture to add @@ -69,9 +82,15 @@ def _create_fixture_element(fixture: UsedFixture, patching_element: ET.Element, """ if assemble_for_fish: return - ET.SubElement(patching_element, "fixture", attrib={ - # Todo add to doku "universe": str(fixture.universe_id), - "start": str(fixture.start_index), - "fixture_file": fixture.fixture_file, - "mode": str(fixture.mode_index), - }) + ET.SubElement( + patching_element, + "fixture", + attrib={ + # Todo add to doku "universe": str(fixture.universe_id), + "id": str(fixture.uuid), + "start": str(fixture.start_index), + "fixture_file": fixture.fixture_file, + "mode": str(fixture.mode_index), + "color": fixture.color_on_stage.name(), + }, + ) diff --git a/src/model/ofl/fixture.py b/src/model/ofl/fixture.py index 573b8f94..c27f727d 100644 --- a/src/model/ofl/fixture.py +++ b/src/model/ofl/fixture.py @@ -12,6 +12,7 @@ import numpy as np from PySide6 import QtCore +from PySide6.QtGui import QColor from model.patching.fixture_channel import FixtureChannel, FixtureChannelType @@ -149,8 +150,9 @@ def __init__( parent_universe: int, start_index: int, uuid: UUID | None = None, - color: str | None = None, + color_on_stage: str | None = None, ) -> None: + """Fixture in use with a specific mode""" super().__init__() self._board_configuration: Final[BoardConfiguration] = board_configuration self._fixture: Final[Fixture] = fixture @@ -166,8 +168,8 @@ def __init__( self._segment_map: dict[FixtureChannelType, NDArray[np.int_]] = segment_map self._color_support: Final[ColorSupport] = color_support - self._color_on_stage: str = ( - color if color else "#" + "".join([random.choice("0123456789ABCDEF") for _ in range(6)]) # noqa: S311 not a secret + self._color_on_stage: QColor = QColor( + color_on_stage if color_on_stage else "#" + "".join([random.choice("0123456789ABCDEF") for _ in range(6)]) # noqa: S311 not a secret ) self._name_on_stage: str = self.short_name if self.short_name else self.name @@ -239,13 +241,13 @@ def fixture_channels(self) -> tuple[FixtureChannel, ...]: return tuple(self._fixture_channels) @property - def color_on_stage(self) -> str: + def color_on_stage(self) -> QColor: """color of the fixture on stage""" return self._color_on_stage @color_on_stage.setter def color_on_stage(self, color: str) -> None: - self._color_on_stage = color + self._color_on_stage = QColor(color) self.static_data_changed.emit() @property @@ -301,7 +303,13 @@ def get_fixture_channel(self, index: int) -> FixtureChannel: def make_used_fixture( - board_configuration: BoardConfiguration, fixture: Fixture, mode_index: int, universe_id: int, start_index: int + board_configuration: BoardConfiguration, + fixture: Fixture, + mode_index: int, + universe_id: int, + start_index: int, + uuid: UUID | None = None, + color: str | None = None, ) -> UsedFixture: """generate a new Used Fixture from a fixture""" - return UsedFixture(board_configuration, fixture, mode_index, universe_id, start_index) + return UsedFixture(board_configuration, fixture, mode_index, universe_id, start_index, uuid, color) From 035cd3efa86c48f9bef92723709b3fa10a9ac87c Mon Sep 17 00:00:00 2001 From: CorsCodini <45790567+CorsCodini@users.noreply.github.com> Date: Sat, 2 Aug 2025 17:08:21 +0200 Subject: [PATCH 02/29] update submodules --- submodules/docs | 2 +- submodules/resources | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/submodules/docs b/submodules/docs index 3075205e..0dfa250a 160000 --- a/submodules/docs +++ b/submodules/docs @@ -1 +1 @@ -Subproject commit 3075205ea28b7ca999aeb8cb64b56f5f64af93a3 +Subproject commit 0dfa250a78d14f2bfe80b678021a49a5a5ae93a7 diff --git a/submodules/resources b/submodules/resources index 9df13559..b2d72dd3 160000 --- a/submodules/resources +++ b/submodules/resources @@ -1 +1 @@ -Subproject commit 9df1355968c6ef9d3b21555f3cdbac8245b96754 +Subproject commit b2d72dd3958ebae00245be69380248124c2b15eb From 8af95ba245ddbf5fb62473e58dc69ba8411176f4 Mon Sep 17 00:00:00 2001 From: CorsCodini <45790567+CorsCodini@users.noreply.github.com> Date: Fri, 8 Aug 2025 17:12:24 +0200 Subject: [PATCH 03/29] chg: only emmit when changed --- src/model/ofl/fixture.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/model/ofl/fixture.py b/src/model/ofl/fixture.py index 5f5c5352..57233603 100644 --- a/src/model/ofl/fixture.py +++ b/src/model/ofl/fixture.py @@ -138,6 +138,12 @@ def start_index(self) -> int: """Start index of theFixture in the Universe indexed by 0.""" return self._start_index + @start_index.setter + def start_index(self, start_index: int) -> None: + if start_index != self._start_index: + self._start_index = start_index + self.static_data_changed.emit() + @property def fixture_file(self) -> str: """File of the fixture.""" @@ -178,9 +184,10 @@ def color_on_stage(self) -> QColor: return self._color_on_stage @color_on_stage.setter - def color_on_stage(self, color: str) -> None: - self._color_on_stage = QColor(color) - self.static_data_changed.emit() + def color_on_stage(self, color: QColor) -> None: + if color != self._color_on_stage: + self._color_on_stage = color + self.static_data_changed.emit() @property def name_on_stage(self) -> str: @@ -189,8 +196,9 @@ def name_on_stage(self) -> str: @name_on_stage.setter def name_on_stage(self, name: str) -> None: - self._name_on_stage = name - self.static_data_changed.emit() + if name != self._name_on_stage: + self._name_on_stage = name + self.static_data_changed.emit() @property def color_support(self) -> ColorSupport: From ab2dd3e1296147d4e4b2593beef42414e438696b Mon Sep 17 00:00:00 2001 From: CorsCodini <45790567+CorsCodini@users.noreply.github.com> Date: Fri, 8 Aug 2025 17:13:58 +0200 Subject: [PATCH 04/29] add: Fixture Dialog --- src/view/dialogs/fixture_dialog.py | 90 ++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 src/view/dialogs/fixture_dialog.py diff --git a/src/view/dialogs/fixture_dialog.py b/src/view/dialogs/fixture_dialog.py new file mode 100644 index 00000000..53cb7454 --- /dev/null +++ b/src/view/dialogs/fixture_dialog.py @@ -0,0 +1,90 @@ +"""Dialog for editing Fixtures.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from PySide6.QtWidgets import ( + QColorDialog, + QDialog, + QGridLayout, + QHBoxLayout, + QLabel, + QLineEdit, + QPushButton, + QVBoxLayout, +) + +if TYPE_CHECKING: + from PySide6.QtGui import QColor + from PySide6.QtWidgets import QWidget + + from model.ofl.fixture import UsedFixture + + +class FixtureDialog(QDialog): + """Dialog for editing Fixtures.""" + + def __init__(self, fixture: UsedFixture, parent: QWidget = None) -> None: + super().__init__(parent) + self._fixture: UsedFixture = fixture + + layout = QVBoxLayout() + + layout_fixture = QGridLayout() + layout_fixture.addWidget(QLabel("Fixture name:"), 0, 0) + layout_fixture.addWidget(QLabel(self._fixture.short_name), 0, 1) + + layout_fixture.addWidget(QLabel("Anzeigename"), 1, 0) + self._name_on_stage = QLineEdit(self._fixture.name_on_stage) + layout_fixture.addWidget(self._name_on_stage, 1, 1) + + layout_fixture.addWidget(QLabel("Start Index"), 2, 0) + self._start_index = QLineEdit(str(self._fixture.start_index + 1)) + layout_fixture.addWidget(self._start_index, 2, 1) + + self._color_label = QLabel("Anzeigefarbe") + self._selected_color = self._fixture.color_on_stage + self._color_label.setStyleSheet(f"background-color: {self._fixture.color_on_stage.name()};") + layout_fixture.addWidget(self._color_label, 3, 0) + color_button = QPushButton("Farbe wählen") + color_button.clicked.connect(self._open_color_picker) + layout_fixture.addWidget(color_button, 3, 1) + + layout_exit = QHBoxLayout() + _ok_button = QPushButton() + _ok_button.setText("Okay") + _ok_button.clicked.connect(self._ok) + _cancel_button = QPushButton() + _cancel_button.setText("cancel") + _cancel_button.clicked.connect(self._cancel) + layout_exit.addWidget(_cancel_button) + layout_exit.addWidget(_ok_button) + + layout.addLayout(layout_fixture) + layout.addLayout(layout_exit) + self.setLayout(layout) + + def _ok(self) -> None: + """Handle OK clicked.""" + self._fixture.color_on_stage = self._selected_color + self._fixture.start_index = int(self._start_index.text()) - 1 + self._fixture.name_on_stage = self._name_on_stage.text() + self.accept() + + def _cancel(self) -> None: + """Handle Cancel clicked.""" + self.reject() + + def _open_color_picker(self) -> None: + """Open Color picker.""" + dialog = QColorDialog(self) + dialog.setOption(QColorDialog.ColorDialogOption.DontUseNativeDialog, True) + + dialog.colorSelected.connect(self._color_chosen) + dialog.open() + + def _color_chosen(self, color: QColor) -> None: + """Handle color chosen.""" + self._selected_color = color + self._color_label.setStyleSheet(f"background-color: {color.name()};") From 83460205b8f6cd6c2e745b9b1019dd90a7f51be4 Mon Sep 17 00:00:00 2001 From: CorsCodini <45790567+CorsCodini@users.noreply.github.com> Date: Fri, 8 Aug 2025 17:15:54 +0200 Subject: [PATCH 05/29] add: PatchBaseItem --- .../patch_view/patch_plan/patch_base_item.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/view/patch_view/patch_plan/patch_base_item.py diff --git a/src/view/patch_view/patch_plan/patch_base_item.py b/src/view/patch_view/patch_plan/patch_base_item.py new file mode 100644 index 00000000..ac5bda84 --- /dev/null +++ b/src/view/patch_view/patch_plan/patch_base_item.py @@ -0,0 +1,40 @@ +"""QGraphicsItem for patching items.""" + +import math +from typing import override + +from PySide6.QtCore import QRectF +from PySide6.QtWidgets import QGraphicsItem + +from model.universe import NUMBER_OF_CHANNELS +from view.patch_view.patch_plan.channel_item_generator import ( + channel_item_height, + channel_item_spacing, + channel_item_width, +) + + +class PatchBaseItem(QGraphicsItem): + """QGraphicsItem for patching items.""" + + _cols = 1 + _rows = 1 + _view_width = 100 + + def __init__(self) -> None: + """Initialize.""" + super().__init__() + + @classmethod + def resize(cls, view_width: int) -> None: + """Handle resize the view.""" + cls._view_width = view_width + new_cols = max(1, cls._view_width // (channel_item_width() + channel_item_spacing())) + if new_cols != cls._cols: + cls._cols = new_cols + cls._rows = math.ceil(NUMBER_OF_CHANNELS / new_cols) + + @override + def boundingRect(self) -> QRectF: + """Bounding rectangle of this item.""" + return QRectF(0, 0, self._view_width, self._rows * (channel_item_height() + channel_item_spacing())) From c798b1730f8a306d49ae5e75f267d933e20fb671 Mon Sep 17 00:00:00 2001 From: CorsCodini <45790567+CorsCodini@users.noreply.github.com> Date: Fri, 8 Aug 2025 17:30:10 +0200 Subject: [PATCH 06/29] Doku --- src/controller/file/read.py | 180 ++++++++++++++++++++++-------------- 1 file changed, 111 insertions(+), 69 deletions(-) diff --git a/src/controller/file/read.py b/src/controller/file/read.py index 56b7cc3a..684f4b63 100644 --- a/src/controller/file/read.py +++ b/src/controller/file/read.py @@ -1,4 +1,4 @@ -"""Handles reading a xml document""" +"""Handle reading a xml document.""" import os import xml.etree.ElementTree as ET @@ -29,10 +29,12 @@ def _parse_and_add_bankset(child: ET.Element, loaded_banksets: dict[str, BankSet]) -> None: - """ - Parse and add a bank set to the show file. - :param child: The XML element to examine - :param loaded_banksets: The list to add the bankset to. + """Parse and add a bank set to the show file. + + Args: + child: The XML element to examine. + loaded_banksets: The list to add the bank set to. + """ _id = child.attrib.get("id") bs: BankSet = BankSet(gui_controlled=True, id_=_id) @@ -69,15 +71,16 @@ def _parse_and_add_bankset(child: ET.Element, loaded_banksets: dict[str, BankSet def read_document(file_name: str, board_configuration: BoardConfiguration) -> bool: - """Parses the specified file to a board configuration data model. + """Parse the specified file to a board configuration data model. Args: - file_name: The path to the file to be parsed. + file_name: The path to the file to parse. + board_configuration: The current BoardConfiguration. Returns: A BoardConfiguration instance parsed from the provided file. - """ + """ pn = get_process_notifier("Load Showfile", 5) try: @@ -160,10 +163,14 @@ def read_document(file_name: str, board_configuration: BoardConfiguration) -> bo def lcd_color_from_string(display_color: str) -> proto.Console_pb2.lcd_color: - """ - Convert the string representation of the LCD backlight color to the enum. - :param display_color: The string representation - :returns: The enum representation + """Convert the string representation of the LCD backlight color to the enum. + + Args: + display_color: The string representation. + + Returns: + The enum representation. + """ match display_color: case "white": @@ -187,18 +194,20 @@ def lcd_color_from_string(display_color: str) -> proto.Console_pb2.lcd_color: def _clean_tags(element: ET.Element, prefix: str) -> None: - """This method recursively cleans up immediate XML tag prefixes.""" + """Recursively clean up immediate XML tag prefixes.""" for child in element: child.tag = child.tag.replace(prefix, "") _clean_tags(child, prefix) def _parse_filter_page(element: ET.Element, parent_scene: Scene, instantiated_pages: list[FilterPage]) -> bool: - """ - Load a filter page from the XML representation. - :param element: The XML element to load the data from - :param parent_scene: The scene to add the page to - :param instantiated_pages: The list of all loaded filter pages, where this element is appended to + """Load a filter page from the XML representation. + + Args: + element: The XML element to load the data from. + parent_scene: The scene to add the page to. + instantiated_pages: The list of all loaded filter pages, to which this element is appended. + """ f = FilterPage(parent_scene) for key, value in element.attrib.items(): @@ -242,11 +251,13 @@ 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: - """ - Load a scene from the show file data structure. - :param scene_element: The XML element to use - :param board_configuration: The show configuration object to insert the scene into. - :param loaded_banksets: A list of bank sets that are associated with the scene. + """Load a scene from the show file data structure. + + Args: + scene_element: The XML element to use. + board_configuration: The show configuration object to insert the scene into. + loaded_banksets: A list of bank sets associated with the scene. + """ human_readable_name = "" scene_id = 0 @@ -298,10 +309,12 @@ def _parse_scene( def _append_ui_page(page_def: ET.Element, scene: Scene) -> None: - """ - Load a UI page (the ones that contain the widgets) from the XML data. - :param page_def: The XML data structure - :param scene: The scene to add it to. + """Load a UI page (containing widgets) from XML data. + + Args: + page_def: The XML data structure. + scene: The scene to add it to. + """ page = UIPage(scene) for k, v in page_def.attrib.items(): @@ -365,10 +378,12 @@ def _append_ui_page(page_def: ET.Element, scene: Scene) -> None: def _parse_filter(filter_element: ET.Element, scene: Scene) -> None: - """ - Load a filter from the XML definition. - :param filter_element: THe XML data to load the filter from - :param scene: The scene to append the filter to + """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. + """ filter_id = "" filter_type = 0 @@ -409,10 +424,12 @@ def _parse_filter(filter_element: ET.Element, scene: Scene) -> None: def _parse_channel_link(initial_parameters_element: ET.Element, filter_: Filter) -> None: - """ - Load a connection between two filters. - :param initial_parameters_element: The XML element describing the connection. - :param filter_: The parent filter (whose input this is) to attach the connection to. + """Load a connection between two filters. + + Args: + initial_parameters_element: The XML element describing the connection. + filter_: The parent filter (whose input this is) to attach the connection to. + """ cl_key = "" cl_value = "" @@ -429,10 +446,12 @@ def _parse_channel_link(initial_parameters_element: ET.Element, filter_: Filter) def _parse_initial_parameters(initial_parameters_element: ET.Element, filter_: Filter) -> None: - """ - Load the parameters of a filter. - :param initial_parameters_element: The XML definition to load the parameters from - :param filter_: The filter whose parameters these are. + """Load the parameters of a filter. + + Args: + initial_parameters_element: The XML definition to load the parameters from. + filter_: The filter whose parameters these are. + """ ip_key = "" ip_value = "" @@ -451,11 +470,13 @@ def _parse_initial_parameters(initial_parameters_element: ET.Element, filter_: F def _parse_filter_configuration(filter_configuration_element: ET.Element, filter_: Filter, fc: dict[str, str]) -> None: - """ - Load the configuration of a filter. - :param filter_configuration_element: The XML data to load the configuration from - :param filter_: The filter which the configuration belongs to - :param fc: The existing configuration to append to + """Load the configuration of a filter. + + Args: + filter_configuration_element: The XML data to load the configuration from. + filter_: The filter to which the configuration belongs. + fc: The existing configuration to append to. + """ fc_key = "" fc_value = "" @@ -477,10 +498,12 @@ def _parse_filter_configuration(filter_configuration_element: ET.Element, filter def _parse_universe(universe_element: ET.Element, board_configuration: BoardConfiguration) -> None: - """ - Load a universe description from XML data. - :param universe_element: The XML data to use. - :param board_configuration: The show to register the universe with. + """Load a universe description from XML data. + + Args: + universe_element: The XML data to use. + board_configuration: The show to register the universe with. + """ universe_id = None name = "" @@ -524,19 +547,27 @@ def _parse_universe(universe_element: ET.Element, board_configuration: BoardConf def _parse_physical_location(location_element: ET.Element) -> int: - """ - Parse a universe definition for one attached directly to the IO mainboard. - :param location_element: The XML data to load from - :returns: The location + """Parse a universe definition for one attached directly to the IO mainboard. + + Args: + location_element: The XML data to load from. + + Returns: + The location. + """ return int(location_element.text) def _parse_artnet_location(location_element: ET.Element) -> proto.UniverseControl_pb2.Universe.ArtNet: - """ - Parse a universe definition of an ArtNet stage box. - :param location_element: The XML data to load from - :returns: An ArtNet universe location + """Parse a universe definition of an ArtNet stage box. + + Args: + location_element: The XML data to load from. + + Returns: + An ArtNet universe location. + """ device_universe_id = 0 ip_address = "" @@ -558,10 +589,14 @@ def _parse_artnet_location(location_element: ET.Element) -> proto.UniverseContro def _parse_ftdi_location(location_element: ET.Element) -> proto.UniverseControl_pb2.Universe.USBConfig: - """ - Load a universe location definition of an USB DMX adapter. - :param location_element: The XML data to load from - :returns: THe loaded connection details + """Load a universe location definition of a USB DMX adapter. + + Args: + location_element: The XML data to load from. + + Returns: + The loaded connection details. + """ product_id = 0 vendor_id = 0 @@ -586,11 +621,16 @@ def _parse_ftdi_location(location_element: ET.Element) -> proto.UniverseControl_ def _parse_patching(board_configuration: BoardConfiguration, location_element: ET.Element, universe_id: int) -> None: - """ - Load patching information from XML data. - :param location_element: The XML data to load from - :param universe_id: The id of the universe which this fixture belongs to. - :returns: The loaded fixtures + """Load patching information from XML data. + + Args: + board_configuration: The current BoardConfiguration. + location_element: The XML data to load from. + universe_id: The ID of the universe this fixture belongs to. + + Returns: + The loaded fixtures. + """ fixtures_path = "/var/cache/missionDMX/fixtures" # TODO config file @@ -609,10 +649,12 @@ def _parse_patching(board_configuration: BoardConfiguration, location_element: E def _parse_ui_hint(ui_hint_element: ET.Element, board_configuration: BoardConfiguration) -> None: - """ - Load general configuration data. - :param ui_hint_element: The XML representation to load from - :param board_configuration: THe show file to apply the settings on. + """Load general configuration data. + + Args: + ui_hint_element: The XML representation to load from. + board_configuration: The show file to apply the settings on. + """ ui_hint_key = "" ui_hint_value = "" From 3ddff9c07cac9360a65d0c4a0c8e8b941e3203f5 Mon Sep 17 00:00:00 2001 From: CorsCodini <45790567+CorsCodini@users.noreply.github.com> Date: Fri, 8 Aug 2025 17:30:28 +0200 Subject: [PATCH 07/29] Doku --- src/model/ofl/fixture.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/model/ofl/fixture.py b/src/model/ofl/fixture.py index 57233603..f3a6b649 100644 --- a/src/model/ofl/fixture.py +++ b/src/model/ofl/fixture.py @@ -38,6 +38,7 @@ class ColorSupport(IntFlag): HAS_UV_SEGMENT = 16 def __str__(self) -> str: + """Return the string representation of ColorSupport.""" if self == ColorSupport.NO_COLOR_SUPPORT: return "No Color Support" s = [] @@ -55,7 +56,7 @@ def __str__(self) -> str: def load_fixture(file: str) -> OflFixture: - """load fixture from OFL JSON""" + """Load fixture from OFL JSON.""" with open(file, "r", encoding="UTF-8") as f: ob: dict = json.load(f) ob.update({"fileName": file.split("/fixtures/")[1]}) @@ -77,7 +78,7 @@ def __init__( uuid: UUID | None = None, color_on_stage: str | None = None, ) -> None: - """Fixture in use with a specific mode""" + """Fixture in use with a specific mode.""" super().__init__() self._board_configuration: Final[BoardConfiguration] = board_configuration self._fixture: Final[OflFixture] = fixture @@ -103,14 +104,12 @@ def __init__( @property def uuid(self) -> UUID: - """uuid of the fixture""" + """UUID of the fixture.""" return self._uuid @property def power(self) -> float: - """ - Fixture maximum continuous power draw (not accounting for capacitor charging as well as lamp warmup) in W. - """ + """Fixture maximum continuous power draw (not accounting for capacitor charging as well as lamp warmup) in W.""" return self._fixture.physical.power @property From 11d5532b16425781cc35bb967cf2bf8bead46226 Mon Sep 17 00:00:00 2001 From: CorsCodini <45790567+CorsCodini@users.noreply.github.com> Date: Fri, 8 Aug 2025 17:44:58 +0200 Subject: [PATCH 08/29] Doku --- src/view/dialogs/fixture_dialog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/view/dialogs/fixture_dialog.py b/src/view/dialogs/fixture_dialog.py index 53cb7454..caafb34d 100644 --- a/src/view/dialogs/fixture_dialog.py +++ b/src/view/dialogs/fixture_dialog.py @@ -26,6 +26,7 @@ class FixtureDialog(QDialog): """Dialog for editing Fixtures.""" def __init__(self, fixture: UsedFixture, parent: QWidget = None) -> None: + """Dialog for editing Fixtures.""" super().__init__(parent) self._fixture: UsedFixture = fixture From 5efb8b6cf80ba89e26190e4163f4a6ae0c845eb7 Mon Sep 17 00:00:00 2001 From: CorsCodini <45790567+CorsCodini@users.noreply.github.com> Date: Fri, 8 Aug 2025 18:12:16 +0200 Subject: [PATCH 09/29] add: fixture Modifying --- src/view/dialogs/fixture_dialog.py | 40 ++++++-- .../patch_plan/channel_item_generator.py | 21 +++-- .../patch_plan/patch_plan_selector.py | 39 ++++---- .../patch_plan/patch_plan_widget.py | 86 +++++++---------- .../patch_plan/used_fixture_widget.py | 94 ++++++++++++++----- 5 files changed, 174 insertions(+), 106 deletions(-) diff --git a/src/view/dialogs/fixture_dialog.py b/src/view/dialogs/fixture_dialog.py index caafb34d..aa325aad 100644 --- a/src/view/dialogs/fixture_dialog.py +++ b/src/view/dialogs/fixture_dialog.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING +import numpy as np from PySide6.QtWidgets import ( QColorDialog, QDialog, @@ -12,23 +13,28 @@ QLabel, QLineEdit, QPushButton, + QSpinBox, QVBoxLayout, ) +from model.universe import NUMBER_OF_CHANNELS + if TYPE_CHECKING: from PySide6.QtGui import QColor from PySide6.QtWidgets import QWidget + from model import BoardConfiguration from model.ofl.fixture import UsedFixture class FixtureDialog(QDialog): """Dialog for editing Fixtures.""" - def __init__(self, fixture: UsedFixture, parent: QWidget = None) -> None: + def __init__(self, fixture: UsedFixture, board_configuration: BoardConfiguration, parent: QWidget = None) -> None: """Dialog for editing Fixtures.""" super().__init__(parent) self._fixture: UsedFixture = fixture + self._board_configuration: BoardConfiguration = board_configuration layout = QVBoxLayout() @@ -41,7 +47,11 @@ def __init__(self, fixture: UsedFixture, parent: QWidget = None) -> None: layout_fixture.addWidget(self._name_on_stage, 1, 1) layout_fixture.addWidget(QLabel("Start Index"), 2, 0) - self._start_index = QLineEdit(str(self._fixture.start_index + 1)) + self._start_index = QSpinBox() + self._start_index.setMinimum(0) + self._start_index.setMaximum(NUMBER_OF_CHANNELS) + self._start_index.setValue(self._fixture.start_index + 1) + self._start_index.textChanged.connect(self._validate_input) layout_fixture.addWidget(self._start_index, 2, 1) self._color_label = QLabel("Anzeigefarbe") @@ -52,17 +62,22 @@ def __init__(self, fixture: UsedFixture, parent: QWidget = None) -> None: color_button.clicked.connect(self._open_color_picker) layout_fixture.addWidget(color_button, 3, 1) + layout_error = QHBoxLayout() + self._error_label = QLabel("No Error Found!") + layout_error.addWidget(self._error_label) + layout_exit = QHBoxLayout() - _ok_button = QPushButton() - _ok_button.setText("Okay") - _ok_button.clicked.connect(self._ok) + self._ok_button = QPushButton() + self._ok_button.setText("Okay") + self._ok_button.clicked.connect(self._ok) _cancel_button = QPushButton() _cancel_button.setText("cancel") _cancel_button.clicked.connect(self._cancel) layout_exit.addWidget(_cancel_button) - layout_exit.addWidget(_ok_button) + layout_exit.addWidget(self._ok_button) layout.addLayout(layout_fixture) + layout.addLayout(layout_error) layout.addLayout(layout_exit) self.setLayout(layout) @@ -89,3 +104,16 @@ def _color_chosen(self, color: QColor) -> None: """Handle color chosen.""" self._selected_color = color self._color_label.setStyleSheet(f"background-color: {color.name()};") + + def _validate_input(self) -> None: + """Validate input.""" + self._ok_button.setEnabled(False) + occupied = np.arange( + int(self._start_index.value() - 1), int(self._start_index.value() - 1) + self._fixture.channel_length + ) + + if np.isin(occupied, self._board_configuration.get_occupied_channels(self._fixture.universe_id)).any(): + self._error_label.setText("Channels already occupied!") + return + self._error_label = QLabel("No Error Found!") + self._ok_button.setEnabled(True) diff --git a/src/view/patch_view/patch_plan/channel_item_generator.py b/src/view/patch_view/patch_plan/channel_item_generator.py index 01a53c34..6a20fa82 100644 --- a/src/view/patch_view/patch_plan/channel_item_generator.py +++ b/src/view/patch_view/patch_plan/channel_item_generator.py @@ -1,22 +1,29 @@ -""" item of the Patching """ +"""Channel items of the Patching.""" + from PySide6.QtGui import QColor, QColorConstants, QFont, QPainter, QPixmap _WIDTH: int = 100 _HEIGHT: int = 100 +_SPACING: int = 1 -def item_width() -> int: - """width of the item""" +def channel_item_width() -> int: + """Width of one Channel item.""" return _WIDTH -def item_height() -> int: - """height of the item""" +def channel_item_height() -> int: + """Height of one Channel item.""" return _HEIGHT -def create_item(number: int, color: QColor = QColorConstants.White) -> QPixmap: - """creates a pixmap of the item""" +def channel_item_spacing() -> int: + """Spacing between two Channel items.""" + return _SPACING + + +def create_item(number: int, color: QColor) -> QPixmap: + """Creates a pixmap of a Channel item.""" pixmap = QPixmap(_WIDTH, _HEIGHT) pixmap.fill(color) diff --git a/src/view/patch_view/patch_plan/patch_plan_selector.py b/src/view/patch_view/patch_plan/patch_plan_selector.py index 5861aa4b..cea459ac 100644 --- a/src/view/patch_view/patch_plan/patch_plan_selector.py +++ b/src/view/patch_view/patch_plan/patch_plan_selector.py @@ -1,27 +1,31 @@ """selector for Patching witch holds all Patching Universes""" + +from __future__ import annotations + from logging import getLogger from typing import TYPE_CHECKING, override from PySide6 import QtCore, QtGui, QtWidgets -from PySide6.QtGui import QContextMenuEvent -from PySide6.QtWidgets import QScrollArea +from PySide6.QtWidgets import QGraphicsScene from model import BoardConfiguration, Universe from model.broadcaster import Broadcaster -from model.ofl.fixture import UsedFixture from view.dialogs.universe_dialog import UniverseDialog -from view.patch_view.patch_plan.patch_plan_widget import PatchPlanWidget +from view.patch_view.patch_plan.patch_plan_widget import AutoResizeView, PatchPlanWidget +from view.patch_view.patch_plan.used_fixture_widget import UsedFixtureWidget if TYPE_CHECKING: - from view.patch_view.patch_mode import PatchMode + from PySide6.QtGui import QContextMenuEvent + from model.ofl.fixture import UsedFixture + from view.patch_view.patch_mode import PatchMode logger = getLogger(__name__) class PatchPlanSelector(QtWidgets.QTabWidget): """selector for Patching witch holds all Patching Universes""" - def __init__(self, board_configuration: BoardConfiguration, parent: "PatchMode") -> None: + def __init__(self, board_configuration: BoardConfiguration, parent: PatchMode) -> None: super().__init__(parent=parent) self._board_configuration = board_configuration self._broadcaster = Broadcaster() @@ -29,7 +33,7 @@ def __init__(self, board_configuration: BoardConfiguration, parent: "PatchMode") self._broadcaster.delete_universe.connect(self._remove_universe) self._broadcaster.add_fixture.connect(self._add_fixture) - self._patch_planes: dict[int, PatchPlanWidget] = {} + self._patch_planes: dict[int, AutoResizeView] = {} self.setTabPosition(QtWidgets.QTabWidget.TabPosition.West) self.addTab(QtWidgets.QWidget(), "+") @@ -38,8 +42,7 @@ def __init__(self, board_configuration: BoardConfiguration, parent: "PatchMode") self.tabBar().setCurrentIndex(0) def _add_fixture(self, fixture: UsedFixture) -> None: - widget: PatchPlanWidget = self._patch_planes[fixture.parent_universe] - widget.add_fixture(fixture) + self._patch_planes[fixture.universe_id].scene().addItem(UsedFixtureWidget(fixture, self._board_configuration)) def _generate_universe(self) -> None: """add a new Universe to universe Selector""" @@ -73,14 +76,16 @@ def _rename_universe(self, index: int) -> None: def _add_universe(self, universe: Universe) -> None: index = self.tabBar().count() - 1 - patch_plan = QScrollArea() - patch_plan.setWidgetResizable(True) - patch_plan.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOn) - patch_plan.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) - widget = PatchPlanWidget() - patch_plan.setWidget(widget) - self._patch_planes.update({universe.id: widget}) - self.insertTab(index, patch_plan, str(universe.name)) + scene = QGraphicsScene(self) + view = AutoResizeView(scene) + view.setRenderHints(view.renderHints() | QtGui.QPainter.RenderHint.Antialiasing) + view.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOn) + view.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + + background = PatchPlanWidget() + scene.addItem(background) + self._patch_planes.update({universe.id: view}) + self.insertTab(index, view, str(universe.name)) def _remove_universe(self, universe: Universe) -> None: del self._patch_planes[universe.id] diff --git a/src/view/patch_view/patch_plan/patch_plan_widget.py b/src/view/patch_view/patch_plan/patch_plan_widget.py index ce62b388..fc52c101 100644 --- a/src/view/patch_view/patch_plan/patch_plan_widget.py +++ b/src/view/patch_view/patch_plan/patch_plan_widget.py @@ -1,66 +1,48 @@ """patch Plan Widget for one Universe""" -import math -from typing import override -from PySide6.QtGui import QPainter, QPaintEvent, QPixmap, QResizeEvent -from PySide6.QtWidgets import QWidget +from typing import Final, override -from model.ofl.fixture import UsedFixture -from model.universe import NUMBER_OF_CHANNELS -from view.patch_view.patch_plan.channel_item_generator import create_item, item_height, item_width -from view.patch_view.patch_plan.used_fixture_widget import UsedFixtureWidget +from PySide6.QtGui import QColorConstants, QPainter, QPixmap, QResizeEvent +from PySide6.QtWidgets import QGraphicsView, QStyleOptionGraphicsItem, QWidget +from model.universe import NUMBER_OF_CHANNELS +from view.patch_view.patch_plan.channel_item_generator import ( + channel_item_height, + channel_item_spacing, + channel_item_width, + create_item, +) +from view.patch_view.patch_plan.patch_base_item import PatchBaseItem -class PatchPlanWidget(QWidget): - """Patch Plan Widget for one Universe""" - def __init__(self) -> None: - super().__init__() - self._chanel_items: list[QPixmap] = [] - self._cols = 1 - self._init_items() - self._fixtures: list[UsedFixtureWidget] = [] +class AutoResizeView(QGraphicsView): + """View, automatically resizes scene.""" - def _init_items(self) -> None: - """initiate Channel Items""" - for i in range(1, 513): - pixmap = create_item(i) - self._chanel_items.append(pixmap) + _base_item = PatchBaseItem() @override - def paintEvent(self, _: QPaintEvent) -> None: - """paint the widget""" - painter = QPainter(self) - cols = self.width() // item_width() - for i, channel_item in enumerate(self._chanel_items): - x = (i % cols) * item_width() - y = (i // cols) * item_height() - painter.drawPixmap(x, y, channel_item) + def resizeEvent(self, event: QResizeEvent, /) -> None: + """Resize the View.""" + super().resizeEvent(event) + new_width = self.viewport().width() + self._base_item.resize(new_width) + self.setSceneRect(0, 0, new_width, self._base_item.boundingRect().height()) - for fixture in self._fixtures: - for i, fixture_channel in enumerate(fixture.pixmap): - x = ((i + fixture.start_index) % cols) * item_width() - y = ((i + fixture.start_index) // cols) * item_height() - painter.drawPixmap(x, y, fixture_channel) - painter.end() +class PatchPlanWidget(PatchBaseItem): + """Patch Plan Widget for one Universe.""" - @override - def resizeEvent(self, event: QResizeEvent) -> None: - """resize the widget""" - new_cols = max(1, self.width() // item_width()) - if new_cols != self._cols: - self._cols = new_cols - self.update_widget_height() - super().resizeEvent(event) + _background_tiles: Final[list[QPixmap]] = [ + create_item(i + 1, QColorConstants.White) for i in range(NUMBER_OF_CHANNELS) + ] - def update_widget_height(self) -> None: - """update the widget height""" - rows = math.ceil(NUMBER_OF_CHANNELS / self._cols) - self.setFixedHeight(rows * item_height()) + def __init__(self) -> None: + super().__init__() + self.setZValue(-10) - def add_fixture(self, fixture: UsedFixture) -> None: - """add a fixture to the widget""" - new_fixture = UsedFixtureWidget(fixture) - self._fixtures.append(new_fixture) - self.update() + @override + def paint(self, painter: QPainter, option: QStyleOptionGraphicsItem, /, widget: QWidget | None = ...) -> None: + for i, channel_item in enumerate(self._background_tiles): + x = (i % self._cols) * (channel_item_width() + channel_item_spacing()) + y = (i // self._cols) * (channel_item_height() + channel_item_spacing()) + painter.drawPixmap(x, y, channel_item) diff --git a/src/view/patch_view/patch_plan/used_fixture_widget.py b/src/view/patch_view/patch_plan/used_fixture_widget.py index c0a35685..fb9868ae 100644 --- a/src/view/patch_view/patch_plan/used_fixture_widget.py +++ b/src/view/patch_view/patch_plan/used_fixture_widget.py @@ -1,39 +1,73 @@ -"""A Used Fixture in the patching view""" +"""A Used Fixture in the patching view.""" -from PySide6.QtCore import Qt -from PySide6.QtGui import QColorConstants, QFont, QMouseEvent, QPainter, QPixmap -from PySide6.QtWidgets import QWidget +from __future__ import annotations -from model.ofl.fixture import UsedFixture -from view.patch_view.patch_plan.channel_item_generator import create_item +import math +from typing import TYPE_CHECKING, override +from PySide6 import QtWidgets +from PySide6.QtGui import QAction, QColorConstants, QContextMenuEvent, QFont, QPainter, QPainterPath, QPixmap +from PySide6.QtWidgets import QGraphicsItem, QStyleOptionGraphicsItem, QWidget -class UsedFixtureWidget(QWidget): - """ - UI Widget of a Used Fixture - """ +from view.dialogs.fixture_dialog import FixtureDialog +from view.patch_view.patch_plan.channel_item_generator import ( + channel_item_height, + channel_item_spacing, + channel_item_width, + create_item, +) +from view.patch_view.patch_plan.patch_plan_widget import PatchBaseItem - def __init__(self, fixture: UsedFixture) -> None: +if TYPE_CHECKING: + from model import BoardConfiguration + from model.ofl.fixture import UsedFixture + + +class UsedFixtureWidget(PatchBaseItem): + """UI Widget of a Used Fixture.""" + + def __init__(self, fixture: UsedFixture, board_configuration: BoardConfiguration) -> None: + """UI Widget of a Used Fixture.""" super().__init__() - self._fixture = fixture + self._fixture: UsedFixture = fixture + self._board_configuration: BoardConfiguration = board_configuration + self._shape_path = QPainterPath() self._channels_static: list[QPixmap] = [] - fixture.static_data_changed.connect(self._build_static_pixmap) - self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, False) + fixture.static_data_changed.connect(self._rebild) + self._rebild() + self.setFlags(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable) - for chanel_index in range(fixture.channel_length): - self._channels_static.append(self._build_static_pixmap(chanel_index)) + @override + def paint(self, painter: QPainter, option: QStyleOptionGraphicsItem, /, widget: QWidget | None = ...) -> None: + x = (self._fixture.start_index % self._cols) * (channel_item_width() + channel_item_spacing()) + y = math.floor(self._fixture.start_index / self._cols) * (channel_item_height() + channel_item_spacing()) + self._shape_path = QPainterPath() + for channel_item in self._channels_static: + if x + channel_item_width() > self._view_width: + x = 0 + y += channel_item_height() + channel_item_spacing() + painter.drawPixmap(x, y, channel_item) + self._shape_path.addRect(x, y, channel_item_width(), channel_item_height()) + x += channel_item_width() + channel_item_spacing() + + @override + def shape(self) -> QPainterPath: + return self._shape_path + + @override + def contextMenuEvent(self, event: QContextMenuEvent) -> None: + """Context Menu.""" + menu = QtWidgets.QMenu() + action_modify = QAction("Bearbeiten", menu) - @property - def pixmap(self) -> list[QPixmap]: - """pixmap of the widget""" - return self._channels_static + action_modify.triggered.connect(self._modify_fixture) - @property - def start_index(self) -> int: - """start index of the fixture""" - return self._fixture.start_index + menu.addAction(action_modify) + + menu.exec(event.screenPos()) def _build_static_pixmap(self, channel_id: int) -> QPixmap: + """Build Static Pixmap for one Cannel.""" pixmap = create_item(self._fixture.start_index + channel_id + 1, self._fixture.color_on_stage) painter = QPainter(pixmap) painter.setRenderHint(QPainter.RenderHint.Antialiasing) @@ -47,3 +81,15 @@ def _build_static_pixmap(self, channel_id: int) -> QPixmap: painter.end() return pixmap + + def _rebild(self) -> None: + """Rebuild all Channel Pixmap's.""" + self._channels_static: list[QPixmap] = [] + for chanel_index in range(self._fixture.channel_length): + self._channels_static.append(self._build_static_pixmap(chanel_index)) + self.update() + + def _modify_fixture(self) -> None: + """Modify clicked Fixture.""" + self._dialog = FixtureDialog(self._fixture, self._board_configuration) + self._dialog.show() From 7f4c50c9a6d7c60779bbd3fb4eab31486028df98 Mon Sep 17 00:00:00 2001 From: CorsCodini <45790567+CorsCodini@users.noreply.github.com> Date: Fri, 8 Aug 2025 18:12:26 +0200 Subject: [PATCH 10/29] Doku --- .../serializing/universe_serialization.py | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/src/controller/file/serializing/universe_serialization.py b/src/controller/file/serializing/universe_serialization.py index e950881b..5a195163 100644 --- a/src/controller/file/serializing/universe_serialization.py +++ b/src/controller/file/serializing/universe_serialization.py @@ -1,4 +1,4 @@ -"""serialization of universes""" +"""Serialization of universes.""" import xml.etree.ElementTree as ET @@ -8,11 +8,13 @@ def _create_universe_element(universe: Universe, parent: ET.Element) -> ET.Element: - """Creates an XML element of type physical_location. + """Create an XML element of type `physical_location`. + + Example: + + ... + - - ... - """ return ET.SubElement( parent, @@ -26,9 +28,11 @@ def _create_universe_element(universe: Universe, parent: ET.Element) -> ET.Eleme def _create_physical_location_element(physical: int, parent: ET.Element) -> ET.Element: - """Creates an XML element of type physical_location. + """Create an XML element of type `physical_location`. + + Example: + 0 - 0 """ physical_location = ET.SubElement(parent, "physical_location") physical_location.text = str(physical) @@ -38,9 +42,11 @@ def _create_physical_location_element(physical: int, parent: ET.Element) -> ET.E def _create_artnet_location_element( artnet_location: proto.UniverseControl_pb2.Universe.ArtNet, parent: ET.Element ) -> ET.Element: - """Creates an xml element of type artnet_location. + """Create an XML element of type `artnet_location`. + + Example: + - """ return ET.SubElement( parent, @@ -56,9 +62,11 @@ def _create_artnet_location_element( def _create_ftdi_location_element( ftdi_location: proto.UniverseControl_pb2.Universe.USBConfig, parent: ET.Element ) -> ET.Element: - """Creates a xml element of type ftdi_location. + """Create an XML element of type `ftdi_location`. + + Example: + - """ # vendor_id=0x0403, product_id=0x6001 return ET.SubElement( @@ -74,11 +82,13 @@ def _create_ftdi_location_element( def _create_fixture_element(fixture: UsedFixture, patching_element: ET.Element, assemble_for_fish: bool) -> None: - """ - add patching information of a fixture to the show file XML structure. - :param fixture: The Fixture to add - :param parent: The parent element to add to - :param assemble_for_fish: Should this information be omitted? + """Add patching information of a fixture to the show file XML structure. + + Args: + fixture: The fixture to add. + patching_element: The parent element to add to. + assemble_for_fish: Whether this information should be omitted. + """ if assemble_for_fish: return From fa1e465fdd9dd37554ff83dac6b50019d2150d25 Mon Sep 17 00:00:00 2001 From: CorsCodini <45790567+CorsCodini@users.noreply.github.com> Date: Fri, 8 Aug 2025 19:48:27 +0200 Subject: [PATCH 11/29] rmv: useless --- src/view/dialogs/patching_dialog.py | 19 +++++++++++++------ .../patch_plan/used_fixture_widget.py | 2 +- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/view/dialogs/patching_dialog.py b/src/view/dialogs/patching_dialog.py index 82a02761..43e191bd 100644 --- a/src/view/dialogs/patching_dialog.py +++ b/src/view/dialogs/patching_dialog.py @@ -6,6 +6,7 @@ import numpy as np from PySide6 import QtCore, QtGui, QtWidgets +import style from model import BoardConfiguration from model.ofl.fixture import make_used_fixture from model.ofl.ofl_fixture import OflFixture @@ -63,7 +64,8 @@ def __init__( patching_layout.addWidget(self._patching) error_layout = QtWidgets.QHBoxLayout() - self._error_label = QtWidgets.QLabel("no Error Found") + self._error_label = QtWidgets.QLabel("No Error Found!") + self._error_label.setStyleSheet(style.LABEL_OKAY) error_layout.addWidget(self._error_label) layout_exit = QtWidgets.QHBoxLayout() @@ -147,10 +149,12 @@ def _validate_input(self) -> None: self._ok.setEnabled(False) if not self._board_configuration.universe(self._patching_information.universe): - self._error_label.setText("no matching Universes") + self._error_label.setText("No matching Universe!") + self._error_label.setStyleSheet(style.LABEL_ERROR) return if 0 < self._patching_information.offset < channel_count: - self._error_label.setText("offset to low") + self._error_label.setText("Offset to low!") + self._error_label.setStyleSheet(style.LABEL_ERROR) return start_index = self.patching_information.channel @@ -161,14 +165,17 @@ def _validate_input(self) -> None: occupied = (block_starts[:, np.newaxis] + channel_offsets).ravel() if occupied[-1] > 511: - self._error_label.setText("not enough channels") + self._error_label.setText("Not enough channels!") + self._error_label.setStyleSheet(style.LABEL_ERROR) return if np.isin( occupied, self._board_configuration.get_occupied_channels(self._patching_information.universe) ).any(): - self._error_label.setText("channels already occupied") + self._error_label.setText("Channels already occupied!") + self._error_label.setStyleSheet(style.LABEL_ERROR) return - self._error_label.setText("No Error Found") + self._error_label.setText("No Error Found!") + self._error_label.setStyleSheet(style.LABEL_OKAY) self._ok.setEnabled(True) diff --git a/src/view/patch_view/patch_plan/used_fixture_widget.py b/src/view/patch_view/patch_plan/used_fixture_widget.py index fb9868ae..0bb97dd8 100644 --- a/src/view/patch_view/patch_plan/used_fixture_widget.py +++ b/src/view/patch_view/patch_plan/used_fixture_widget.py @@ -75,7 +75,7 @@ def _build_static_pixmap(self, channel_id: int) -> QPixmap: painter.setPen(QColorConstants.Black) font = QFont("Arial", 10) painter.setFont(font) - painter.drawText(5, 35, str(self._fixture.short_name) if self._fixture.short_name else str(self._fixture.name)) + painter.drawText(5, 35, self._fixture.short_name if self._fixture.short_name else self._fixture.name) painter.drawText(5, 50, str(self._fixture.get_fixture_channel(channel_id).name)) painter.drawText(5, 65, str(self._fixture.name_on_stage)) painter.end() From c6faed603b36d7d4f9700a6e927841fd5abdbe5e Mon Sep 17 00:00:00 2001 From: CorsCodini <45790567+CorsCodini@users.noreply.github.com> Date: Fri, 8 Aug 2025 19:49:11 +0200 Subject: [PATCH 12/29] add Style --- src/view/dialogs/fixture_dialog.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/view/dialogs/fixture_dialog.py b/src/view/dialogs/fixture_dialog.py index aa325aad..03067834 100644 --- a/src/view/dialogs/fixture_dialog.py +++ b/src/view/dialogs/fixture_dialog.py @@ -17,6 +17,7 @@ QVBoxLayout, ) +import style from model.universe import NUMBER_OF_CHANNELS if TYPE_CHECKING: @@ -40,7 +41,9 @@ def __init__(self, fixture: UsedFixture, board_configuration: BoardConfiguration layout_fixture = QGridLayout() layout_fixture.addWidget(QLabel("Fixture name:"), 0, 0) - layout_fixture.addWidget(QLabel(self._fixture.short_name), 0, 1) + layout_fixture.addWidget( + QLabel(self._fixture.short_name if self._fixture.short_name else self._fixture.name), 0, 1 + ) layout_fixture.addWidget(QLabel("Anzeigename"), 1, 0) self._name_on_stage = QLineEdit(self._fixture.name_on_stage) @@ -64,6 +67,8 @@ def __init__(self, fixture: UsedFixture, board_configuration: BoardConfiguration layout_error = QHBoxLayout() self._error_label = QLabel("No Error Found!") + self._error_label.setFixedHeight(20) + self._error_label.setStyleSheet(style.LABEL_OKAY) layout_error.addWidget(self._error_label) layout_exit = QHBoxLayout() @@ -114,6 +119,9 @@ def _validate_input(self) -> None: if np.isin(occupied, self._board_configuration.get_occupied_channels(self._fixture.universe_id)).any(): self._error_label.setText("Channels already occupied!") + self._error_label.setStyleSheet(style.LABEL_ERROR) return - self._error_label = QLabel("No Error Found!") + + self._error_label.setText("No Error Found!") + self._error_label.setStyleSheet(style.LABEL_OKAY) self._ok_button.setEnabled(True) From ed10c013c11bbea3f444772d7317a187071baac1 Mon Sep 17 00:00:00 2001 From: CorsCodini <45790567+CorsCodini@users.noreply.github.com> Date: Fri, 8 Aug 2025 19:49:31 +0200 Subject: [PATCH 13/29] beautification --- src/view/dialogs/universe_dialog.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/view/dialogs/universe_dialog.py b/src/view/dialogs/universe_dialog.py index 2bff447a..2ec48837 100644 --- a/src/view/dialogs/universe_dialog.py +++ b/src/view/dialogs/universe_dialog.py @@ -1,5 +1,9 @@ """dialog for editing patching universe""" + +from typing import Any + from PySide6 import QtWidgets +from PySide6.QtWidgets import QWidget import proto.UniverseControl_pb2 @@ -7,8 +11,9 @@ class UniverseDialog(QtWidgets.QDialog): """dialog for editing patching universe""" - def __init__(self, patching_universe_or_id: proto.UniverseControl_pb2.Universe | int, - parent: object = None) -> None: + def __init__( + self, patching_universe_or_id: proto.UniverseControl_pb2.Universe | int, parent: QWidget = None + ) -> None: super().__init__(parent) if isinstance(patching_universe_or_id, int): patching_proto: proto.UniverseControl_pb2.Universe = proto.UniverseControl_pb2.Universe( @@ -34,12 +39,18 @@ def __init__(self, patching_universe_or_id: proto.UniverseControl_pb2.Universe | self._switch_button.clicked.connect(self._change_widget) ftdi_dongle = patching_proto.ftdi_dongle - ftdi_items = [["vendor id", ftdi_dongle.vendor_id], ["product id", ftdi_dongle.product_id], - ["serial", ftdi_dongle.serial], ["device_name", ftdi_dongle.device_name]] + ftdi_items = [ + ["vendor id", ftdi_dongle.vendor_id], + ["product id", ftdi_dongle.product_id], + ["serial", ftdi_dongle.serial], + ["device_name", ftdi_dongle.device_name], + ] remote_location = patching_proto.remote_location - art_net_items: list[list[str, any]] = [["ip address", remote_location.ip_address], - ["port", remote_location.port], - ["universe on device", remote_location.universe_on_device]] + art_net_items: list[list[str, any]] = [ + ["ip address", remote_location.ip_address], + ["port", remote_location.port], + ["universe on device", remote_location.universe_on_device], + ] ftdi_widget, self._ftdi_widgets = _generate_widget(ftdi_items, "ftdi dongle") art_net_widget, self._remote_location_widgets = _generate_widget(art_net_items, "art net") @@ -103,7 +114,7 @@ def cancel(self) -> None: self.reject() -def _generate_widget(items: list[list[str, any]], name: str) -> tuple[QtWidgets.QWidget, list[QtWidgets.QLineEdit]]: +def _generate_widget(items: list[tuple[str, Any]], name: str) -> tuple[QtWidgets.QWidget, list[QtWidgets.QLineEdit]]: output = QtWidgets.QWidget() layout = QtWidgets.QGridLayout() widgets = [] From f2dfb73fb310d141c85fd02cbc2902dd7aea240a Mon Sep 17 00:00:00 2001 From: CorsCodini <45790567+CorsCodini@users.noreply.github.com> Date: Fri, 8 Aug 2025 19:56:28 +0200 Subject: [PATCH 14/29] Doku --- src/view/dialogs/patching_dialog.py | 30 ++++++++----------- src/view/dialogs/universe_dialog.py | 10 ++++--- .../patch_plan/channel_item_generator.py | 2 +- .../patch_plan/patch_plan_selector.py | 10 +++---- .../patch_plan/patch_plan_widget.py | 3 +- 5 files changed, 26 insertions(+), 29 deletions(-) diff --git a/src/view/dialogs/patching_dialog.py b/src/view/dialogs/patching_dialog.py index 43e191bd..c6937c9a 100644 --- a/src/view/dialogs/patching_dialog.py +++ b/src/view/dialogs/patching_dialog.py @@ -1,4 +1,4 @@ -"""Dialog for Patching Fixture""" +"""Dialog for Patching Fixture.""" import re from dataclasses import dataclass @@ -14,9 +14,10 @@ @dataclass class PatchingInformation: - """Information for Patching""" + """Information for Patching.""" def __init__(self, fixture: OflFixture) -> None: + """Information for Patching.""" self._fixture: OflFixture = fixture self.count: int = 0 self.universe: int = 0 @@ -25,16 +26,17 @@ def __init__(self, fixture: OflFixture) -> None: @property def fixture(self) -> OflFixture: - """property of the Fixture""" + """OFL Fixture.""" return self._fixture class PatchingDialog(QtWidgets.QDialog): - """Dialog for Patching Fixture""" + """Dialog for Patching Fixture.""" def __init__( self, board_configuration: BoardConfiguration, fixture: tuple[OflFixture, int], parent: object = None ) -> None: + """Dialog for Patching Fixture.""" super().__init__(parent) # Create widgets self._board_configuration = board_configuration @@ -42,7 +44,7 @@ def __init__( layout_fixture = QtWidgets.QHBoxLayout() self._select_mode = QtWidgets.QComboBox() - self._select_mode.currentIndexChanged.connect(self._update_used_fixture) + self._select_mode.currentIndexChanged.connect(self._validate_input) layout_fixture.addWidget(QtWidgets.QLabel(fixture[0].name)) layout_fixture.addWidget(self._select_mode) @@ -95,19 +97,11 @@ def __init__( @property def patching_information(self) -> PatchingInformation: - """property of used Fixture""" + """Patching Information.""" return self._patching_information - def set_error(self, text: str) -> None: - """update Error Label""" - self._error_label.setText(text) - - def _update_used_fixture(self) -> None: - self._validate_input() - def generate_fixtures(self) -> None: - """generate a used Fixture list from Patching information""" - + """Generate a used Fixture list from Patching information.""" start_index = self.patching_information.channel for _ in range(self.patching_information.count): used_fixture = make_used_fixture( @@ -124,15 +118,15 @@ def generate_fixtures(self) -> None: start_index += self._patching_information.offset def _accept(self) -> None: - """accept the Fixture""" + """Handle Accept button.""" self.accept() def _reject(self) -> None: - """cancel Patching""" + """Handle Cancel button.""" self.reject() def _validate_input(self) -> None: - """validate the patching String and update count, universe, channel and offset""" + """Validate the patching String and update count, universe, channel and offset.""" patching = self._patching.text() if patching == "": patching = "1" diff --git a/src/view/dialogs/universe_dialog.py b/src/view/dialogs/universe_dialog.py index 2ec48837..83075f99 100644 --- a/src/view/dialogs/universe_dialog.py +++ b/src/view/dialogs/universe_dialog.py @@ -1,4 +1,4 @@ -"""dialog for editing patching universe""" +"""Dialog for editing patching universe.""" from typing import Any @@ -9,11 +9,12 @@ class UniverseDialog(QtWidgets.QDialog): - """dialog for editing patching universe""" + """Dialog for editing patching universe.""" def __init__( self, patching_universe_or_id: proto.UniverseControl_pb2.Universe | int, parent: QWidget = None ) -> None: + """Dialog for editing patching universe.""" super().__init__(parent) if isinstance(patching_universe_or_id, int): patching_proto: proto.UniverseControl_pb2.Universe = proto.UniverseControl_pb2.Universe( @@ -85,7 +86,7 @@ def _change_widget(self) -> None: self._widgets.setCurrentIndex(0) def ok(self) -> None: - """accept the universe""" + """Handle Ok button.""" if self._widgets.currentIndex() == 0: # art net self.output = proto.UniverseControl_pb2.Universe( @@ -110,11 +111,12 @@ def ok(self) -> None: self.accept() def cancel(self) -> None: - """cancel universe""" + """Handle cancel button.""" self.reject() def _generate_widget(items: list[tuple[str, Any]], name: str) -> tuple[QtWidgets.QWidget, list[QtWidgets.QLineEdit]]: + """Generate a widget for a patching universe.""" output = QtWidgets.QWidget() layout = QtWidgets.QGridLayout() widgets = [] diff --git a/src/view/patch_view/patch_plan/channel_item_generator.py b/src/view/patch_view/patch_plan/channel_item_generator.py index 6a20fa82..9801fb90 100644 --- a/src/view/patch_view/patch_plan/channel_item_generator.py +++ b/src/view/patch_view/patch_plan/channel_item_generator.py @@ -23,7 +23,7 @@ def channel_item_spacing() -> int: def create_item(number: int, color: QColor) -> QPixmap: - """Creates a pixmap of a Channel item.""" + """Create a pixmap of a Channel item.""" pixmap = QPixmap(_WIDTH, _HEIGHT) pixmap.fill(color) diff --git a/src/view/patch_view/patch_plan/patch_plan_selector.py b/src/view/patch_view/patch_plan/patch_plan_selector.py index cea459ac..7bb0dfab 100644 --- a/src/view/patch_view/patch_plan/patch_plan_selector.py +++ b/src/view/patch_view/patch_plan/patch_plan_selector.py @@ -1,4 +1,4 @@ -"""selector for Patching witch holds all Patching Universes""" +"""Selector for Patching witch holds all Patching Universes.""" from __future__ import annotations @@ -23,9 +23,10 @@ class PatchPlanSelector(QtWidgets.QTabWidget): - """selector for Patching witch holds all Patching Universes""" + """Selector for Patching witch holds all Patching Universes.""" def __init__(self, board_configuration: BoardConfiguration, parent: PatchMode) -> None: + """Selector for Patching witch holds all Patching Universes.""" super().__init__(parent=parent) self._board_configuration = board_configuration self._broadcaster = Broadcaster() @@ -45,15 +46,14 @@ def _add_fixture(self, fixture: UsedFixture) -> None: self._patch_planes[fixture.universe_id].scene().addItem(UsedFixtureWidget(fixture, self._board_configuration)) def _generate_universe(self) -> None: - """add a new Universe to universe Selector""" - + """Add a new Universe to universe Selector.""" dialog = UniverseDialog(self._board_configuration.next_universe_id()) if dialog.exec(): Universe(dialog.output) @override def contextMenuEvent(self, event: QContextMenuEvent) -> None: - """context menu""" + """Context menu.""" for index in range(self.tabBar().count() - 1): if self.tabBar().tabRect(index).contains(event.pos()): menu = QtWidgets.QMenu(self) diff --git a/src/view/patch_view/patch_plan/patch_plan_widget.py b/src/view/patch_view/patch_plan/patch_plan_widget.py index fc52c101..111274f3 100644 --- a/src/view/patch_view/patch_plan/patch_plan_widget.py +++ b/src/view/patch_view/patch_plan/patch_plan_widget.py @@ -1,4 +1,4 @@ -"""patch Plan Widget for one Universe""" +"""Patch Plan Widget for one Universe.""" from typing import Final, override @@ -37,6 +37,7 @@ class PatchPlanWidget(PatchBaseItem): ] def __init__(self) -> None: + """Patch Plan Widget for one Universe.""" super().__init__() self.setZValue(-10) From 6d9641440b54eecd96827124b9290f1534a18dfd Mon Sep 17 00:00:00 2001 From: CorsCodini <45790567+CorsCodini@users.noreply.github.com> Date: Fri, 8 Aug 2025 21:12:12 +0200 Subject: [PATCH 15/29] chg: type correction --- src/model/universe.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/model/universe.py b/src/model/universe.py index 7bd70ff1..375b2193 100644 --- a/src/model/universe.py +++ b/src/model/universe.py @@ -1,4 +1,5 @@ """DMX Universe""" + from typing import Final import proto.UniverseControl_pb2 @@ -13,9 +14,10 @@ class Universe: def __init__(self, universe_proto: proto.UniverseControl_pb2.Universe) -> None: self._broadcaster = Broadcaster() - self._universe_proto: proto.UniverseControl_pb2 = universe_proto - self._channels: Final[list[Channel]] = [Channel(channel_address) for channel_address in - range(NUMBER_OF_CHANNELS)] + self._universe_proto: proto.UniverseControl_pb2.Universe = universe_proto + self._channels: Final[list[Channel]] = [ + Channel(channel_address) for channel_address in range(NUMBER_OF_CHANNELS) + ] self._name = f"Universe {self.universe_proto.id + 1}" self._description = self.name @@ -60,7 +62,8 @@ def description(self, description: str) -> None: @property def location( - self) -> int | proto.UniverseControl_pb2.Universe.ArtNet | proto.UniverseControl_pb2.Universe.USBConfig: + self, + ) -> int | proto.UniverseControl_pb2.Universe.ArtNet | proto.UniverseControl_pb2.Universe.USBConfig: """network location""" if self._universe_proto.remote_location.ip_address != "": return self._universe_proto.remote_location From 24865ca33109f883b2f711c0203789aed23e191f Mon Sep 17 00:00:00 2001 From: CorsCodini <45790567+CorsCodini@users.noreply.github.com> Date: Fri, 8 Aug 2025 21:14:08 +0200 Subject: [PATCH 16/29] Doku --- src/model/universe.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/model/universe.py b/src/model/universe.py index 375b2193..fe51e41d 100644 --- a/src/model/universe.py +++ b/src/model/universe.py @@ -1,4 +1,4 @@ -"""DMX Universe""" +"""DMX Universe.""" from typing import Final @@ -10,9 +10,10 @@ class Universe: - """DMX universe with 512 channels""" + """DMX universe with 512 channels.""" def __init__(self, universe_proto: proto.UniverseControl_pb2.Universe) -> None: + """DMX universe with 512 channels.""" self._broadcaster = Broadcaster() self._universe_proto: proto.UniverseControl_pb2.Universe = universe_proto self._channels: Final[list[Channel]] = [ @@ -25,7 +26,7 @@ def __init__(self, universe_proto: proto.UniverseControl_pb2.Universe) -> None: @property def universe_proto(self) -> proto.UniverseControl_pb2.Universe: - """the UniverseProto of the Universe""" + """UniverseProto of the Universe.""" return self._universe_proto @universe_proto.setter @@ -34,12 +35,12 @@ def universe_proto(self, proto_: proto.UniverseControl_pb2.Universe) -> None: @property def channels(self) -> list[Channel]: - """List of all 512 dmx channels belonging to the Universe""" + """List of all 512 dmx channels belonging to the Universe.""" return self._channels @property def id(self) -> int: - """id of the universe""" + """ID of the universe.""" return self._universe_proto.id @property @@ -64,7 +65,7 @@ def description(self, description: str) -> None: def location( self, ) -> int | proto.UniverseControl_pb2.Universe.ArtNet | proto.UniverseControl_pb2.Universe.USBConfig: - """network location""" + """Network location.""" if self._universe_proto.remote_location.ip_address != "": return self._universe_proto.remote_location if self._universe_proto.ftdi_dongle.vendor_id != "": From 607a7cdbbb62a486fc8737d925b5d1accb5121d5 Mon Sep 17 00:00:00 2001 From: CorsCodini <45790567+CorsCodini@users.noreply.github.com> Date: Fri, 8 Aug 2025 21:17:41 +0200 Subject: [PATCH 17/29] chg: fixture save Universe not id --- src/controller/file/read.py | 2 +- src/model/ofl/fixture.py | 25 +++++++++++++++---------- src/view/dialogs/patching_dialog.py | 2 +- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/controller/file/read.py b/src/controller/file/read.py index 684f4b63..7da92303 100644 --- a/src/controller/file/read.py +++ b/src/controller/file/read.py @@ -639,7 +639,7 @@ def _parse_patching(board_configuration: BoardConfiguration, location_element: E board_configuration, load_fixture(os.path.join(fixtures_path, child.attrib["fixture_file"])), int(child.attrib["mode"]), - universe_id, + board_configuration.universe(universe_id), int(child.attrib["start"]), UUID(child.attrib.get("id")) if child.attrib.get("id") else None, child.attrib.get("color"), diff --git a/src/model/ofl/fixture.py b/src/model/ofl/fixture.py index f3a6b649..320921b4 100644 --- a/src/model/ofl/fixture.py +++ b/src/model/ofl/fixture.py @@ -22,7 +22,7 @@ from numpy.typing import NDArray - from model import BoardConfiguration + from model import BoardConfiguration, Universe logger = getLogger(__name__) @@ -73,7 +73,7 @@ def __init__( board_configuration: BoardConfiguration, fixture: OflFixture, mode_index: int, - parent_universe: int, + parent_universe: Universe, start_index: int, uuid: UUID | None = None, color_on_stage: str | None = None, @@ -86,7 +86,7 @@ def __init__( self._start_index: int = start_index self._mode_index: int = mode_index - self._universe_id: int = parent_universe + self._universe: Universe = parent_universe channels, segment_map, color_support = self._generate_fixture_channels() @@ -155,12 +155,17 @@ def mode_index(self) -> int: @property def universe_id(self) -> int: - """Id of the universe for the fixture.""" - return self._universe_id + """Universe for the fixture.""" + return self._universe.id - @universe_id.setter - def universe_id(self, universe_id: int) -> None: - self._universe_id = universe_id + @property + def universe(self) -> Universe: + """Universe of the fixture.""" + return self._universe + + @universe.setter + def universe(self, universe: Universe) -> None: + self._universe = universe @property def channel_length(self) -> int: @@ -249,10 +254,10 @@ def make_used_fixture( board_configuration: BoardConfiguration, fixture: OflFixture, mode_index: int, - universe_id: int, + universe: Universe, start_index: int, uuid: UUID | None = None, color: str | None = None, ) -> UsedFixture: """Generate a new Used Fixture from a oflFixture.""" - return UsedFixture(board_configuration, fixture, mode_index, universe_id, start_index, uuid, color) + return UsedFixture(board_configuration, fixture, mode_index, universe, start_index, uuid, color) diff --git a/src/view/dialogs/patching_dialog.py b/src/view/dialogs/patching_dialog.py index c6937c9a..4168e4c9 100644 --- a/src/view/dialogs/patching_dialog.py +++ b/src/view/dialogs/patching_dialog.py @@ -108,7 +108,7 @@ def generate_fixtures(self) -> None: self._board_configuration, self._patching_information.fixture, self._select_mode.currentIndex(), - self.patching_information.universe, + self._board_configuration.universe(self.patching_information.universe), start_index, ) From af4d1f29294d519e9585e72b882f1a7198212307 Mon Sep 17 00:00:00 2001 From: CorsCodini <45790567+CorsCodini@users.noreply.github.com> Date: Fri, 8 Aug 2025 21:19:12 +0200 Subject: [PATCH 18/29] chg: get_occupied_channels by universe --- src/model/board_configuration.py | 29 ++++++++++++++++++----------- src/view/dialogs/fixture_dialog.py | 2 +- src/view/dialogs/patching_dialog.py | 5 ++++- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/model/board_configuration.py b/src/model/board_configuration.py index dba66273..c78fcd5f 100644 --- a/src/model/board_configuration.py +++ b/src/model/board_configuration.py @@ -1,18 +1,25 @@ """Provides data structures with accessors and modifiers for DMX""" -from collections.abc import Callable, Sequence + +from __future__ import annotations + from logging import getLogger +from typing import TYPE_CHECKING import numpy as np from PySide6 import QtCore, QtGui -import proto.FilterMode_pb2 - from .broadcaster import Broadcaster -from .device import Device -from .macro import Macro -from .ofl.fixture import UsedFixture -from .scene import Scene -from .universe import Universe + +if TYPE_CHECKING: + from collections.abc import Callable, Sequence + + import proto.FilterMode_pb2 + + from .device import Device + from .macro import Macro + from .ofl.fixture import UsedFixture + from .scene import Scene + from .universe import Universe logger = getLogger(__name__) @@ -195,7 +202,7 @@ def broadcaster(self) -> Broadcaster: @property def file_path(self) -> str: - """ path to the showfile""" + """Path to the showfile.""" return self._show_file_path @file_path.setter @@ -289,12 +296,12 @@ def next_universe_id(self) -> int: nex_id += 1 return nex_id - def get_occupied_channels(self, universe_id: int) -> np.typing.NDArray[int]: + def get_occupied_channels(self, universe: Universe) -> np.typing.NDArray[int]: """Returns a list of all channels that are occupied by a scene.""" ranges = [ np.arange(fixture.start_index, fixture.start_index + fixture.channel_length) for fixture in self.fixtures - if fixture.universe_id == universe_id + if fixture.universe == universe ] return np.concatenate(ranges) if ranges else np.array([], dtype=int) diff --git a/src/view/dialogs/fixture_dialog.py b/src/view/dialogs/fixture_dialog.py index 03067834..dd38c934 100644 --- a/src/view/dialogs/fixture_dialog.py +++ b/src/view/dialogs/fixture_dialog.py @@ -117,7 +117,7 @@ def _validate_input(self) -> None: int(self._start_index.value() - 1), int(self._start_index.value() - 1) + self._fixture.channel_length ) - if np.isin(occupied, self._board_configuration.get_occupied_channels(self._fixture.universe_id)).any(): + if np.isin(occupied, self._board_configuration.get_occupied_channels(self._fixture.universe)).any(): self._error_label.setText("Channels already occupied!") self._error_label.setStyleSheet(style.LABEL_ERROR) return diff --git a/src/view/dialogs/patching_dialog.py b/src/view/dialogs/patching_dialog.py index 4168e4c9..0bf6f2d0 100644 --- a/src/view/dialogs/patching_dialog.py +++ b/src/view/dialogs/patching_dialog.py @@ -164,7 +164,10 @@ def _validate_input(self) -> None: return if np.isin( - occupied, self._board_configuration.get_occupied_channels(self._patching_information.universe) + occupied, + self._board_configuration.get_occupied_channels( + self._board_configuration.universe(self._patching_information.universe) + ), ).any(): self._error_label.setText("Channels already occupied!") self._error_label.setStyleSheet(style.LABEL_ERROR) From b825247ba5ec6f65489d6f84bb144c9d136a8158 Mon Sep 17 00:00:00 2001 From: CorsCodini <45790567+CorsCodini@users.noreply.github.com> Date: Fri, 8 Aug 2025 21:29:10 +0200 Subject: [PATCH 19/29] Doku --- src/model/board_configuration.py | 116 +++++++++++++++++-------------- 1 file changed, 65 insertions(+), 51 deletions(-) diff --git a/src/model/board_configuration.py b/src/model/board_configuration.py index c78fcd5f..6d5cab56 100644 --- a/src/model/board_configuration.py +++ b/src/model/board_configuration.py @@ -1,4 +1,4 @@ -"""Provides data structures with accessors and modifiers for DMX""" +"""Provides data structures with accessors and modifiers for DMX.""" from __future__ import annotations @@ -28,6 +28,7 @@ class BoardConfiguration: """Board configuration of a show file.""" def __init__(self, show_name: str = "", default_active_scene: int = 0, notes: str = "") -> None: + """Board configuration of a show file.""" self._show_name: str = show_name self._default_active_scene: int = default_active_scene self._notes: str = notes @@ -55,7 +56,7 @@ def __init__(self, show_name: str = "", default_active_scene: int = 0, notes: st self._broadcaster.update_filter_parameter.connect(self._distribute_filter_update_message) def _clear(self) -> None: - """This method resets the show data prior to loading a new one.""" + """Reset the show data prior to loading new one.""" for scene in self._scenes: self._broadcaster.delete_scene.emit(scene) for universe in self._universes: @@ -76,38 +77,44 @@ def _clear(self) -> None: self._macros.clear() def _add_scene(self, scene: Scene) -> None: - """Adds a scene to the list of scenes. + """Add a scene to the list of scenes. Args: - scene: The scene to be added. + scene: The scene to add. + """ self._scenes.append(scene) self._scenes_index[scene.scene_id] = len(self._scenes) - 1 def _delete_scene(self, scene: Scene) -> None: - """Removes the passed scene from the list of scenes. + """Remove the given scene from the list of scenes. Args: - scene: The scene to be removed. + scene: The scene to remove. + """ self._scenes.remove(scene) self._scenes_index.pop(scene.scene_id) def _add_universe(self, universe: Universe) -> None: - """Creates and adds a universe from passed patching universe. + """Create and add a universe from the given patching universe. + Args: universe: The universe to add. + """ self._universes.update({universe.id: universe}) def _add_fixture(self, used_fixture: UsedFixture) -> None: + """Handle add Fixture signal.""" self._fixtures.append(used_fixture) def _delete_universe(self, universe: Universe) -> None: - """Removes the passed universe from the list of universes. + """Remove the given universe from the list of universes. Args: - universe: The universe to be removed. + universe: The universe to remove. + """ try: del self._universes[universe.id] @@ -115,89 +122,89 @@ def _delete_universe(self, universe: Universe) -> None: logger.exception("Unable to remove universe %s", universe.name) def _add_device(self, device: Device) -> None: - """Adds the device to the board configuration. + """Add the given device to the board configuration. Args: - device: The device to be added. + device: The device to add. + """ self._devices.append(device) def _delete_device(self, device: Device) -> None: - """Removes the passed device from the list of devices. + """Remove the given device from the list. Args: - device: The device to be removed. + device: The device to remove. + """ def universe(self, universe_id: int) -> Universe | None: - """Tries to find a universe by id. + """Find a universe by its ID. - Arg: - universe_id: The id of the universe requested. + Args: + universe_id: The ID of the requested universe. Returns: The universe if found, else None. + """ return self._universes.get(universe_id, None) @property def fixtures(self) -> Sequence[UsedFixture]: - """Fixtures associated with this Show""" + """Fixtures associated with this Show.""" return self._fixtures @property def show_name(self) -> str: - """The name of the show""" + """Name of the show.""" return self._show_name @show_name.setter def show_name(self, show_name: str) -> None: - """Sets the show name""" self._show_name = show_name @property def default_active_scene(self) -> int: - """Scene to be activated by fish on loadup""" + """Scene to be activated by fish on loadup.""" return self._default_active_scene @default_active_scene.setter def default_active_scene(self, default_active_scene: int) -> None: - """Setss the scene to be activated by fish on loadup""" self._default_active_scene = default_active_scene @property def notes(self) -> str: - """Notes for the show""" + """Notes for the show.""" return self._notes @notes.setter def notes(self, notes: str) -> None: - """Sets the notes for the show""" self._notes = notes @property def scenes(self) -> list[Scene]: - """The scenes of the show""" + """Scenes of the show.""" return self._scenes @property def devices(self) -> list[Device]: - """The devices of the show""" + """Devices of the show.""" return self._devices @property def universes(self) -> list[Universe]: - """The universes of the show""" + """Universes of the show.""" return list(self._universes.values()) @property def ui_hints(self) -> dict[str, str]: - """UI hints for the show""" + """UI hints for the show.""" return self._ui_hints @property def broadcaster(self) -> Broadcaster: - """The broadcaster the board configuration uses""" + """The broadcaster the board configuration use.""" return self._broadcaster @property @@ -207,13 +214,11 @@ def file_path(self) -> str: @file_path.setter def file_path(self, new_path: str) -> None: - """Update the show file path. - :param new_path: The location to save the show file to""" self._show_file_path = new_path self._broadcaster.show_file_path_changed.emit(new_path) def get_scene_by_id(self, scene_id: int) -> Scene | None: - """Returns the scene by her id""" + """Return the scene by id.""" looked_up_position = self._scenes_index.get(scene_id) if looked_up_position is not None and looked_up_position < len(self._scenes): return self._scenes[looked_up_position] @@ -223,21 +228,23 @@ def get_scene_by_id(self, scene_id: int) -> Scene | None: return None def _distribute_filter_update_message(self, param: proto.FilterMode_pb2.update_parameter) -> None: - """Find listeners to incoming filter update message and distribute it to them.""" + """Find listeners for an incoming filter update message and distribute it.""" candidate_list = self._filter_update_msg_register.get((param.scene_id, param.filter_id)) if candidate_list is not None: for c in candidate_list: c(param) def register_filter_update_callback(self, target_scene: int, target_filter_id: str, c: Callable) -> None: - """ - Register a new callback for filter update messages. + """Register a new callback for filter update messages. + + If filter update messages are received, they are routed to their intended + destination. Suitable callables receive the update message as a parameter. + + Args: + target_scene: The scene the callback belongs to. + target_filter_id: The filter ID to listen on. + c: The callable to register. - If filter update messages are received, they need to be routed to their intended destination. This is done using - this registration method. Suitable callables receive the update message as a parameter. - :param target_scene: The scene the callback belongs to. - :param target_filter_id: The filter id to listen on. - :param c: The callable to register. """ callable_list = self._filter_update_msg_register.get((target_scene, target_filter_id)) if callable_list is None: @@ -247,11 +254,13 @@ def register_filter_update_callback(self, target_scene: int, target_filter_id: s callable_list.append(c) def remove_filter_update_callback(self, target_scene: int, target_filter_id: str, c: Callable) -> None: - """ - Remove a previously registered callback. - :param target_scene: The scene the callback belongs to. - :param target_filter_id: The filter id which it is listening on. - :param c: The callable to be removed. + """Remove a previously registered callback. + + Args: + target_scene: The scene the callback belongs to. + target_filter_id: The filter ID it is listening on. + c: The callable to remove. + """ callable_list = self._filter_update_msg_register.get((target_scene, target_filter_id)) if callable_list is None: @@ -260,12 +269,13 @@ def remove_filter_update_callback(self, target_scene: int, target_filter_id: str callable_list.remove(c) def add_macro(self, m: Macro) -> None: - """ - Add a new macro to the show file. + """Add a new macro to the show file. This method must be called from a QObject as it triggers an event. - :param m: The macro to add. + Args: + m: The macro to add. + """ new_index = len(self._macros) self._macros.append(m) @@ -273,7 +283,11 @@ def add_macro(self, m: Macro) -> None: def get_macro(self, macro_id: int | str) -> Macro | None: """Get the macro specified by its index. - :returns: The macro or None if none was found.""" + + Returns: + The macro, or None if none was found. + + """ if isinstance(macro_id, int): if macro_id >= len(self._macros): return None @@ -286,18 +300,18 @@ def get_macro(self, macro_id: int | str) -> Macro | None: @property def macros(self) -> list[Macro]: - """Get a list of registered macros.""" + """List of registered macros.""" return self._macros.copy() def next_universe_id(self) -> int: - """next empty universe id""" + """Next empty universe id.""" nex_id = len(self._universes) while self._universes.get(nex_id): nex_id += 1 return nex_id def get_occupied_channels(self, universe: Universe) -> np.typing.NDArray[int]: - """Returns a list of all channels that are occupied by a scene.""" + """Return a list of all channels that are occupied by Fixtures in a Universe.""" ranges = [ np.arange(fixture.start_index, fixture.start_index + fixture.channel_length) for fixture in self.fixtures From 4c6ec28e5cfa6d953ae60221b211d747677c00b6 Mon Sep 17 00:00:00 2001 From: CorsCodini <45790567+CorsCodini@users.noreply.github.com> Date: Fri, 8 Aug 2025 21:38:13 +0200 Subject: [PATCH 20/29] serialize only fixtures in universe --- .../file/serializing/general_serialization.py | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/controller/file/serializing/general_serialization.py b/src/controller/file/serializing/general_serialization.py index 1672faba..3f7c3216 100644 --- a/src/controller/file/serializing/general_serialization.py +++ b/src/controller/file/serializing/general_serialization.py @@ -1,4 +1,5 @@ """serialization functions""" + import xml.etree.ElementTree as ET from controller.file.serializing.events_and_macros import _write_event_sender, _write_macro @@ -16,8 +17,9 @@ from model.events import get_all_senders -def create_xml(board_configuration: BoardConfiguration, pn: ProcessNotifier, - assemble_for_fish_loading: bool = False) -> ET.Element: +def create_xml( + board_configuration: BoardConfiguration, pn: ProcessNotifier, assemble_for_fish_loading: bool = False +) -> ET.Element: """Creates an XML element from the given board configuration. Args: @@ -55,7 +57,8 @@ def create_xml(board_configuration: BoardConfiguration, pn: ProcessNotifier, if fixtures := board_configuration.fixtures: patching_element = ET.SubElement(universe_element, "patching") for fixture in fixtures: - _create_fixture_element(fixture, patching_element, assemble_for_fish_loading) + if fixture.universe == universe: + _create_fixture_element(fixture, patching_element, assemble_for_fish_loading) pn.total_step_count += 1 pn.current_step_description = "Storing device list." @@ -87,14 +90,17 @@ def _create_board_configuration_element(board_configuration: BoardConfiguration) """ # TODO we're not filling in the version attribute - return ET.Element("bord_configuration", attrib={ - "xmlns": "http://www.asta.uni-luebeck.de/MissionDMX/ShowFile", - "xsi:schemaLocation": "http://www.asta.uni-luebeck.de/MissionDMX/ShowFile", - "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance", - "show_name": str(board_configuration.show_name), - "default_active_scene": str(board_configuration.default_active_scene), - "notes": str(board_configuration.notes), - }) + return ET.Element( + "bord_configuration", + attrib={ + "xmlns": "http://www.asta.uni-luebeck.de/MissionDMX/ShowFile", + "xsi:schemaLocation": "http://www.asta.uni-luebeck.de/MissionDMX/ShowFile", + "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance", + "show_name": str(board_configuration.show_name), + "default_active_scene": str(board_configuration.default_active_scene), + "notes": str(board_configuration.notes), + }, + ) def _create_device_element(device: Device, parent: ET.Element) -> ET.Element: From dbb446b27feeae387be7467c720bac6c34650cc6 Mon Sep 17 00:00:00 2001 From: CorsCodini <45790567+CorsCodini@users.noreply.github.com> Date: Fri, 8 Aug 2025 21:40:47 +0200 Subject: [PATCH 21/29] Doku --- .../file/serializing/general_serialization.py | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/controller/file/serializing/general_serialization.py b/src/controller/file/serializing/general_serialization.py index 3f7c3216..f53d5b19 100644 --- a/src/controller/file/serializing/general_serialization.py +++ b/src/controller/file/serializing/general_serialization.py @@ -1,4 +1,4 @@ -"""serialization functions""" +"""Serialization functions.""" import xml.etree.ElementTree as ET @@ -20,16 +20,18 @@ def create_xml( board_configuration: BoardConfiguration, pn: ProcessNotifier, assemble_for_fish_loading: bool = False ) -> ET.Element: - """Creates an XML element from the given board configuration. + """Create an XML element from the given board configuration. Args: - board_configuration: The board configuration to be converted. - assemble_for_fish_loading: Pass True if the XML is build for fish. - This will skip the UI and resolve virtual filters + board_configuration: The board configuration to convert. + pn: The Process notifier to use. + assemble_for_fish_loading: Pass True if the XML is built for fish. + This will skip the UI and resolve virtual filters. Returns: The XML element containing the board configuration. See https://github.com/Mission-DMX/Docs/blob/main/FormatSchemes/ProjectFile/ShowFile_v0.xsd for more information + """ pn.current_step_description = "Creating document root." pn.total_step_count += 1 + len(board_configuration.scenes) + 3 @@ -81,13 +83,15 @@ def create_xml( def _create_board_configuration_element(board_configuration: BoardConfiguration) -> ET.Element: - """Creates an XML element of a type scene. + """Create an XML element of type `BoardConfiguration`. + + Example: + + ... + - - ... - """ # TODO we're not filling in the version attribute return ET.Element( @@ -104,7 +108,9 @@ def _create_board_configuration_element(board_configuration: BoardConfiguration) def _create_device_element(device: Device, parent: ET.Element) -> ET.Element: - """TODO implement patching of devices + """TODO: Implement patching of devices. + + Example: + - """ From 8baa99bc1d880c0633695c21a6427c45a247d9a8 Mon Sep 17 00:00:00 2001 From: CorsCodini <45790567+CorsCodini@users.noreply.github.com> Date: Fri, 8 Aug 2025 21:44:36 +0200 Subject: [PATCH 22/29] chg: set Minimum to 1 --- src/view/dialogs/fixture_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/dialogs/fixture_dialog.py b/src/view/dialogs/fixture_dialog.py index dd38c934..8e27a979 100644 --- a/src/view/dialogs/fixture_dialog.py +++ b/src/view/dialogs/fixture_dialog.py @@ -51,7 +51,7 @@ def __init__(self, fixture: UsedFixture, board_configuration: BoardConfiguration layout_fixture.addWidget(QLabel("Start Index"), 2, 0) self._start_index = QSpinBox() - self._start_index.setMinimum(0) + self._start_index.setMinimum(1) self._start_index.setMaximum(NUMBER_OF_CHANNELS) self._start_index.setValue(self._fixture.start_index + 1) self._start_index.textChanged.connect(self._validate_input) From f62c41ef7c1fad058d881f0d3f8c6d95b66edf1e Mon Sep 17 00:00:00 2001 From: CorsCodini <45790567+CorsCodini@users.noreply.github.com> Date: Sat, 9 Aug 2025 12:34:45 +0200 Subject: [PATCH 23/29] beautification --- src/model/patching/fixture_channel.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/model/patching/fixture_channel.py b/src/model/patching/fixture_channel.py index 0b3ef568..8299c438 100644 --- a/src/model/patching/fixture_channel.py +++ b/src/model/patching/fixture_channel.py @@ -1,4 +1,5 @@ """Channels of a Fixture""" + from enum import IntFlag from typing import Final @@ -7,6 +8,7 @@ class FixtureChannelType(IntFlag): """Types of channels of a fixture""" + UNDEFINED = 0 RED = 1 GREEN = 2 @@ -25,7 +27,8 @@ class FixtureChannelType(IntFlag): class FixtureChannel: """A channel of a fixture""" - updated: QtCore.Signal(int) = QtCore.Signal(int) + + updated: QtCore.Signal = QtCore.Signal(int) def __init__(self, name: str) -> None: self._name: Final[str] = name From 202fb5c5ba85d56cd2377da0f827e2e5cc1525af Mon Sep 17 00:00:00 2001 From: CorsCodini <45790567+CorsCodini@users.noreply.github.com> Date: Sat, 9 Aug 2025 12:36:51 +0200 Subject: [PATCH 24/29] Doku --- src/model/patching/fixture_channel.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/model/patching/fixture_channel.py b/src/model/patching/fixture_channel.py index 8299c438..596f672d 100644 --- a/src/model/patching/fixture_channel.py +++ b/src/model/patching/fixture_channel.py @@ -1,4 +1,4 @@ -"""Channels of a Fixture""" +"""Channel of a fixture.""" from enum import IntFlag from typing import Final @@ -7,7 +7,7 @@ class FixtureChannelType(IntFlag): - """Types of channels of a fixture""" + """Channels Types of a fixture.""" UNDEFINED = 0 RED = 1 @@ -26,33 +26,34 @@ class FixtureChannelType(IntFlag): class FixtureChannel: - """A channel of a fixture""" + """Channel of a fixture.""" updated: QtCore.Signal = QtCore.Signal(int) def __init__(self, name: str) -> None: + """Channel of a fixture.""" self._name: Final[str] = name self._type: Final[FixtureChannelType] = self._get_channel_type_from_string() self._ignore_black = True @property def name(self) -> str: - """Returns the name of the channel""" + """Name of the channel.""" return self._name @property def type(self) -> FixtureChannelType: - """Returns the channel type""" + """Type of the channel.""" return self._type @property def type_as_list(self) -> list[FixtureChannelType]: - """export the Fixture Channel Types as a list""" + """Export the Fixture Channel Types as a list.""" return [flag for flag in type(self._type) if flag in self._type] @property def ignore_black(self) -> bool: - """ignore this channel blackout""" + """Ignore this channel blackout.""" return self._ignore_black @ignore_black.setter @@ -60,7 +61,7 @@ def ignore_black(self, ignore_black: bool) -> None: self._ignore_black = ignore_black def _get_channel_type_from_string(self) -> FixtureChannelType: - """Returns the channel type""" + """Generate channel types from a string.""" types: FixtureChannelType = FixtureChannelType.UNDEFINED # TODO vielleicht aus OFL sauber extrahieren for channel_type in FixtureChannelType: From db2302153496cecb0f9b1ebcdb59d36e832205c4 Mon Sep 17 00:00:00 2001 From: CorsCodini <45790567+CorsCodini@users.noreply.github.com> Date: Sat, 9 Aug 2025 12:43:14 +0200 Subject: [PATCH 25/29] add: fixture can change universe --- src/model/ofl/fixture.py | 9 +++++-- src/view/dialogs/fixture_dialog.py | 24 +++++++++++++++---- .../patch_plan/patch_plan_selector.py | 12 +++++++++- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/model/ofl/fixture.py b/src/model/ofl/fixture.py index 320921b4..fd701cbc 100644 --- a/src/model/ofl/fixture.py +++ b/src/model/ofl/fixture.py @@ -67,6 +67,7 @@ class UsedFixture(QtCore.QObject): """Fixture in use with a specific mode.""" static_data_changed: QtCore.Signal = QtCore.Signal() + universe_changed: QtCore.Signal = QtCore.Signal(int) def __init__( self, @@ -99,7 +100,7 @@ def __init__( ) self._name_on_stage: str = self.short_name if self.short_name else self.name - self.parent_universe: int = parent_universe + self.parent_universe: int = parent_universe.id # TODO remove self._board_configuration.broadcaster.add_fixture.emit(self) @property @@ -156,6 +157,7 @@ def mode_index(self) -> int: @property def universe_id(self) -> int: """Universe for the fixture.""" + # TODO remove return self._universe.id @property @@ -165,7 +167,10 @@ def universe(self) -> Universe: @universe.setter def universe(self, universe: Universe) -> None: - self._universe = universe + if universe != self._universe: + old_id = self._universe + self._universe = universe + self.universe_changed.emit(old_id) @property def channel_length(self) -> int: diff --git a/src/view/dialogs/fixture_dialog.py b/src/view/dialogs/fixture_dialog.py index 8e27a979..9472baf4 100644 --- a/src/view/dialogs/fixture_dialog.py +++ b/src/view/dialogs/fixture_dialog.py @@ -7,6 +7,7 @@ import numpy as np from PySide6.QtWidgets import ( QColorDialog, + QComboBox, QDialog, QGridLayout, QHBoxLayout, @@ -49,21 +50,30 @@ def __init__(self, fixture: UsedFixture, board_configuration: BoardConfiguration self._name_on_stage = QLineEdit(self._fixture.name_on_stage) layout_fixture.addWidget(self._name_on_stage, 1, 1) - layout_fixture.addWidget(QLabel("Start Index"), 2, 0) + layout_fixture.addWidget(QLabel("Universum"), 2, 0) + self._universe = QComboBox() + for index, universe in enumerate(self._board_configuration.universes): + self._universe.addItem(universe.name, userData=universe) + if self._fixture.universe == universe: + self._universe.setCurrentIndex(index) + + layout_fixture.addWidget(self._universe, 2, 1) + + layout_fixture.addWidget(QLabel("Start Index"), 3, 0) self._start_index = QSpinBox() self._start_index.setMinimum(1) self._start_index.setMaximum(NUMBER_OF_CHANNELS) self._start_index.setValue(self._fixture.start_index + 1) self._start_index.textChanged.connect(self._validate_input) - layout_fixture.addWidget(self._start_index, 2, 1) + layout_fixture.addWidget(self._start_index, 3, 1) self._color_label = QLabel("Anzeigefarbe") self._selected_color = self._fixture.color_on_stage self._color_label.setStyleSheet(f"background-color: {self._fixture.color_on_stage.name()};") - layout_fixture.addWidget(self._color_label, 3, 0) + layout_fixture.addWidget(self._color_label, 4, 0) color_button = QPushButton("Farbe wählen") color_button.clicked.connect(self._open_color_picker) - layout_fixture.addWidget(color_button, 3, 1) + layout_fixture.addWidget(color_button, 4, 1) layout_error = QHBoxLayout() self._error_label = QLabel("No Error Found!") @@ -91,6 +101,7 @@ def _ok(self) -> None: self._fixture.color_on_stage = self._selected_color self._fixture.start_index = int(self._start_index.text()) - 1 self._fixture.name_on_stage = self._name_on_stage.text() + self._fixture.universe = self._universe.itemData(self._universe.currentIndex()) self.accept() def _cancel(self) -> None: @@ -117,7 +128,10 @@ def _validate_input(self) -> None: int(self._start_index.value() - 1), int(self._start_index.value() - 1) + self._fixture.channel_length ) - if np.isin(occupied, self._board_configuration.get_occupied_channels(self._fixture.universe)).any(): + if np.isin( + occupied, + self._board_configuration.get_occupied_channels(self._universe.itemData(self._universe.currentIndex())), + ).any(): self._error_label.setText("Channels already occupied!") self._error_label.setStyleSheet(style.LABEL_ERROR) return diff --git a/src/view/patch_view/patch_plan/patch_plan_selector.py b/src/view/patch_view/patch_plan/patch_plan_selector.py index 7bb0dfab..0fe7d5d5 100644 --- a/src/view/patch_view/patch_plan/patch_plan_selector.py +++ b/src/view/patch_view/patch_plan/patch_plan_selector.py @@ -2,6 +2,7 @@ from __future__ import annotations +from functools import partial from logging import getLogger from typing import TYPE_CHECKING, override @@ -35,6 +36,7 @@ def __init__(self, board_configuration: BoardConfiguration, parent: PatchMode) - self._broadcaster.add_fixture.connect(self._add_fixture) self._patch_planes: dict[int, AutoResizeView] = {} + self._fixture_items: dict[UsedFixture, UsedFixtureWidget] = {} self.setTabPosition(QtWidgets.QTabWidget.TabPosition.West) self.addTab(QtWidgets.QWidget(), "+") @@ -43,7 +45,15 @@ def __init__(self, board_configuration: BoardConfiguration, parent: PatchMode) - self.tabBar().setCurrentIndex(0) def _add_fixture(self, fixture: UsedFixture) -> None: - self._patch_planes[fixture.universe_id].scene().addItem(UsedFixtureWidget(fixture, self._board_configuration)) + new_widget = UsedFixtureWidget(fixture, self._board_configuration) + fixture.universe_changed.connect(partial(self._switch_universe, fixture)) + self._fixture_items[fixture] = new_widget + self._patch_planes[fixture.universe.id].scene().addItem(new_widget) + + def _switch_universe(self, fixture: UsedFixture, old_universe_id: int) -> None: + widget = self._fixture_items[fixture] + self._patch_planes[old_universe_id].scene().removeItem(widget) + self._patch_planes[fixture.universe.id].scene().addItem(widget) def _generate_universe(self) -> None: """Add a new Universe to universe Selector.""" From b144d97f3c9920b19810b10b8836052ba3d2cf17 Mon Sep 17 00:00:00 2001 From: CorsCodini <45790567+CorsCodini@users.noreply.github.com> Date: Sat, 9 Aug 2025 12:46:18 +0200 Subject: [PATCH 26/29] chg: use universe.id --- src/view/utility_widgets/wizzards/patch_plan_export.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/view/utility_widgets/wizzards/patch_plan_export.py b/src/view/utility_widgets/wizzards/patch_plan_export.py index 85ce7cde..4e60a38d 100644 --- a/src/view/utility_widgets/wizzards/patch_plan_export.py +++ b/src/view/utility_widgets/wizzards/patch_plan_export.py @@ -149,7 +149,7 @@ def _write_csv_file( self, fixtures: list[UsedFixture], phase_association: dict[UsedFixture, int], phases: Counter[int] ) -> None: """Given a fixture list with its phase schedule, this method writes the CSV file.""" - fixtures.sort(key=lambda f: f.universe_id * 512 + f.start_index) + fixtures.sort(key=lambda f: f.universe.id * 512 + f.start_index) with open(self._export_location_tb.text(), "w", newline="") as csv_file: logger.info("Exporting Fixtures as CSV to %s.", csv_file.name) writer = csv.writer(csv_file, delimiter=";") @@ -170,7 +170,7 @@ def _write_csv_file( [ fixture.name_on_stage or fixture.name, fixture.fixture_file, - str(fixture.universe_id), + str(fixture.universe.id), str(fixture.start_index), f"L{fixture_phase + 1}", str(fixture.power), From 7ec5bb8688b0ecce1551af030f6bfd5c7cfdcebd Mon Sep 17 00:00:00 2001 From: CorsCodini <45790567+CorsCodini@users.noreply.github.com> Date: Sat, 9 Aug 2025 12:50:58 +0200 Subject: [PATCH 27/29] Doku --- .../wizzards/patch_plan_export.py | 35 ++++++++----------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/src/view/utility_widgets/wizzards/patch_plan_export.py b/src/view/utility_widgets/wizzards/patch_plan_export.py index 4e60a38d..0326db4b 100644 --- a/src/view/utility_widgets/wizzards/patch_plan_export.py +++ b/src/view/utility_widgets/wizzards/patch_plan_export.py @@ -1,6 +1,4 @@ -""" -This file provides a wizard to automatically export the configured patch plan as a spreadsheet. -""" +"""Wizard for automatically exporting the configured patch plan as a spreadsheet.""" import csv import os.path @@ -31,13 +29,10 @@ class PatchPlanExportWizard(QWizard): - """ - This wizard guides the user to export the patching configuration as a CSV file, providing a power distribution - guide in the process. - """ + """Wizard to export the patching configuration as a CSV file, including a power distribution guide.""" def __init__(self, parent: QWidget, show_data: BoardConfiguration) -> None: - """Instantiate a new wizard object""" + """Wizard to export the patching configuration as a CSV file, including a power distribution guide.""" super().__init__(parent) self.setModal(True) self.setMinimumSize(600, 300) @@ -92,19 +87,16 @@ def __init__(self, parent: QWidget, show_data: BoardConfiguration) -> None: self._show = show_data def _select_export_location(self) -> None: - """This button callback prompts the user to select an CSV file export destination.""" + """Prompt the user to select a CSV file export destination.""" self._file_selection_dialog.show() def _export_location_selected(self, file_name: str) -> None: - """This callback applies the path of the user-selected location into the text box.""" + """Apply the path of the user-selected location to the text box.""" self._export_location_tb.setText(file_name) self._first_page.completeChanged.emit() def _load_fixture_list(self, _: ComposableWizardPage) -> None: - """ - This method loads all available fixtures into the list widget, - prompting the user to select the one desired for export. - """ + """Load all available fixtures into the list widget and prompt the user to select one for export.""" for fixture in self._show.fixtures: item = AnnotatedListWidgetItem(self._fixture_list) item.setText(str(fixture)) @@ -113,7 +105,7 @@ def _load_fixture_list(self, _: ComposableWizardPage) -> None: item.setCheckState(Qt.CheckState.Checked) def _commit_changes(self, _: ComposableWizardPage) -> bool: - """After the user finished the wizard, the export will be generated using this method.""" + """Generate the export after the user finishes the wizard.""" pn = get_process_notifier("Export Fixtures to CSV list", 3) pn.current_step_description = "Loading Fixtures" pn.current_step_number = 0 @@ -148,7 +140,7 @@ def _commit_changes(self, _: ComposableWizardPage) -> bool: def _write_csv_file( self, fixtures: list[UsedFixture], phase_association: dict[UsedFixture, int], phases: Counter[int] ) -> None: - """Given a fixture list with its phase schedule, this method writes the CSV file.""" + """Write a CSV file for a fixture list with its phase schedule.""" fixtures.sort(key=lambda f: f.universe.id * 512 + f.start_index) with open(self._export_location_tb.text(), "w", newline="") as csv_file: logger.info("Exporting Fixtures as CSV to %s.", csv_file.name) @@ -181,12 +173,13 @@ def _write_csv_file( def _schedule_phases( self, fixtures: list[UsedFixture], phase_association: dict[UsedFixture, int], phases: Counter[int] ) -> None: - """ - This method distributes the fixtures on the available power phases. + """Distribute fixtures on the available power phases. + + Args: + fixtures: The list of fixtures to use. + phase_association: The mapping of fixtures to their phases. This dictionary will be filled in. + phases: The load on each power phase in watts. - :param fixtures: The fixture list to use - :param phase_association: The mapping of fixtures to their phases. This dictionary will be filled in. - :param phases: The load on each power phase in Watt. """ number_of_phases = self._number_phases_sb.value() fixtures.sort(key=lambda f: f.physical.power, reverse=True) From e9e890b30cece28de0ea24313de67871f3036b9e Mon Sep 17 00:00:00 2001 From: Doralitze Date: Sun, 22 Feb 2026 14:02:00 +0100 Subject: [PATCH 28/29] add: sample unit test config file --- .../virtual_filters/color_to_colorwheel.py | 138 ++++++++++++++++++ test/unittests/.gitignore | 1 + test/unittests/config.sample.py | 1 + 3 files changed, 140 insertions(+) create mode 100644 src/model/virtual_filters/color_to_colorwheel.py create mode 100644 test/unittests/.gitignore create mode 100644 test/unittests/config.sample.py diff --git a/src/model/virtual_filters/color_to_colorwheel.py b/src/model/virtual_filters/color_to_colorwheel.py new file mode 100644 index 00000000..104aac5e --- /dev/null +++ b/src/model/virtual_filters/color_to_colorwheel.py @@ -0,0 +1,138 @@ +"""Contains ColorToColorwheel vFilter.""" + +from __future__ import annotations + +import os +from typing import TYPE_CHECKING, override + +from jinja2 import Environment + +from model.filter import Filter, VirtualFilter, FilterTypeEnumeration +from utility import resource_path + +if TYPE_CHECKING: + from jinja2.environment import Template + + from model import Scene + +with open(resource_path(os.path.join("resources", "data", "color-to-colorwheel-template.lua.j2")), "r") as f: + _FILTER_CONTENT_TEMPLATE: Template = Environment().from_string(f.read()) + + +class ColorToColorWheel(VirtualFilter): + """A vFilter that takes a color channel as an input and maps it to a color wheel channel.""" + + def __init__(self, scene: Scene, filter_id: str, pos: tuple[int] | None = None) -> None: + """Initialize the vFilter. + + The following filter configuration properties are available: + - mode: Either automatic (the filter loads its color points from the fixture) or manual. + - fixture-uuid: The fixture UUID to use in automatic mode. + - color-mappings: The color mappings to use in manual mode. Format: h:s:cw;h:s:cw;... + - dimmer-input: Should a dimmer input be used? Can either be "8bit", "16bit", "float" or "". + - dimmer-output: The data type of the dimmer output channel. Can either be "8bit", "16bit", "float" or "". + - colorwheel-datatype: The data type of the color wheel slot. Can either be "8bit" or "16bit". + - wheel_speed: Time in ms to switch between two adjacent wheel slots in manual mode. + - dim_when_off: If true, the brightness will be suspended as long as the wrong color wheel slot is dialed in. + + The following ports are provided: + - input: The color input channel (input, color) + - in_dimmer: Optional, Input dimmer mixin (input, 8bit or 16bit or float) + - dimmer: Output dimmer channel (output, 8bit or 16bit or float) + - colorwheel: The color wheel slot to use (output, 8bit or 16bit) + + Args: + scene: The scene to use for this filter. + filter_id: The id of the filter to use. + pos: Optional, The position of the filter to use. + + """ + super().__init__(scene, filter_id, FilterTypeEnumeration.VFILTER_COLOR_TO_COLORWHEEL, pos=pos) + if "mode" not in self._filter_configurations: + self.filter_configurations["mode"] = "automatic" + if "fixture-uuid" not in self._filter_configurations: + self.filter_configurations["fixture-uuid"] = "" + if "color-mappings" not in self._filter_configurations: + self.filter_configurations["color-mappings"] = "" + if "dimmer-input" not in self._filter_configurations: + self.filter_configurations["dimmer-input"] = "8bit" + if "dimmer-output" not in self._filter_configurations: + self.filter_configurations["dimmer-output"] = "" + if "colorwheel-datatype" not in self._filter_configurations: + self.filter_configurations["colorwheel-datatype"] = "8bit" + if not "wheel_speed" in self.filter_configurations: + self.filter_configurations["wheel_speed"] = "300" + if not "dim_when_off" in self.filter_configurations: + self.filter_configurations["dim_when_off"] = "true" + + @override + def resolve_output_port_id(self, virtual_port_id: str) -> str | None: + if virtual_port_id not in ["dimmer", "colorwheel"]: + raise ValueError(f"Invalid virtual port ID. Filter ID: {self.filter_id}, Requested Port: {virtual_port_id}") + return f"{self.filter_id}:{virtual_port_id}" + + @override + def instantiate_filters(self, filter_list: list[Filter]) -> None: + if self.filter_configurations["mode"] == "automatic": + # TODO + raise ValueError(f"Automatic mode is currently not implemented. Filter ID: {self.filter_id}") + input_dimmer_channel = self.channel_links.get("in_dimmer", "") + hue_values: list[float] = [] + saturation_values: list[float] = [] + slots: list[int] = [] + required_dimmer_output_data_type = self.filter_configurations["dimmer-output"] + dimmer_input_data_type = self.filter_configurations["dimmer-input"] + + colorwheel_is_16bit = self.filter_configurations["colorwheel-datatype"] == "16bit" + + input_dimmer_configured = input_dimmer_channel != "" + dimmer_output_required = required_dimmer_output_data_type != "" + wheel_speed = self.filter_configurations.get("wheel_speed", "300") + + for mapping_str in self.filter_configurations.get("color-mappings", "").split(";"): + hue, saturation, slot = mapping_str.split(":") + hue_values.append(float(hue)) + saturation_values.append(float(saturation)) + slots.append(int(slot)) + + match required_dimmer_output_data_type: + case "8bit": + output_dimmer_multiplier = "255" + case "16bit": + output_dimmer_multiplier = "65535" + case _: + output_dimmer_multiplier = "1" + + match dimmer_input_data_type: + case "8bit": + input_dimmer_multiplier = "255" + case "16bit": + input_dimmer_multiplier = "65535" + case _: + input_dimmer_multiplier = "1" + + script = _FILTER_CONTENT_TEMPLATE.render({ + "input_dimmer_channel_connected": input_dimmer_configured, + "input_dimmer_channel_data_type": dimmer_input_data_type, + "input_dimmer_multiplier": input_dimmer_multiplier, + "hue_values": hue_values, + "saturation_values": saturation_values, + "slots": slots, + "wheel_speed": wheel_speed, + "dimmer_output_required": dimmer_output_required, + "output_dimmer_data_type": required_dimmer_output_data_type, + "output_dimmer_multiplier": output_dimmer_multiplier, + "dim_when_off": self.filter_configurations.get("dim_when_off", "true") == "true", + "colorwheel_datatype": self.filter_configurations.get("colorwheel-datatype", "8bit"), + }) + + f = Filter(self.scene, self.filter_id, FilterTypeEnumeration.FILTER_SCRIPTING_LUA, pos=self.pos) + f.initial_parameters["script"] = script + f.filter_configurations["in_mapping"] = "input:color;time:float" + if input_dimmer_configured: + f.filter_configurations["in_mapping"] += f";in_dimmer:{dimmer_input_data_type}" + f.filter_configurations["out_mapping"] = f"colorwheel:{"16bit" if colorwheel_is_16bit else "8bit"}" + if dimmer_output_required: + f.filter_configurations["out_mapping"] += f";dimmer:{required_dimmer_output_data_type}" + f.channel_links.update(self.channel_links) + filter_list.append(f) diff --git a/test/unittests/.gitignore b/test/unittests/.gitignore new file mode 100644 index 00000000..f85c6b18 --- /dev/null +++ b/test/unittests/.gitignore @@ -0,0 +1 @@ +config.py \ No newline at end of file diff --git a/test/unittests/config.sample.py b/test/unittests/config.sample.py new file mode 100644 index 00000000..c4c41659 --- /dev/null +++ b/test/unittests/config.sample.py @@ -0,0 +1 @@ +FISH_EXEC_PATH = "../realtime-fish/bin/fish" \ No newline at end of file From 65ce25bffdd476b8a0fbc44b46dda2d52324d829 Mon Sep 17 00:00:00 2001 From: Doralitze Date: Sun, 22 Feb 2026 15:47:10 +0100 Subject: [PATCH 29/29] add: fixture inst unit test --- src/model/ofl/fixture_library_loader.py | 33 ++++++++ .../virtual_filters/auto_tracker_filter.py | 8 +- .../patch_view/patching/patching_select.py | 25 +----- test/unittests/fixture_inst_test.py | 82 +++++++++++++++++++ test/unittests/test_universe.py | 14 ++++ 5 files changed, 140 insertions(+), 22 deletions(-) create mode 100644 src/model/ofl/fixture_library_loader.py create mode 100644 test/unittests/fixture_inst_test.py create mode 100644 test/unittests/test_universe.py diff --git a/src/model/ofl/fixture_library_loader.py b/src/model/ofl/fixture_library_loader.py new file mode 100644 index 00000000..8c91f641 --- /dev/null +++ b/src/model/ofl/fixture_library_loader.py @@ -0,0 +1,33 @@ +import os +import zipfile +from logging import getLogger + +import requests + +logger = getLogger(__name__) + +def ensure_standard_fixture_library_exists(prefix="/var/cache/missionDMX") -> tuple[bool, str]: + """This function ensures that the standard library fixture library exists. + + This method will attempt to download the library if it is missing. + + :return: True if the standard library fixture library exists or was successfully downloaded, otherwise False. + """ + if not os.path.exists(prefix): + os.mkdir(prefix) + fixtures_path = os.path.join(prefix, "fixtures/") + if not os.path.exists(fixtures_path): + zip_path = os.path.join(prefix, "fixtures.zip") + logger.info("Downloading fixture library. Please wait") + url = "https://open-fixture-library.org/download.ofl" + r = requests.get(url, allow_redirects=True, timeout=5) + if r.status_code != 200: + logger.error("Failed to download fixture library") + return False, None + + with open(zip_path, "wb") as file: + file.write(r.content) + with zipfile.ZipFile(zip_path) as zip_ref: + zip_ref.extractall(fixtures_path) + logger.info("Fixture lib downloaded and installed.") + return True, fixtures_path \ No newline at end of file diff --git a/src/model/virtual_filters/auto_tracker_filter.py b/src/model/virtual_filters/auto_tracker_filter.py index 19ce8265..fd7cc03b 100644 --- a/src/model/virtual_filters/auto_tracker_filter.py +++ b/src/model/virtual_filters/auto_tracker_filter.py @@ -1,6 +1,12 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + from model import Filter, Scene from model.filter import DataType, FilterTypeEnumeration, VirtualFilter -from view.show_mode.show_ui_widgets.autotracker.v_filter_light_controller import VFilterLightController + +if TYPE_CHECKING: + from view.show_mode.show_ui_widgets.autotracker.v_filter_light_controller import VFilterLightController class _MHControlInstance: diff --git a/src/view/patch_view/patching/patching_select.py b/src/view/patch_view/patching/patching_select.py index abc338af..5ad2bb57 100644 --- a/src/view/patch_view/patching/patching_select.py +++ b/src/view/patch_view/patching/patching_select.py @@ -2,16 +2,14 @@ from __future__ import annotations -import os -import zipfile from logging import getLogger from typing import TYPE_CHECKING -import requests from PySide6 import QtWidgets import style from layouts.flow_layout import FlowLayout +from model.ofl.fixture_library_loader import ensure_standard_fixture_library_exists from model.ofl.manufacture import Manufacture, generate_manufacturers from view.dialogs.patching_dialog import PatchingDialog from view.patch_view.patching.fixture_item import FixtureItem @@ -32,24 +30,9 @@ class PatchingSelect(QtWidgets.QScrollArea): def __init__(self, board_configuration: BoardConfiguration, parent: QWidget) -> None: super().__init__(parent) self._board_configuration = board_configuration - cache_path = "/var/cache/missionDMX" - if not os.path.exists(cache_path): - os.mkdir(cache_path) - fixtures_path = os.path.join(cache_path, "fixtures/") - zip_path = os.path.join(cache_path, "fixtures.zip") - if not os.path.exists(fixtures_path): - logger.info("Downloading fixture library. Please wait") - url = "https://open-fixture-library.org/download.ofl" - r = requests.get(url, allow_redirects=True, timeout=5) - if r.status_code != 200: - logger.error("Failed to download fixture library") - return - - with open(zip_path, "wb") as file: - file.write(r.content) - with zipfile.ZipFile(zip_path) as zip_ref: - zip_ref.extractall(fixtures_path) - logger.info("Fixture lib downloaded and installed.") + library_exists, fixtures_path = ensure_standard_fixture_library_exists() + if not library_exists: + return manufacturers: list[tuple[Manufacture, list[OflFixture]]] = generate_manufacturers(fixtures_path) self.index = 0 self.container = QtWidgets.QStackedWidget() diff --git a/test/unittests/fixture_inst_test.py b/test/unittests/fixture_inst_test.py new file mode 100644 index 00000000..00f11385 --- /dev/null +++ b/test/unittests/fixture_inst_test.py @@ -0,0 +1,82 @@ +import json +import os +import shutil +import unittest +import urllib.request +from logging import getLogger + +from model import BoardConfiguration, Scene +from model.filter import FilterTypeEnumeration +from model.ofl.fixture import load_fixture, UsedFixture +from model.ofl.fixture_library_loader import ensure_standard_fixture_library_exists +from model.scene import FilterPage +from test.unittests.test_universe import TestUniverse +from view.show_mode.editor.show_browser.fixture_to_filter import place_fixture_filters_in_scene + +TEST_FIXTURE_SET_URL = "https://raw.githubusercontent.com/OpenLightingProject/open-fixture-library/refs/heads/master/tests/test-fixtures.json" + +logger = getLogger(__name__) + +class FixtureInstantiationTest(unittest.TestCase): + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + tmp_fixture_lib_prefix = "/tmp/missiondmx-unittests/fixturetests" + logger.info("Initializing FixtureInstantiationTest (Downloading fixtures to %s)", tmp_fixture_lib_prefix) + if os.path.exists(tmp_fixture_lib_prefix): + shutil.rmtree(tmp_fixture_lib_prefix) + os.makedirs(tmp_fixture_lib_prefix) + with urllib.request.urlopen(TEST_FIXTURE_SET_URL) as f: + self._test_fixture_set = json.load(f) + self._library_exists, self._library_path = ensure_standard_fixture_library_exists(prefix=tmp_fixture_lib_prefix) + logger.info("Done.") + + def test_set_fixtures(self): + """This test simply ensurers that all capabilities can be instantiated.""" + self.assertTrue(self._library_exists) + for fixture_request in self._test_fixture_set: + name = fixture_request["key"] + ".json" + manufacturer = fixture_request["man"] + capabilities = fixture_request["features"] + print("Testing fixture ", name) + bc = BoardConfiguration() + scene = Scene(0, f"Test scene for fixture {name}", bc) + bc._add_scene(scene) + fp = FilterPage(scene) + scene.insert_filterpage(fp) + fixture_template = load_fixture(os.path.join(self._library_path, manufacturer, name)) + model_index = len(fixture_template.modes) - 1 + universe = TestUniverse() + used_fixture = UsedFixture(bc, fixture_template, model_index, universe, 1) + place_fixture_filters_in_scene(used_fixture, fp) + self.assertTrue(len(fp.filters) > 0) + found_required_filter = False + for filter in fp.filters: + if filter.filter_type == FilterTypeEnumeration.VFILTER_UNIVERSE: + found_required_filter = True + self.assertTrue(found_required_filter, "Expecting filter inst to produce Universe output VFilter for fixture.") + if "fine-channel-alias" in capabilities: + found_required_filter = False + for filter in fp.filters: + if filter.filter_type == FilterTypeEnumeration.FILTER_ADAPTER_16BIT_TO_DUAL_8BIT: + found_required_filter = True + self.assertTrue(found_required_filter, "Expecting a 16bit to dual 8bit filter as fine channels exist. " + "Found the following filter types: " + ", ".join( + str(FilterTypeEnumeration(f.filter_type).name) for f in fp.filters + )) + + def test_segment_group_loading(self): + # TODO Matrix channel inserts should automatically group their channels in order to identify colors + # fixture capabilities should be grouped and instantiation should use them. All Non-grouped channels should be + # part of a single default group. + self.assertTrue(False, "This test is not yet implemented.") + + def test_instantiation_of_fine_channels(self): + # TODO for a given fixture, the pan/tilt channels should be grouped accordingly based on their "fineChannelAliases" + # TODO Make sure that only channels with capability.type=Pan or capability.type=Tilt get the pan tilt constant + # TODO Make sure that channels with capability.type="WheelSlot" and matching wheel name with color filters receive a colo2colorwheel v-filter + self.assertTrue(False, "This test is not yet implemented.") + + def test_gobo_resource_loading(self): + # TODO load a fixture with gobos (for example spot 230) and make sure the gobos are available as an asset afterwards + self.assertTrue(False, "This test is not yet implemented.") diff --git a/test/unittests/test_universe.py b/test/unittests/test_universe.py new file mode 100644 index 00000000..d547e8eb --- /dev/null +++ b/test/unittests/test_universe.py @@ -0,0 +1,14 @@ +from model import Universe +import proto.UniverseControl_pb2 + +issued_test_universes = 0 + +class TestUniverse(Universe): + """Test Universe mockup""" + + def __init__(self): + global issued_test_universes + definition = proto.UniverseControl_pb2.Universe(id=issued_test_universes) + #definition.name = "Test Universe" + issued_test_universes += 1 + super().__init__(definition)