Add EntitySelect/IconSelect UI improvements across modals
- Portal IconSelect popups to document.body with position:fixed to prevent clipping by modal overflow-y:auto - Replace custom scene selectors in automation editor with EntitySelect command-palette pickers (main scene + fallback scene) - Add IconSelect grid for automation deactivation mode (none/revert/fallback) - Add IconSelect grid for automation condition type and match type - Replace mapped zone source dropdowns with EntitySelect pickers - Replace scene target selector with EntityPalette.pick() pattern - Remove effect palette preview bar from CSS editor - Remove sensitivity badge from audio color strip source cards - Clean up unused scene-selector CSS and scene-target-add-row CSS - Add locale keys for all new UI elements across en/ru/zh Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,7 @@ from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
from wled_controller.core.capture.calibration import CalibrationConfig, PixelMapper
|
||||
from wled_controller.core.capture.calibration import CalibrationConfig, PixelMapper, AdvancedPixelMapper, create_pixel_mapper
|
||||
from wled_controller.core.capture.screen_capture import extract_border_pixels
|
||||
from wled_controller.core.processing.live_stream import LiveStream
|
||||
from wled_controller.utils import get_logger
|
||||
@@ -151,15 +151,22 @@ class PictureColorStripStream(ColorStripStream):
|
||||
restarting the thread (except when the underlying LiveStream changes).
|
||||
"""
|
||||
|
||||
def __init__(self, live_stream: LiveStream, source):
|
||||
def __init__(self, live_stream, source):
|
||||
"""
|
||||
Args:
|
||||
live_stream: Acquired LiveStream (lifecycle managed by ColorStripStreamManager)
|
||||
live_stream: Acquired LiveStream or Dict[str, LiveStream] for advanced mode.
|
||||
Lifecycle managed by ColorStripStreamManager.
|
||||
source: PictureColorStripSource config
|
||||
"""
|
||||
from wled_controller.storage.color_strip_source import PictureColorStripSource
|
||||
|
||||
self._live_stream = live_stream
|
||||
# Support both single LiveStream and dict of streams (advanced mode)
|
||||
if isinstance(live_stream, dict):
|
||||
self._live_streams = live_stream
|
||||
self._live_stream = next(iter(live_stream.values()))
|
||||
else:
|
||||
self._live_streams = {}
|
||||
self._live_stream = live_stream
|
||||
self._fps: int = 30 # internal capture rate (send FPS is on the target)
|
||||
self._smoothing: float = source.smoothing
|
||||
self._brightness: float = source.brightness
|
||||
@@ -167,7 +174,7 @@ class PictureColorStripStream(ColorStripStream):
|
||||
self._gamma: float = source.gamma
|
||||
self._interpolation_mode: str = source.interpolation_mode
|
||||
self._calibration: CalibrationConfig = source.calibration
|
||||
self._pixel_mapper = PixelMapper(
|
||||
self._pixel_mapper = create_pixel_mapper(
|
||||
self._calibration, interpolation_mode=self._interpolation_mode
|
||||
)
|
||||
cal_leds = self._calibration.get_total_leds()
|
||||
@@ -201,6 +208,8 @@ class PictureColorStripStream(ColorStripStream):
|
||||
|
||||
@property
|
||||
def display_index(self) -> Optional[int]:
|
||||
if self._live_streams:
|
||||
return None # multi-source, ambiguous
|
||||
return self._live_stream.display_index
|
||||
|
||||
@property
|
||||
@@ -255,9 +264,9 @@ class PictureColorStripStream(ColorStripStream):
|
||||
|
||||
PixelMapper is rebuilt atomically if calibration or interpolation_mode changed.
|
||||
"""
|
||||
from wled_controller.storage.color_strip_source import PictureColorStripSource
|
||||
from wled_controller.storage.color_strip_source import PictureColorStripSource, AdvancedPictureColorStripSource
|
||||
|
||||
if not isinstance(source, PictureColorStripSource):
|
||||
if not isinstance(source, (PictureColorStripSource, AdvancedPictureColorStripSource)):
|
||||
return
|
||||
|
||||
self._smoothing = source.smoothing
|
||||
@@ -277,7 +286,7 @@ class PictureColorStripStream(ColorStripStream):
|
||||
self._calibration = source.calibration
|
||||
cal_leds = source.calibration.get_total_leds()
|
||||
self._led_count = source.led_count if source.led_count > 0 else cal_leds
|
||||
self._pixel_mapper = PixelMapper(
|
||||
self._pixel_mapper = create_pixel_mapper(
|
||||
source.calibration, interpolation_mode=source.interpolation_mode
|
||||
)
|
||||
self._previous_colors = None # Reset smoothing history on calibration change
|
||||
@@ -375,10 +384,21 @@ class PictureColorStripStream(ColorStripStream):
|
||||
t0 = time.perf_counter()
|
||||
|
||||
calibration = self._calibration
|
||||
border_pixels = extract_border_pixels(frame, calibration.border_width)
|
||||
t1 = time.perf_counter()
|
||||
mapper = self._pixel_mapper
|
||||
|
||||
led_colors = self._pixel_mapper.map_border_to_leds(border_pixels)
|
||||
if isinstance(mapper, AdvancedPixelMapper):
|
||||
# Advanced mode: gather frames from all live streams
|
||||
frames_dict = {}
|
||||
for ps_id, ls in self._live_streams.items():
|
||||
f = ls.get_latest_frame()
|
||||
if f is not None:
|
||||
frames_dict[ps_id] = f
|
||||
t1 = time.perf_counter()
|
||||
led_colors = mapper.map_lines_to_leds(frames_dict)
|
||||
else:
|
||||
border_pixels = extract_border_pixels(frame, calibration.border_width)
|
||||
t1 = time.perf_counter()
|
||||
led_colors = mapper.map_border_to_leds(border_pixels)
|
||||
t2 = time.perf_counter()
|
||||
|
||||
# Ensure scratch pool is sized for this frame
|
||||
|
||||
@@ -42,12 +42,14 @@ class _ColorStripEntry:
|
||||
|
||||
stream: ColorStripStream
|
||||
ref_count: int
|
||||
# ID of the picture source whose LiveStream we acquired (for release)
|
||||
picture_source_id: str
|
||||
# IDs of picture sources whose LiveStreams we acquired (for release)
|
||||
picture_source_ids: list = None
|
||||
# Per-consumer target FPS values (target_id → fps)
|
||||
target_fps: Dict[str, int] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.picture_source_ids is None:
|
||||
self.picture_source_ids = []
|
||||
if self.target_fps is None:
|
||||
self.target_fps = {}
|
||||
|
||||
@@ -155,7 +157,7 @@ class ColorStripStreamManager:
|
||||
css_stream.start()
|
||||
key = f"{css_id}:{consumer_id}" if consumer_id else css_id
|
||||
self._streams[key] = _ColorStripEntry(
|
||||
stream=css_stream, ref_count=1, picture_source_id="",
|
||||
stream=css_stream, ref_count=1, picture_source_ids=[],
|
||||
)
|
||||
logger.info(f"Created {source.source_type} stream {key}")
|
||||
return css_stream
|
||||
@@ -167,26 +169,47 @@ class ColorStripStreamManager:
|
||||
logger.info(f"Reusing stream {css_id} (ref_count={entry.ref_count})")
|
||||
return entry.stream
|
||||
|
||||
# Create new picture stream — needs a LiveStream from the capture pipeline
|
||||
from wled_controller.storage.color_strip_source import PictureColorStripSource
|
||||
if not isinstance(source, PictureColorStripSource):
|
||||
# Create new picture stream — needs LiveStream(s) from the capture pipeline
|
||||
from wled_controller.storage.color_strip_source import PictureColorStripSource, AdvancedPictureColorStripSource
|
||||
if not isinstance(source, (PictureColorStripSource, AdvancedPictureColorStripSource)):
|
||||
raise ValueError(
|
||||
f"Unsupported sharable source type '{source.source_type}' for {css_id}"
|
||||
)
|
||||
|
||||
if not source.picture_source_id:
|
||||
raise ValueError(
|
||||
f"Color strip source {css_id} has no picture_source_id assigned"
|
||||
)
|
||||
|
||||
# Acquire the underlying live stream (ref-counted)
|
||||
live_stream = self._live_stream_manager.acquire(source.picture_source_id)
|
||||
# Determine required picture sources based on calibration mode
|
||||
required_ps_ids = source.calibration.get_required_picture_source_ids()
|
||||
if not required_ps_ids:
|
||||
# Simple mode: use the CSS source's single picture_source_id
|
||||
ps_id = getattr(source, "picture_source_id", "")
|
||||
if not ps_id:
|
||||
raise ValueError(
|
||||
f"Color strip source {css_id} has no picture_source_id assigned"
|
||||
)
|
||||
required_ps_ids = [ps_id]
|
||||
|
||||
# Acquire all required live streams (with rollback on failure)
|
||||
acquired = {}
|
||||
try:
|
||||
css_stream = PictureColorStripStream(live_stream, source)
|
||||
for ps_id in required_ps_ids:
|
||||
acquired[ps_id] = self._live_stream_manager.acquire(ps_id)
|
||||
except Exception as e:
|
||||
for ps_id in acquired:
|
||||
self._live_stream_manager.release(ps_id)
|
||||
raise ValueError(
|
||||
f"Failed to acquire live streams for source {css_id}: {e}"
|
||||
) from e
|
||||
|
||||
# Create stream (single LiveStream for simple, dict for advanced)
|
||||
try:
|
||||
if len(acquired) == 1 and source.calibration.mode == "simple":
|
||||
live_arg = next(iter(acquired.values()))
|
||||
else:
|
||||
live_arg = acquired
|
||||
css_stream = PictureColorStripStream(live_arg, source)
|
||||
css_stream.start()
|
||||
except Exception as e:
|
||||
self._live_stream_manager.release(source.picture_source_id)
|
||||
for ps_id in acquired:
|
||||
self._live_stream_manager.release(ps_id)
|
||||
raise RuntimeError(
|
||||
f"Failed to start color strip stream for source {css_id}: {e}"
|
||||
) from e
|
||||
@@ -194,7 +217,7 @@ class ColorStripStreamManager:
|
||||
self._streams[css_id] = _ColorStripEntry(
|
||||
stream=css_stream,
|
||||
ref_count=1,
|
||||
picture_source_id=source.picture_source_id,
|
||||
picture_source_ids=list(acquired.keys()),
|
||||
)
|
||||
|
||||
logger.info(f"Created picture color strip stream {css_id}")
|
||||
@@ -229,13 +252,13 @@ class ColorStripStreamManager:
|
||||
source_id = key.split(":")[0] if ":" in key else key
|
||||
self._release_clock(source_id, entry.stream)
|
||||
|
||||
picture_source_id = entry.picture_source_id
|
||||
picture_source_ids = entry.picture_source_ids
|
||||
del self._streams[key]
|
||||
logger.info(f"Removed color strip stream {key}")
|
||||
|
||||
# Release the underlying live stream (not needed for static sources)
|
||||
if picture_source_id:
|
||||
self._live_stream_manager.release(picture_source_id)
|
||||
# Release all underlying live streams
|
||||
for ps_id in picture_source_ids:
|
||||
self._live_stream_manager.release(ps_id)
|
||||
|
||||
def update_source(self, css_id: str, new_source) -> None:
|
||||
"""Hot-update processing params on all running streams for a source.
|
||||
@@ -280,15 +303,19 @@ class ColorStripStreamManager:
|
||||
source_id = key.split(":")[0] if ":" in key else key
|
||||
self._release_clock(source_id, entry.stream)
|
||||
|
||||
# Track picture_source_id change for future reference counting
|
||||
from wled_controller.storage.color_strip_source import PictureColorStripSource
|
||||
if isinstance(new_source, PictureColorStripSource):
|
||||
# Track picture source changes for future reference counting
|
||||
from wled_controller.storage.color_strip_source import PictureColorStripSource, AdvancedPictureColorStripSource
|
||||
if isinstance(new_source, (PictureColorStripSource, AdvancedPictureColorStripSource)):
|
||||
new_ps_ids = new_source.calibration.get_required_picture_source_ids()
|
||||
if not new_ps_ids:
|
||||
ps_id = getattr(new_source, "picture_source_id", "")
|
||||
new_ps_ids = [ps_id] if ps_id else []
|
||||
for key in matching_keys:
|
||||
entry = self._streams[key]
|
||||
if new_source.picture_source_id != entry.picture_source_id:
|
||||
if set(new_ps_ids) != set(entry.picture_source_ids):
|
||||
logger.info(
|
||||
f"CSS {css_id}: picture_source_id changed — "
|
||||
f"restart target to use new source"
|
||||
f"CSS {css_id}: picture source set changed — "
|
||||
f"restart target to use new sources"
|
||||
)
|
||||
break
|
||||
|
||||
|
||||
Reference in New Issue
Block a user