Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a5f7ebb
add: option to remove assets
Doralitze Jan 10, 2026
63f36dd
add: option to edit icons of macro buttons
Doralitze Jan 10, 2026
66caaf5
add: dialog to ask for exit
Doralitze Jan 10, 2026
f1c056e
fix: duplicate declaration of closing event in main
Doralitze Jan 12, 2026
25f4180
fix: main window close callback to be private
Doralitze Jan 15, 2026
5afc4c6
add: dmx default value support to scene model
Doralitze Apr 15, 2026
e474638
mrg: branch '169-color-to-color-wheel-adapter-v-filter' into scene-de…
Doralitze Apr 15, 2026
c17dd43
mrg: branch 'media-asset-config-ui' into scene-default-dmx-values
Doralitze Apr 15, 2026
bf70dc5
mrg: branch 'dimmer-brightness-mixin-vfilter' into scene-default-dmx-…
Doralitze Apr 15, 2026
4270515
fix: ruff issues
Doralitze Apr 15, 2026
91ff1d5
add: skeleton
Doralitze Apr 15, 2026
81e7154
add: tab loading and closing mechanics
Doralitze Apr 15, 2026
1722447
add: crude editing UI
Doralitze Apr 15, 2026
fadad97
fix: inverted update bug
Doralitze Apr 15, 2026
4ce9b85
fix: docs in showmanager.py
Doralitze Apr 15, 2026
650b463
fix: typo
Doralitze Apr 15, 2026
4521faf
add: entry removal option
Doralitze Apr 16, 2026
d63816c
add: default mapping from console
Doralitze Apr 16, 2026
da301da
add: reasonable error message for missing fixture definitions
Doralitze Apr 20, 2026
19affa7
fix: saving of show files
Doralitze Apr 21, 2026
a1463fb
fix: context menu for single value add
Doralitze Apr 21, 2026
a2161b2
Merge branch 'main' into scene-default-dmx-values
CorsCodini May 10, 2026
6521fbf
mrg: branch 'main' into scene-default-dmx-values
Doralitze May 11, 2026
6158a02
fix: docs in model/universe.py
Doralitze May 11, 2026
38ca5f5
fix: crash on outdated fixture loading (missing mode)
Doralitze May 14, 2026
106f6cf
upd: docs of fixture.mode
Doralitze May 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 27 additions & 8 deletions src/controller/file/read.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import xmlschema
from defusedxml.ElementTree import parse
from PySide6.QtWidgets import QMessageBox

import proto.Console_pb2
import proto.UniverseControl_pb2
Expand All @@ -21,7 +22,7 @@
from model.media_assets.asset_loading_factory import load_asset
from model.media_assets.factory_hint import AssetFactoryObjectHint
from model.media_assets.registry import clear as clear_media_registry
from model.ofl.fixture import load_fixture, make_used_fixture
from model.ofl.fixture import FixtureDefNotFoundError, load_fixture, make_used_fixture
from model.scene import FilterPage
from model.virtual_filters.vfilter_factory import construct_virtual_filter_instance
from utility import resource_path
Expand Down Expand Up @@ -265,6 +266,14 @@ def _parse_filter_page(element: ET.Element, parent_scene: Scene, instantiated_pa
return True


def _parse_dmx_default_value(scene: Scene, child: ET.Element) -> None:
scene.insert_dmx_default_value(
int(child.attrib["universe"]),
int(child.attrib["channel"]),
int(child.attrib["value"])
)


def _parse_scene(
scene_element: ET.Element, board_configuration: BoardConfiguration, loaded_banksets: dict[str, BankSet]
) -> None:
Expand Down Expand Up @@ -301,6 +310,8 @@ def _parse_scene(
filter_pages.append(child)
case "uipage":
ui_page_elements.append(child)
case "dmxdefaultvalue":
_parse_dmx_default_value(scene, child)
case _:
logger.warning("Scene %s contains unknown element: %s", human_readable_name, child.tag)

Expand Down Expand Up @@ -652,13 +663,21 @@ def _parse_patching(board_configuration: BoardConfiguration, location_element: E
fixtures_path = "/var/cache/missionDMX/fixtures" # TODO config file

for child in location_element:
make_used_fixture(
board_configuration,
load_fixture(os.path.join(fixtures_path, child.attrib["fixture_file"])),
int(child.attrib["mode"]),
universe_id,
int(child.attrib["start"]),
)
try:
make_used_fixture(
board_configuration,
load_fixture(os.path.join(fixtures_path, child.attrib["fixture_file"])),
int(child.attrib["mode"]),
universe_id,
int(child.attrib["start"]),
)
except FixtureDefNotFoundError as e:
# Calling Dialog exec is not an issue here as we're in the process of loading the show file anyway
mb = QMessageBox(QMessageBox.Icon.Critical, "Failed to load fixture", str(e) +
"\n\nDo not continue until this error is fixed as the show is now corrupted.\nMaybe try "
"updating the fixture database.")
mb.exec_()
continue

# TODO load fixture name from file

Expand Down
7 changes: 7 additions & 0 deletions src/controller/file/serializing/scene_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@ def generate_scene_xml_description(

for ui_page in scene.ui_pages:
_add_ui_page_to_element(scene_element, ui_page)
scene.sort_dmx_default_values()
for default_value in scene.dmx_default_values:
ET.SubElement(scene_element, "dmxdefaultvalue", attrib={
"universe": str(default_value.universe_id),
"channel": str(default_value.channel),
"value": str(default_value.value),
})


def _create_scene_element(scene: Scene, parent: ET.Element) -> ET.Element:
Expand Down
5 changes: 3 additions & 2 deletions src/model/board_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,13 +330,14 @@ def get_fixture_by_address(self, fixture_univ: int, fixture_chan: int) -> UsedFi

Args:
fixture_univ: The universe of the fixture.
fixture_chan: The first channel of the fixture.
fixture_chan: A channel of the fixture.

Returns:
The fixture or None if no fixture was found.

"""
for fixture in self._fixtures.values():
if fixture.universe_id == fixture_univ and fixture.start_index == fixture_chan:
if fixture.universe_id == fixture_univ and \
fixture.start_index <= fixture_chan < fixture.start_index + fixture.channel_length:
return fixture
return None
1 change: 1 addition & 0 deletions src/model/broadcaster.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class Broadcaster(QtCore.QObject, metaclass=QObjectSingletonMeta):
scene_open_in_editor_requested: QtCore.Signal = QtCore.Signal(object) # FilterPage
bankset_open_in_editor_requested: QtCore.Signal = QtCore.Signal(dict)
uipage_opened_in_editor_requested: QtCore.Signal = QtCore.Signal(dict)
default_dmx_value_editor_opening_requested: QtCore.Signal = QtCore.Signal(object)
delete_scene: QtCore.Signal = QtCore.Signal(object)
delete_universe: QtCore.Signal = QtCore.Signal(object)
device_created: QtCore.Signal = QtCore.Signal(object) # device
Expand Down
20 changes: 15 additions & 5 deletions src/model/channel.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,47 @@
"""Basic dmx channel with 256 values"""
"""Basic dmx channel with 256 values."""

from __future__ import annotations

from typing import TYPE_CHECKING

from PySide6 import QtCore

if TYPE_CHECKING:
from model import Universe

class Channel(QtCore.QObject):
"""Basic dmx channel with 256 values"""
"""Basic dmx channel with 256 values."""

updated: QtCore.Signal = QtCore.Signal(int)

def __init__(self, channel_address: int) -> None:
def __init__(self, parent_universe: Universe, channel_address: int) -> None:
"""Constructs a channel."""
super().__init__(None)
self.parent_universe = parent_universe
if not (0 <= channel_address <= 511):
raise ValueError(f"Tried to create a channel with address {channel_address}")
self._address: int = channel_address
self._value: int = 0

@property
def address(self) -> int:
"""Address of the channel. 0-indexed"""
"""Address of the channel. 0-indexed."""
return self._address

@property
def value(self) -> int:
"""The current value of the channel"""
"""The current value of the channel."""
return self._value

@value.setter
def value(self, value: int) -> None:
"""Updates the value of the channel.

Must be between 0 and 255.

Raises:
ValueError: The value is below 0 or above 255.

"""
if not (0 <= value <= 511):
raise ValueError(f"Tried to set channel {self._address} to {value}.")
Expand Down
6 changes: 5 additions & 1 deletion src/model/media_assets/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from typing import TYPE_CHECKING
from uuid import uuid4

from model.media_assets.registry import register
from model.media_assets.registry import register, unregister

if TYPE_CHECKING:
from PySide6.QtGui import QPixmap
Expand Down Expand Up @@ -102,3 +102,7 @@ def is_local_resource(self) -> bool:
Non-local resources need to be provided and copied together with the show file.
"""
return False

def unregister(self) -> None:
"""Unregister this asset."""
unregister(self)
19 changes: 19 additions & 0 deletions src/model/media_assets/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,25 @@ def register(asset: MediaAsset, uuid: str) -> bool:
d[uuid] = asset
return True

def unregister(asset: MediaAsset) -> bool:
"""Method unregisters a media asset.

Args:
asset: the media asset to unregister

Returns:
True if the asset was successfully unregistered, False otherwise.

"""
reg = _asset_library.get(asset.get_type())
if reg is None:
return False
try:
reg.pop(asset.id)
return True
except KeyError:
return False

def get_asset_by_uuid(uuid: str) -> MediaAsset | None:
"""Get a media asset by its UUID.

Expand Down
35 changes: 32 additions & 3 deletions src/model/ofl/fixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,26 @@ def __str__(self) -> str:
return "+".join(s)


class FixtureDefNotFoundError(Exception):
"""Exception raised when fixture definition could not be found on disk."""

def __init__(self, fixture_path: str, further_info: str) -> None:
"""Initialize with default message and provided fixture info."""
super().__init__("Fixture Definition Not Found")
self.fixture_path = fixture_path
self.further_info = further_info

def __str__(self) -> str:
"""Generate reasonable error message for observing human."""
return f"Failed to load fixture {self.fixture_path}.\n{"File not Found.\n" if not
os.path.exists(self.fixture_path) else ""}Further info: {self.further_info}"


def load_fixture(file: str) -> OflFixture | None:
"""Load fixture from OFL JSON."""
if not os.path.isfile(file):
logger.error("Fixture definition %s not found.", file)
return None
raise FixtureDefNotFoundError(file, "Path is no file. Does it exist?")
with open(file, "r", encoding="UTF-8") as f:
try:
ob: dict = json.load(f)
Expand Down Expand Up @@ -198,7 +213,17 @@ def comment(self) -> str:

@property
def mode(self) -> FixtureMode:
"""Mode of theFixture."""
"""Mode of theFixture.

Raises:
FixtureDefNotFoundError if the fixture mode does not exist.

"""
if len(self._fixture.modes) <= self._mode_index:
raise FixtureDefNotFoundError(
self._fixture.fileName,
"Fixture does not have requested mode. Are the fixture defintions up to date?"
)
return self._fixture.modes[self._mode_index]

@property
Expand Down Expand Up @@ -325,4 +350,8 @@ def make_used_fixture(
board_configuration: BoardConfiguration, fixture: OflFixture, mode_index: int, universe_id: int, start_index: int
) -> UsedFixture:
"""Generate a new Used Fixture from a oflFixture."""
return UsedFixture(board_configuration, fixture, mode_index, universe_id, start_index)
try:
return UsedFixture(board_configuration, fixture, mode_index, universe_id, start_index)
except ValueError as e:
logger.error(e)
raise FixtureDefNotFoundError(fixture.fileName, str(e)) from e
83 changes: 81 additions & 2 deletions src/model/scene.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

from __future__ import annotations

from typing import TYPE_CHECKING, override
from typing import TYPE_CHECKING, NamedTuple, override

from PySide6.QtCore import QObject, Signal

from .universe import Universe

if TYPE_CHECKING:
from .board_configuration import BoardConfiguration
Expand Down Expand Up @@ -62,11 +66,22 @@ def copy(self, new_scene: Scene = None) -> FilterPage:
return new_fp


class Scene:
class DmxDefaultValue(NamedTuple):
"""Contains a single default entry to be applied on scene activation by fish."""

universe_id: int
channel: int
value: int


class Scene(QObject):
"""Scene for a show file."""

default_values_changed = Signal()

def __init__(self, scene_id: int, human_readable_name: str, board_configuration: BoardConfiguration) -> None:
"""Scene for a show file."""
super().__init__()
self._scene_id: int = scene_id
self._human_readable_name: str = human_readable_name
self._board_configuration: BoardConfiguration = board_configuration
Expand All @@ -75,6 +90,7 @@ def __init__(self, scene_id: int, human_readable_name: str, board_configuration:
self._filter_pages: list[FilterPage] = []
self._associated_bankset: BankSet | None = None
self._ui_pages: list[UIPage] = []
self._dmx_default_values: list[DmxDefaultValue] = []

@property
def scene_id(self) -> int:
Expand Down Expand Up @@ -116,6 +132,58 @@ def pages(self) -> list[FilterPage]:
self._filter_pages.append(default_page)
return self._filter_pages

@property
def dmx_default_values(self) -> list[DmxDefaultValue]:
"""Get the list of default values to be applied on scene switch."""
return self._dmx_default_values.copy()

def insert_dmx_default_value(self, universe: Universe | int, channel: int, value: int,
supress_emission: bool = False) -> bool:
"""Add a new default value to the scene.

Existing values will be updated.

Args:
universe: target universe or its ID.
channel: target channel.
value: value to set on scene entry.
supress_emission: If this is enabled to change signal will be enabled. Only use this if you're certain
you're taking care of all updates yourself.

Returns:
True if a value was updated and false if it was added.

"""
universe_id = universe.id if isinstance(universe, Universe) else universe
value_to_remove = None
for existing_value in self._dmx_default_values:
if existing_value.universe_id == universe_id and existing_value.channel == channel:
value_to_remove = existing_value
if value_to_remove is not None:
self._dmx_default_values.remove(value_to_remove)
self._dmx_default_values.append(DmxDefaultValue(universe_id, channel, value))
if not supress_emission:
self.default_values_changed.emit()
return value_to_remove is not None

def remove_dmx_default_value(self, universe: Universe | int, channel: int, supress_emission: bool = False) -> None:
"""Remove a default DMX value from the scene.

Args:
universe: target universe or its ID.
channel: target channel.
supress_emission: If this is enabled to change signal will be enabled. Only use this if you're certain
you're taking care of all updates yourself.

"""
universe_id = universe.id if isinstance(universe, Universe) else universe
values_to_remove = [val for val in self._dmx_default_values if
val.universe_id == universe_id and val.channel == channel]
for item in values_to_remove:
self._dmx_default_values.remove(item)
if len(values_to_remove) > 0 and not supress_emission:
self.default_values_changed.emit()

def insert_filterpage(self, fp: FilterPage) -> None:
"""Add a filterpage to the scene."""
self._filter_pages.append(fp)
Expand Down Expand Up @@ -184,6 +252,8 @@ def copy(self, existing_scenes: list[Scene]) -> Scene:
scene.linked_bankset = self._associated_bankset.copy()
for page in self._ui_pages:
scene._ui_pages.append(page.copy(scene))
for ddv in self._dmx_default_values:
scene._dmx_default_values.append(ddv)
return scene

def get_filter_by_id(self, fid: str) -> Filter | None:
Expand Down Expand Up @@ -266,3 +336,12 @@ def notify_about_filter_rename_action(self, sender: Filter, old_id: str) -> None
for page in self._ui_pages:
for widget in page.widgets:
widget.notify_id_rename(old_id, sender.filter_id)

def sort_dmx_default_values(self) -> None:
"""Sorts the dmx defaults by their universe and channel.

This improves human interaction and performance of fish.

"""
self._dmx_default_values.sort(key=lambda x: (x.universe_id * 512) + x.channel)
self.default_values_changed.emit()
Loading
Loading