mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-12 15:33:45 +08:00
StarPort: Keep saved layouts when an update adds or changes widgets
This commit is contained in:
@@ -7,7 +7,7 @@ from pathlib import Path
|
||||
from openpilot.starpilot.system.starport.identity import DATA_DIR
|
||||
from openpilot.starpilot.system.starport.display_profile import screen_geometry
|
||||
from openpilot.starpilot.ui.onroad_customization import (CEM_GAUGE_WIDGET, CLOCK_WIDGET, JETLINK_WIDGET, MODE_WIDGET,
|
||||
STOPPED_TIMER_WIDGET, CEM_GAUGE_FRAME, CEM_GAUGE_INSET, MAP_MIN_SIZE, NAV_CARD_SIZE,
|
||||
STOPPED_TIMER_WIDGET, WEATHER_WIDGET, CEM_GAUGE_FRAME, CEM_GAUGE_INSET, MAP_MIN_SIZE, NAV_CARD_SIZE,
|
||||
SPEED_ALIGNMENTS, customization_metadata, validate_map_box, validate_widget_order)
|
||||
from openpilot.starpilot.ui.onroad_stopped_timer import timer_size
|
||||
from openpilot.starpilot.ui.onroad_torque_geometry import maximum_footprint
|
||||
@@ -223,7 +223,7 @@ def validate_layout_for_viewport(value, viewport, *, _legacy_geometry=False):
|
||||
type(value['version']) is not int or value['version'] not in (1, 2, 3, 4, 5) or
|
||||
value['canvas'] != {key: metadata[key] for key in ('width', 'height')} or
|
||||
type(value['widgets']) is not dict or not set(value['widgets']) <= set(metadata['widgets']) or
|
||||
not set(metadata['widgets']) - set(value['widgets']) <= {CEM_GAUGE_WIDGET, MODE_WIDGET, CLOCK_WIDGET, *PROJECTION_WIDGETS}):
|
||||
not set(metadata['widgets']) - set(value['widgets']) <= {CEM_GAUGE_WIDGET, MODE_WIDGET, CLOCK_WIDGET, WEATHER_WIDGET, *PROJECTION_WIDGETS}):
|
||||
raise ValueError('Projection layout does not match saved screen')
|
||||
result = copy.deepcopy(value)
|
||||
if 'clock24Hour' in value and type(value['clock24Hour']) is not bool:
|
||||
@@ -243,7 +243,7 @@ def validate_layout_for_viewport(value, viewport, *, _legacy_geometry=False):
|
||||
if 'widgetOrder' in value:
|
||||
order = value['widgetOrder']
|
||||
if type(order) is list:
|
||||
order = [*order, *(key for key in (CEM_GAUGE_WIDGET, MODE_WIDGET, CLOCK_WIDGET, *PROJECTION_WIDGETS) if key not in order)]
|
||||
order = [*order, *(key for key in (CEM_GAUGE_WIDGET, MODE_WIDGET, CLOCK_WIDGET, WEATHER_WIDGET, *PROJECTION_WIDGETS) if key not in order)]
|
||||
order = validate_widget_order(order, metadata['widgets'])
|
||||
# Older or hand-edited documents may put the escape control underneath
|
||||
# another widget. Preserve their order while restoring this safety layer.
|
||||
@@ -362,7 +362,52 @@ def decode_layout_for_viewport(raw, viewport):
|
||||
exit_placement = widgets.get(CAR_EXIT) if type(widgets) is dict else None
|
||||
if type(exit_placement) is dict and type(exit_placement.get('opacity')) is int and 0 <= exit_placement['opacity'] < CAR_EXIT_MIN_OPACITY:
|
||||
exit_placement['opacity'] = CAR_EXIT_MIN_OPACITY
|
||||
return validate_layout_for_viewport(value, viewport)
|
||||
try:
|
||||
return validate_layout_for_viewport(value, viewport)
|
||||
except ValueError:
|
||||
return _salvage_layout(value, viewport)
|
||||
|
||||
|
||||
def _salvage_layout(value, viewport):
|
||||
"""Keep every saved placement an update still accepts, so one stale widget does not reset the whole layout."""
|
||||
metadata = layout_metadata_for_viewport(viewport)
|
||||
if type(value) is not dict or value.get('canvas') != {key: metadata[key] for key in ('width', 'height')} or type(value.get('widgets')) is not dict:
|
||||
raise ValueError('Projection layout does not match saved screen')
|
||||
defaults = {key: dict(widget['default']) for key, widget in metadata['widgets'].items()}
|
||||
saved = {key: placement for key, placement in value['widgets'].items() if key in defaults}
|
||||
result = {'version': value.get('version'), 'canvas': value['canvas'], 'widgets': dict(defaults)}
|
||||
|
||||
def accepts(candidate):
|
||||
try:
|
||||
validate_layout_for_viewport(candidate, viewport)
|
||||
return True
|
||||
except (ValueError, TypeError, KeyError):
|
||||
return False
|
||||
|
||||
if not accepts(result):
|
||||
result['version'] = 5
|
||||
for field in ('clock24Hour', 'largeUiGammaTrial', *DISPLAY_DEFAULTS):
|
||||
if field in value and accepts({**result, field: value[field]}):
|
||||
result[field] = value[field]
|
||||
for key, placement in saved.items():
|
||||
if type(placement) is not dict:
|
||||
continue
|
||||
merged = {**defaults[key], **{field: placement[field] for field in placement if field in defaults[key]}}
|
||||
clamped = dict(merged)
|
||||
try:
|
||||
bounds, size = metadata['bounds'], placement_size(key, metadata['widgets'][key], merged)
|
||||
for (axis, extent), dimension in zip((('x', 'width'), ('y', 'height')), size, strict=True):
|
||||
clamped[axis] = max(bounds[axis], min(clamped[axis], bounds[axis] + bounds[extent] - dimension))
|
||||
except (KeyError, TypeError, ValueError):
|
||||
pass
|
||||
for candidate in (placement, merged, clamped):
|
||||
trial = {**result, 'widgets': {**result['widgets'], key: candidate}}
|
||||
if accepts(trial):
|
||||
result = trial
|
||||
break
|
||||
if 'widgetOrder' in value and accepts({**result, 'widgetOrder': value['widgetOrder']}):
|
||||
result['widgetOrder'] = value['widgetOrder']
|
||||
return validate_layout_for_viewport(result, viewport)
|
||||
|
||||
|
||||
def projection_customization(document, base_customization):
|
||||
|
||||
@@ -34,6 +34,26 @@ class TestProjectionLayout(unittest.TestCase):
|
||||
self.assertGreaterEqual(placed[axis], metadata['bounds'][axis])
|
||||
self.assertLessEqual(placed[axis] + widget[extent], metadata['bounds'][axis] + metadata['bounds'][extent])
|
||||
|
||||
def test_saved_layout_survives_a_new_widget_and_a_stale_one(self):
|
||||
document = default_layout(SCREEN)
|
||||
document['widgets']['steering_wheel'].update(x=500, y=400, enabled=False)
|
||||
del document['widgets']['weather']
|
||||
document['widgets']['retired_widget'] = {'x': 0, 'y': 0, 'enabled': True}
|
||||
document['widgets']['nav_card']['x'] = 99999
|
||||
decoded = decode_layout(json.dumps(document).encode(), SCREEN)
|
||||
self.assertEqual({key: decoded['widgets']['steering_wheel'][key] for key in ('x', 'y', 'enabled')}, {'x': 500, 'y': 400, 'enabled': False})
|
||||
self.assertEqual(decoded['widgets']['weather'], default_layout(SCREEN)['widgets']['weather'])
|
||||
self.assertNotIn('retired_widget', decoded['widgets'])
|
||||
metadata = layout_metadata(SCREEN)
|
||||
bounds, card = metadata['bounds'], metadata['widgets']['nav_card']
|
||||
self.assertEqual(decoded['widgets']['nav_card']['x'], bounds['x'] + bounds['width'] - card['width'])
|
||||
|
||||
def test_salvage_still_rejects_a_different_screen(self):
|
||||
document = default_layout(SCREEN)
|
||||
document['canvas'] = {'width': 1, 'height': 1}
|
||||
with self.assertRaises(ValueError):
|
||||
decode_layout(json.dumps(document).encode(), SCREEN)
|
||||
|
||||
def test_unsaved_layer_order_draws_the_editor_default(self):
|
||||
document = default_layout(SCREEN)
|
||||
self.assertNotIn('widgetOrder', document)
|
||||
|
||||
Reference in New Issue
Block a user