add plugin settings, per-tile refresh, settings menu, touch-friendly UI

- Plugin settings infrastructure in PluginManager with config/plugin_settings.cfg
  storage, manifest [settings] sections, CRUD API, plugin_setting_changed signal
- Per-tile refresh intervals: 100ms base tick, per-tile update_interval_ms
  metadata, dynamic tween durations clamped to refresh window
- Settings menu (PopupPanel) with General/Plugins/About tabs, plugin
  activation toggles, per-plugin settings cog buttons
- Plugin settings popup (PopupPanel) with dynamic UI from setting definitions
  (SpinBox/CheckBox/LineEdit), auto-save on change
- Modal behavior: exclusive = true on settings windows
- Touch-friendly sizing: enlarged close buttons, cog buttons, controls, spacing
- Red corner close button redesign with rounded corners, hover/pressed states
- Split system_monitor into local_system_monitor (CPU, Memory) and test plugins
- Plugin activation/deactivation with layout preservation
This commit is contained in:
Eric Smith 2026-05-21 12:47:25 -04:00
parent 12b45b2685
commit 57b36798b9
24 changed files with 1065 additions and 183 deletions

View file

@ -5,6 +5,7 @@ extends PanelContainer
@onready var grid: DashboardGrid = %DashboardGrid
var _tile_instances: Array[Control] = []
var _last_refresh: Dictionary = {} # tile_instance -> last tick ms
func _ready() -> void:
@ -32,6 +33,13 @@ func _load_tiles_from_plugins() -> void:
if all_tile_defs.is_empty():
return
# Set grid dimensions from layout before placing tiles
if LayoutManager.has_method("get_grid_size"):
var ls := LayoutManager.get_grid_size()
grid.grid_columns = ls.columns
grid.grid_rows = ls.rows
grid._rebuild_grid()
# Load saved layout (if any)
var layout_data: Dictionary = {}
if LayoutManager.has_method("get_layout"):
@ -42,13 +50,14 @@ func _load_tiles_from_plugins() -> void:
# First pass: place tiles that have a saved layout position
for tid in layout_data:
var pos: Dictionary = layout_data[tid]
var def := _find_tile_def(all_tile_defs, tid)
if def.is_empty():
# Plugin/tile not installed — skip gracefully
continue
var instance := PluginManager.instantiate_tile(tid)
if instance == null:
continue
var pos: Dictionary = layout_data[tid]
grid.place_module(instance, pos.col, pos.row, pos.w, pos.h)
_tile_instances.append(instance)
placed_ids.append(tid)
@ -79,36 +88,152 @@ func _load_tiles_from_plugins() -> void:
next_col = 0
next_row = gs.y
# Connect auto-save signals
grid.module_placed.connect(_save_layout)
# Connect auto-save signals and tile tracking
grid.module_placed.connect(_on_module_placed)
if grid.has_signal("module_resized"):
grid.module_resized.connect(_save_layout)
# Start refresh timer
_start_refresh_timer()
# Listen for plugin activation changes
if PluginManager.has_signal("plugin_active_changed"):
PluginManager.plugin_active_changed.connect(_on_plugin_active_changed)
# Listen for plugin setting changes to update cached intervals
if PluginManager.has_signal("plugin_setting_changed"):
PluginManager.plugin_setting_changed.connect(_on_plugin_setting_changed)
# Cache intervals for all placed tiles
for mod in _tile_instances:
if is_instance_valid(mod):
_cache_tile_interval(mod)
# Start per-tile refresh tick
_start_tick_timer()
func _start_refresh_timer() -> void:
var interval: float = 1.0
if ConfigManager.has_method("get_setting"):
var val: Variant = ConfigManager.get_setting("performance_refresh_interval", 1.0)
if typeof(val) == TYPE_FLOAT or typeof(val) == TYPE_INT:
interval = float(val)
func _start_tick_timer() -> void:
var timer := Timer.new()
timer.timeout.connect(_refresh_modules)
timer.timeout.connect(_tick_refresh)
timer.autostart = true
timer.wait_time = interval
timer.wait_time = 0.1
add_child(timer)
func _refresh_modules() -> void:
func _tick_refresh() -> void:
var now := Time.get_ticks_msec()
var data: Dictionary = {}
for mod in _tile_instances:
if is_instance_valid(mod) and mod.has_method("refresh"):
if not is_instance_valid(mod) or not mod.has_method("refresh"):
continue
var interval: int = mod.get_meta("update_interval_ms", 1000)
var last: int = _last_refresh.get(mod, 0)
if now - last >= interval:
_last_refresh[mod] = now
data["update_interval_ms"] = interval
mod.refresh(data)
func _cache_tile_interval(mod: Control) -> void:
var tid: String = mod.get_meta("tile_id", "")
if tid.is_empty():
return
var parts := tid.split("/")
if parts.size() < 2:
return
var pid: String = parts[0]
var tile_local: String = parts[1]
if PluginManager.has_method("get_plugin_setting"):
var interval: int = PluginManager.get_plugin_setting(pid, tile_local + "_update_interval_ms", 1000)
mod.set_meta("update_interval_ms", interval)
func _on_module_placed(mod: Control, col: int, row: int) -> void:
_save_layout(mod, col, row)
if mod not in _tile_instances:
_tile_instances.append(mod)
_cache_tile_interval(mod)
# Give the new tile a chance to set up before the next refresh
if mod.has_method("refresh"):
mod.refresh({})
func _on_plugin_setting_changed(plugin_id: String, key: String, value: Variant) -> void:
# Update cached interval on affected tiles
var suffix := "_update_interval_ms"
if not key.ends_with(suffix):
return
for mod in _tile_instances:
if not is_instance_valid(mod):
continue
var tid: String = mod.get_meta("tile_id", "")
if tid.begins_with(plugin_id + "/"):
_cache_tile_interval(mod)
func _on_plugin_active_changed(plugin_id: String, active: bool) -> void:
if active:
_activate_plugin_tiles(plugin_id)
else:
_deactivate_plugin_tiles(plugin_id)
func _deactivate_plugin_tiles(plugin_id: String) -> void:
# Save layout first to preserve tile positions
_save_layout()
var to_remove: Array[Control] = []
for mod in _tile_instances:
if not is_instance_valid(mod):
continue
var tid: String = mod.get_meta("tile_id", "")
if tid.begins_with(plugin_id + "/"):
to_remove.append(mod)
for mod in to_remove:
grid.remove_module(mod)
_tile_instances.erase(mod)
_last_refresh.erase(mod)
mod.queue_free()
# Do NOT save layout again — old positions are preserved in the saved layout
func _activate_plugin_tiles(plugin_id: String) -> void:
var tile_ids: PackedStringArray = PluginManager.get_plugin_tile_ids(plugin_id)
if tile_ids.is_empty():
return
var layout_data: Dictionary = {}
if LayoutManager.has_method("get_layout"):
layout_data = LayoutManager.get_layout()
var gs := grid.get_grid_size()
for tid in tile_ids:
var instance := PluginManager.instantiate_tile(tid)
if instance == null:
continue
var pos: Dictionary = layout_data.get(tid, {})
if pos.is_empty():
# No saved position — skip
instance.queue_free()
continue
var col := clampi(pos.get("col", 0), 0, gs.x - 1)
var row := clampi(pos.get("row", 0), 0, gs.y - 1)
var w := clampi(pos.get("w", 1), 1, gs.x - col)
var h := clampi(pos.get("h", 1), 1, gs.y - row)
# Only place if the cell is free
var existing := grid.get_module_at(col, row)
if existing != null:
instance.queue_free()
continue
grid.place_module(instance, col, row, w, h)
_tile_instances.append(instance)
_cache_tile_interval(instance)
func _save_layout(_mod: Control = null, _a: int = 0, _b: int = 0) -> void:
if not LayoutManager.has_method("set_tile_position"):
return