mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-01 08:48:05 +00:00
Add overflow handling: keep ordered content whole instead of rotating a window
The width budget split any oversized plugin by advancing a window each cycle.
That is right for interchangeable items — news headlines, odds, stock prices —
but wrong for ordered content: a league table showed ranks 1-6, then resumed at
7 two rotations later, which reads as out of order and out of context. Nobody
needs rank 23 in a ticker; they need the top of the table, every time.
overflow_mode chooses between them:
rotate — advance a window each cycle so everything is seen eventually
(unchanged default)
truncate — always show the start and drop the rest, keeping ordered content
coherent. Records no window state, so every pass starts at the top.
Per-plugin vegas_overflow overrides the global setting, since one install has
both kinds of plugin. Also adds per-plugin vegas_max_width_screens, so content
that must stay whole can be given more room — or uncapped with 0 — without
lifting the cap on every ticker.
Applied on the test rig: f1-scoreboard and ledmatrix-leaderboard set to
truncate, and baseball given 4.5 screens because it was showing 8 of 9 games
when the whole slate needed only a little more room. Verified: F1 now reports
"the first 10 of 116 ... the rest are not shown", baseball has dropped out of
the budget log entirely, and stocks, odds-ticker and stock-news still rotate.
Also corrects the crop log, which claimed "window advances next cycle"
unconditionally and so misreported truncated crops. A test now pins the
behaviour behind the message: truncate must leave no offset recorded.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
This commit is contained in:
@@ -145,6 +145,7 @@
|
||||
"lead_in_width": 0,
|
||||
"plugins_per_cycle": 6,
|
||||
"max_plugin_width_ratio": 3.0,
|
||||
"overflow_mode": "rotate",
|
||||
"dynamic_duration_enabled": true,
|
||||
"min_cycle_duration": 60,
|
||||
"max_cycle_duration": 240
|
||||
|
||||
@@ -91,6 +91,18 @@ class VegasModeConfig:
|
||||
# letter floating between two unrelated plugins.
|
||||
min_cut_gap: int = 6
|
||||
|
||||
# What to do when a plugin's content exceeds its width budget.
|
||||
#
|
||||
# "rotate" — advance a window each cycle so everything is seen eventually.
|
||||
# Right for interchangeable items: news headlines, odds, stocks.
|
||||
# "truncate" — always show the start. Right for ordered content, where a
|
||||
# window into the middle is meaningless: a league table that
|
||||
# shows ranks 1-6 then resumes at 7 two rotations later reads
|
||||
# as out of order and out of context.
|
||||
#
|
||||
# Override per plugin with vegas_overflow.
|
||||
overflow_mode: str = "rotate"
|
||||
|
||||
# Cap on one plugin's share of a cycle, as a multiple of display width.
|
||||
# A single ticker returning 7,000px would otherwise hold the panel for over
|
||||
# two minutes. Overflow is deferred to later cycles rather than discarded.
|
||||
@@ -148,6 +160,7 @@ class VegasModeConfig:
|
||||
plugins_per_cycle=int(vegas_config.get('plugins_per_cycle', 6)),
|
||||
max_plugin_width_ratio=float(
|
||||
vegas_config.get('max_plugin_width_ratio', 3.0)),
|
||||
overflow_mode=str(vegas_config.get('overflow_mode', 'rotate')),
|
||||
plugin_order=list(vegas_config.get('plugin_order', [])),
|
||||
excluded_plugins=set(vegas_config.get('excluded_plugins', [])),
|
||||
target_fps=int(vegas_config.get('target_fps', 125)),
|
||||
@@ -179,6 +192,7 @@ class VegasModeConfig:
|
||||
'lead_in_width': self.lead_in_width,
|
||||
'plugins_per_cycle': self.plugins_per_cycle,
|
||||
'max_plugin_width_ratio': self.max_plugin_width_ratio,
|
||||
'overflow_mode': self.overflow_mode,
|
||||
'plugin_order': self.plugin_order,
|
||||
'excluded_plugins': list(self.excluded_plugins),
|
||||
'target_fps': self.target_fps,
|
||||
@@ -322,6 +336,11 @@ class VegasModeConfig:
|
||||
errors.append(
|
||||
f"plugins_per_cycle must be <= 50, got {self.plugins_per_cycle}")
|
||||
|
||||
if self.overflow_mode not in ('rotate', 'truncate'):
|
||||
errors.append(
|
||||
"overflow_mode must be 'rotate' or 'truncate', "
|
||||
f"got {self.overflow_mode!r}")
|
||||
|
||||
if self.max_plugin_width_ratio < 0:
|
||||
errors.append(
|
||||
"max_plugin_width_ratio must be >= 0 "
|
||||
@@ -375,6 +394,8 @@ class VegasModeConfig:
|
||||
if 'max_plugin_width_ratio' in vegas_config:
|
||||
self.max_plugin_width_ratio = float(
|
||||
vegas_config['max_plugin_width_ratio'])
|
||||
if 'overflow_mode' in vegas_config:
|
||||
self.overflow_mode = str(vegas_config['overflow_mode'])
|
||||
if 'plugin_order' in vegas_config:
|
||||
self.plugin_order = list(vegas_config['plugin_order'])
|
||||
if 'excluded_plugins' in vegas_config:
|
||||
|
||||
@@ -120,7 +120,7 @@ class PluginAdapter:
|
||||
"[%s] Native content SUCCESS: %d images, %dpx total",
|
||||
plugin_id, len(content), total_width
|
||||
)
|
||||
return self._finalize(content, plugin_id, 'native')
|
||||
return self._finalize(content, plugin_id, 'native', plugin)
|
||||
logger.info("[%s] Native content returned None", plugin_id)
|
||||
|
||||
# Try to get scroll_helper's cached image (for scrolling plugins like stocks/odds)
|
||||
@@ -133,7 +133,7 @@ class PluginAdapter:
|
||||
"[%s] ScrollHelper content SUCCESS: %d images, %dpx total",
|
||||
plugin_id, len(content), total_width
|
||||
)
|
||||
return self._finalize(content, plugin_id, 'scroll_helper')
|
||||
return self._finalize(content, plugin_id, 'scroll_helper', plugin)
|
||||
if has_scroll_helper:
|
||||
logger.info("[%s] ScrollHelper content returned None", plugin_id)
|
||||
|
||||
@@ -154,7 +154,7 @@ class PluginAdapter:
|
||||
"[%s] Fallback capture SUCCESS: %d images, %dpx total",
|
||||
plugin_id, len(content), total_width
|
||||
)
|
||||
return self._finalize(content, plugin_id, 'fallback')
|
||||
return self._finalize(content, plugin_id, 'fallback', plugin)
|
||||
|
||||
logger.warning(
|
||||
"[%s] NO CONTENT from any method (native=%s, scroll_helper=%s, fallback=tried)",
|
||||
@@ -163,7 +163,8 @@ class PluginAdapter:
|
||||
return None
|
||||
|
||||
def _finalize(
|
||||
self, images: List[Image.Image], plugin_id: str, source: str
|
||||
self, images: List[Image.Image], plugin_id: str, source: str,
|
||||
plugin: Optional['BasePlugin'] = None
|
||||
) -> Optional[List[Image.Image]]:
|
||||
"""
|
||||
Trim dead space off a segment, then cache it.
|
||||
@@ -190,7 +191,7 @@ class PluginAdapter:
|
||||
# turning off margin cropping should not let one plugin hold the
|
||||
# panel for minutes. Skipping it here previously let a 14,848px
|
||||
# segment through untouched.
|
||||
kept = self._apply_width_budget(list(images), plugin_id)
|
||||
kept = self._apply_width_budget(list(images), plugin_id, plugin)
|
||||
self._cache_content(plugin_id, kept)
|
||||
return kept
|
||||
|
||||
@@ -235,7 +236,7 @@ class PluginAdapter:
|
||||
len(kept), dropped_blank
|
||||
)
|
||||
|
||||
kept = self._apply_width_budget(kept, plugin_id)
|
||||
kept = self._apply_width_budget(kept, plugin_id, plugin)
|
||||
|
||||
self._cache_content(plugin_id, kept)
|
||||
return kept
|
||||
@@ -332,15 +333,74 @@ class PluginAdapter:
|
||||
threshold=self.config.trim_threshold,
|
||||
)
|
||||
|
||||
def _width_budget(self) -> int:
|
||||
"""Maximum columns one plugin may occupy in a cycle. 0 means unlimited."""
|
||||
def _plugin_setting(self, plugin: 'BasePlugin', key: str):
|
||||
"""Read a per-plugin config override, or None if absent."""
|
||||
plugin_cfg = getattr(plugin, 'config', None)
|
||||
if not isinstance(plugin_cfg, dict):
|
||||
return None
|
||||
value = plugin_cfg.get(key)
|
||||
return None if value in (None, '') else value
|
||||
|
||||
def resolve_overflow_mode(self, plugin: 'BasePlugin', plugin_id: str) -> str:
|
||||
"""
|
||||
How to handle content that exceeds this plugin's width budget.
|
||||
|
||||
'rotate' advances a window each cycle so everything is seen eventually,
|
||||
which suits interchangeable items. 'truncate' always shows the start,
|
||||
which suits ordered content — a league table that shows ranks 1-6 and
|
||||
then resumes at 7 two rotations later reads as out of order, and nobody
|
||||
needs rank 23 in a ticker anyway.
|
||||
|
||||
Per-plugin ``vegas_overflow`` wins over the global ``overflow_mode``.
|
||||
"""
|
||||
raw = self._plugin_setting(plugin, 'vegas_overflow')
|
||||
if raw is not None:
|
||||
candidate = str(raw).strip().lower()
|
||||
if candidate in ('rotate', 'truncate'):
|
||||
return candidate
|
||||
logger.warning(
|
||||
"[%s] Invalid vegas_overflow %r, expected 'rotate' or 'truncate'",
|
||||
plugin_id, raw
|
||||
)
|
||||
return self.config.overflow_mode
|
||||
|
||||
def _width_budget(self, plugin: Optional['BasePlugin'] = None,
|
||||
plugin_id: str = '') -> int:
|
||||
"""
|
||||
Maximum columns one plugin may occupy in a cycle. 0 means unlimited.
|
||||
|
||||
A per-plugin ``vegas_max_width_screens`` overrides the global ratio, so
|
||||
content that has to stay whole can be given room (or uncapped with 0)
|
||||
without lifting the cap on every ticker.
|
||||
"""
|
||||
ratio = self.config.max_plugin_width_ratio
|
||||
|
||||
if plugin is not None:
|
||||
raw = self._plugin_setting(plugin, 'vegas_max_width_screens')
|
||||
if raw is not None:
|
||||
try:
|
||||
candidate = float(raw)
|
||||
except (TypeError, ValueError):
|
||||
logger.warning(
|
||||
"[%s] Invalid vegas_max_width_screens %r, ignoring",
|
||||
plugin_id, raw
|
||||
)
|
||||
else:
|
||||
if candidate >= 0:
|
||||
ratio = candidate
|
||||
else:
|
||||
logger.warning(
|
||||
"[%s] vegas_max_width_screens must be >= 0, got %s",
|
||||
plugin_id, candidate
|
||||
)
|
||||
|
||||
if ratio <= 0:
|
||||
return 0
|
||||
return int(self.display_width * ratio)
|
||||
|
||||
def _apply_width_budget(
|
||||
self, images: List[Image.Image], plugin_id: str
|
||||
self, images: List[Image.Image], plugin_id: str,
|
||||
plugin: Optional['BasePlugin'] = None
|
||||
) -> List[Image.Image]:
|
||||
"""
|
||||
Hold one plugin to its share of a cycle.
|
||||
@@ -359,7 +419,9 @@ class PluginAdapter:
|
||||
Images that fit the budget, starting from the plugin's current
|
||||
rotation offset.
|
||||
"""
|
||||
budget = self._width_budget()
|
||||
budget = self._width_budget(plugin, plugin_id)
|
||||
mode = (self.resolve_overflow_mode(plugin, plugin_id)
|
||||
if plugin is not None else self.config.overflow_mode)
|
||||
|
||||
# Count the gaps the compositor will actually insert, not just the
|
||||
# pixels of the rows — otherwise a plugin with many rows quietly
|
||||
@@ -377,8 +439,14 @@ class PluginAdapter:
|
||||
return images
|
||||
|
||||
if len(images) == 1:
|
||||
return [self._crop_to_budget(images[0], budget, plugin_id)]
|
||||
return [self._crop_to_budget(images[0], budget, plugin_id, mode)]
|
||||
|
||||
if mode == 'truncate':
|
||||
# Ordered content: always show from the top. Deliberately does not
|
||||
# advance the offset, so the same opening items appear every time
|
||||
# rather than the viewer being shown the middle of a ranked list.
|
||||
start = 0
|
||||
else:
|
||||
start = self._item_offsets.get(plugin_id, 0) % len(images)
|
||||
selected: List[Image.Image] = []
|
||||
used = 0
|
||||
@@ -397,8 +465,14 @@ class PluginAdapter:
|
||||
used += cost
|
||||
consumed += 1
|
||||
|
||||
if mode == 'truncate':
|
||||
logger.info(
|
||||
"[%s] Width budget %dpx: showing the first %d of %d row(s) "
|
||||
"(%dpx incl. gaps); the rest are not shown (overflow=truncate)",
|
||||
plugin_id, budget, len(selected), len(images), used
|
||||
)
|
||||
else:
|
||||
self._item_offsets[plugin_id] = (start + consumed) % len(images)
|
||||
|
||||
logger.info(
|
||||
"[%s] Width budget %dpx: showing %d of %d row(s) (%dpx incl. gaps) "
|
||||
"from offset %d; remainder deferred to a later cycle",
|
||||
@@ -407,7 +481,8 @@ class PluginAdapter:
|
||||
return selected
|
||||
|
||||
def _crop_to_budget(
|
||||
self, img: Image.Image, budget: int, plugin_id: str
|
||||
self, img: Image.Image, budget: int, plugin_id: str,
|
||||
mode: str = 'rotate'
|
||||
) -> Image.Image:
|
||||
"""
|
||||
Narrow a single oversized image to the budget, advancing a window
|
||||
@@ -416,6 +491,11 @@ class PluginAdapter:
|
||||
The cut is snapped to the nearest blank column so it does not slice
|
||||
through a glyph or logo and leave half a character at the panel edge.
|
||||
"""
|
||||
if mode == 'truncate':
|
||||
# Always the start of the strip, so a ranked table is never entered
|
||||
# from the middle.
|
||||
offset = 0
|
||||
else:
|
||||
offset = self._item_offsets.get(plugin_id, 0)
|
||||
if offset >= img.width:
|
||||
offset = 0
|
||||
@@ -435,11 +515,13 @@ class PluginAdapter:
|
||||
# (words, ticker entries); it would be wrong to let a solid image
|
||||
# escape the cap in its name.
|
||||
end = min(offset + budget, img.width)
|
||||
if mode != 'truncate':
|
||||
self._item_offsets[plugin_id] = 0 if end >= img.width else end
|
||||
logger.info(
|
||||
"[%s] Width budget %dpx: cropped continuous %dpx image to "
|
||||
"[%d:%d] (no item gaps of %dpx+ to align to)",
|
||||
plugin_id, budget, img.width, offset, end, min_run
|
||||
"[%d:%d] (no item gaps of %dpx+ to align to)%s",
|
||||
plugin_id, budget, img.width, offset, end, min_run,
|
||||
"" if mode != 'truncate' else "; showing the start only"
|
||||
)
|
||||
return img.crop((offset, 0, end, img.height))
|
||||
|
||||
@@ -456,13 +538,16 @@ class PluginAdapter:
|
||||
# because the alternative is cutting through an item.
|
||||
end = max(within) if within else min(later)
|
||||
|
||||
if mode != 'truncate':
|
||||
# Next cycle resumes where this one stopped; wrap when the strip ends.
|
||||
self._item_offsets[plugin_id] = 0 if end >= img.width else end
|
||||
|
||||
logger.info(
|
||||
"[%s] Width budget %dpx: cropped single %dpx image to [%d:%d] "
|
||||
"(%dpx) at item boundaries, window advances next cycle",
|
||||
plugin_id, budget, img.width, start, end, end - start
|
||||
"(%dpx) at item boundaries, %s",
|
||||
plugin_id, budget, img.width, start, end, end - start,
|
||||
"showing the start only (overflow=truncate)"
|
||||
if mode == 'truncate' else "window advances next cycle"
|
||||
)
|
||||
return img.crop((start, 0, end, img.height))
|
||||
|
||||
|
||||
@@ -1509,3 +1509,131 @@ class TestDeferredDraining:
|
||||
p.extend_scroll_content()
|
||||
assert p.drain_deferred() is True
|
||||
assert p.drain_deferred() is True
|
||||
|
||||
|
||||
class TestOverflowMode:
|
||||
"""
|
||||
Rotating a window through content only makes sense when the items are
|
||||
interchangeable. For ordered content it shows the middle of a ranked list —
|
||||
ranks 1-6, then 7 onwards two rotations later, which reads as out of order.
|
||||
"""
|
||||
|
||||
class Cfg:
|
||||
def __init__(self, cfg=None):
|
||||
self.config = cfg or {}
|
||||
|
||||
def get_vegas_content(self):
|
||||
return None
|
||||
|
||||
def test_default_is_rotate(self):
|
||||
adapter = adapter_with()
|
||||
assert adapter.resolve_overflow_mode(self.Cfg(), 'p') == 'rotate'
|
||||
|
||||
def test_global_mode_applies(self):
|
||||
adapter = adapter_with(overflow_mode='truncate')
|
||||
assert adapter.resolve_overflow_mode(self.Cfg(), 'p') == 'truncate'
|
||||
|
||||
def test_per_plugin_override_wins(self):
|
||||
adapter = adapter_with(overflow_mode='rotate')
|
||||
plugin = self.Cfg({'vegas_overflow': 'truncate'})
|
||||
assert adapter.resolve_overflow_mode(plugin, 'p') == 'truncate'
|
||||
|
||||
@pytest.mark.parametrize('bad', ['sideways', '', None, 5])
|
||||
def test_invalid_override_falls_back(self, bad):
|
||||
adapter = adapter_with(overflow_mode='rotate')
|
||||
plugin = self.Cfg({'vegas_overflow': bad})
|
||||
assert adapter.resolve_overflow_mode(plugin, 'p') == 'rotate'
|
||||
|
||||
def test_truncate_always_shows_the_same_opening_rows(self):
|
||||
# The reported problem: a ranked list should not resume from the middle.
|
||||
adapter = adapter_with(content_padding=0, max_plugin_width_ratio=1.0,
|
||||
intra_plugin_gap=0, min_content_separation=0,
|
||||
overflow_mode='truncate')
|
||||
rows = [canvas([(0, 200)], width=200) for _ in range(8)]
|
||||
plugin = NativePlugin(rows)
|
||||
|
||||
first = adapter.get_content(plugin, 'ranks')
|
||||
adapter.invalidate_cache('ranks')
|
||||
second = adapter.get_content(plugin, 'ranks')
|
||||
assert len(first) == len(second)
|
||||
assert 'ranks' not in adapter._item_offsets, "must not advance a window"
|
||||
|
||||
def test_rotate_still_advances(self):
|
||||
adapter = adapter_with(content_padding=0, max_plugin_width_ratio=1.0,
|
||||
intra_plugin_gap=0, min_content_separation=0,
|
||||
overflow_mode='rotate')
|
||||
rows = [canvas([(0, 200)], width=200) for _ in range(8)]
|
||||
adapter.get_content(NativePlugin(rows), 'ticker')
|
||||
assert adapter._item_offsets.get('ticker', 0) > 0
|
||||
|
||||
def test_truncate_on_a_single_wide_image_starts_at_zero(self):
|
||||
adapter = adapter_with(content_padding=0, max_plugin_width_ratio=1.0,
|
||||
overflow_mode='truncate')
|
||||
strip = canvas([(0, 5000)], width=5000)
|
||||
plugin = NativePlugin([strip])
|
||||
first = adapter.get_content(plugin, 'table')[0]
|
||||
adapter.invalidate_cache('table')
|
||||
second = adapter.get_content(plugin, 'table')[0]
|
||||
assert first.tobytes() == second.tobytes(), "window must not advance"
|
||||
|
||||
|
||||
class TestPerPluginWidthBudget:
|
||||
class Cfg:
|
||||
def __init__(self, cfg=None):
|
||||
self.config = cfg or {}
|
||||
|
||||
def get_vegas_content(self):
|
||||
return None
|
||||
|
||||
def test_global_ratio_by_default(self):
|
||||
adapter = adapter_with(max_plugin_width_ratio=3.0)
|
||||
assert adapter._width_budget(self.Cfg(), 'p') == DISPLAY_W * 3
|
||||
|
||||
def test_per_plugin_override_widens(self):
|
||||
adapter = adapter_with(max_plugin_width_ratio=3.0)
|
||||
plugin = self.Cfg({'vegas_max_width_screens': 8})
|
||||
assert adapter._width_budget(plugin, 'p') == DISPLAY_W * 8
|
||||
|
||||
def test_per_plugin_zero_means_uncapped(self):
|
||||
adapter = adapter_with(max_plugin_width_ratio=3.0)
|
||||
plugin = self.Cfg({'vegas_max_width_screens': 0})
|
||||
assert adapter._width_budget(plugin, 'p') == 0
|
||||
|
||||
def test_uncapped_plugin_keeps_all_its_content(self):
|
||||
adapter = adapter_with(content_padding=0, max_plugin_width_ratio=1.0)
|
||||
|
||||
class Wide(NativePlugin):
|
||||
def __init__(self, images):
|
||||
super().__init__(images)
|
||||
self.config = {'vegas_max_width_screens': 0}
|
||||
|
||||
strip = canvas([(0, 6000)], width=6000)
|
||||
assert adapter.get_content(Wide([strip]), 'whole')[0].width == 6000
|
||||
|
||||
@pytest.mark.parametrize('bad', ['wide', -1, None, ''])
|
||||
def test_invalid_override_falls_back_to_global(self, bad):
|
||||
adapter = adapter_with(max_plugin_width_ratio=2.0)
|
||||
plugin = self.Cfg({'vegas_max_width_screens': bad})
|
||||
assert adapter._width_budget(plugin, 'p') == DISPLAY_W * 2
|
||||
|
||||
def test_no_plugin_uses_the_global(self):
|
||||
adapter = adapter_with(max_plugin_width_ratio=2.0)
|
||||
assert adapter._width_budget() == DISPLAY_W * 2
|
||||
|
||||
def test_truncate_single_image_never_records_an_offset(self):
|
||||
# The behavioural guarantee behind the log message: no window state is
|
||||
# kept, so every pass starts at the top.
|
||||
adapter = adapter_with(content_padding=0, max_plugin_width_ratio=1.0,
|
||||
overflow_mode='truncate')
|
||||
strip = canvas([(0, 5000)], width=5000)
|
||||
for _ in range(4):
|
||||
adapter.invalidate_cache('table')
|
||||
adapter.get_content(NativePlugin([strip]), 'table')
|
||||
assert 'table' not in adapter._item_offsets
|
||||
|
||||
def test_rotate_single_image_does_record_an_offset(self):
|
||||
adapter = adapter_with(content_padding=0, max_plugin_width_ratio=1.0,
|
||||
overflow_mode='rotate')
|
||||
strip = canvas([(0, 5000)], width=5000)
|
||||
adapter.get_content(NativePlugin([strip]), 'ticker')
|
||||
assert adapter._item_offsets.get('ticker', 0) > 0
|
||||
|
||||
@@ -926,7 +926,7 @@ def save_main_config():
|
||||
'vegas_intra_plugin_gap', 'vegas_render_width_pct',
|
||||
'vegas_min_content_separation', 'vegas_min_cut_gap',
|
||||
'vegas_continuous_scroll', 'vegas_extend_threshold_screens',
|
||||
'vegas_smooth_scroll']
|
||||
'vegas_smooth_scroll', 'vegas_overflow_mode']
|
||||
|
||||
if any(k in data for k in vegas_fields):
|
||||
if 'display' not in current_config:
|
||||
@@ -951,6 +951,16 @@ def save_main_config():
|
||||
|
||||
# max_plugin_width_ratio is the one fractional setting, so it is
|
||||
# handled outside the integer loop below.
|
||||
if data.get('vegas_overflow_mode') not in ('', None):
|
||||
mode = str(data['vegas_overflow_mode']).strip().lower()
|
||||
if mode not in ('rotate', 'truncate'):
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': "Invalid value for vegas_overflow_mode: "
|
||||
"must be 'rotate' or 'truncate'"
|
||||
}), 400
|
||||
vegas_config['overflow_mode'] = mode
|
||||
|
||||
if data.get('vegas_extend_threshold_screens') not in ('', None):
|
||||
try:
|
||||
screens = float(data['vegas_extend_threshold_screens'])
|
||||
|
||||
@@ -547,6 +547,14 @@
|
||||
class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="setting-display-vegas_overflow_mode" data-setting-key="display.vegas_scroll.overflow_mode">
|
||||
<label for="vegas_overflow_mode" class="block text-sm font-medium text-gray-700">When A Plugin Is Too Wide{{ ui.help_tip('What to do when a plugin has more content than its width allowance.\nRotate through it: show a different slice each time round, so everything is seen eventually. Right for interchangeable items like news headlines, odds or stock prices.\nShow the start only: always display from the beginning and drop the rest. Right for ordered content — a league table that shows ranks 1-6 and then resumes at 7 two rotations later reads as out of order.\nDefault: rotate. Override for one plugin with vegas_overflow in its own settings.', 'Overflow Handling') }}</label>
|
||||
<select id="vegas_overflow_mode" name="vegas_overflow_mode" class="form-control">
|
||||
<option value="rotate" {% if main_config.display.get('vegas_scroll', {}).get('overflow_mode', 'rotate') == 'rotate' %}selected{% endif %}>Rotate through it (default)</option>
|
||||
<option value="truncate" {% if main_config.display.get('vegas_scroll', {}).get('overflow_mode', 'rotate') == 'truncate' %}selected{% endif %}>Show the start only</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="setting-display-vegas_max_plugin_width_ratio" data-setting-key="display.vegas_scroll.max_plugin_width_ratio">
|
||||
<label for="vegas_max_plugin_width_ratio" class="block text-sm font-medium text-gray-700">Max Plugin Width (screens){{ ui.help_tip('Caps how much of one cycle a single plugin may occupy, measured in screen widths (0–20).\nDefault: 3. A long ticker such as a news feed or leaderboard is trimmed to this and the remainder shown on later cycles, so one plugin cannot hold the display for minutes. Set 0 for no limit.', 'Max Plugin Width') }}</label>
|
||||
<input type="number"
|
||||
|
||||
Reference in New Issue
Block a user