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

@ -7,10 +7,12 @@ signal module_placed(module: Control, col: int, row: int)
signal module_removed(module: Control)
signal module_resized(module: Control, col: int, row: int)
@export var cell_min_size: Vector2 = Vector2(300, 240)
@export var grid_columns: int = 4
@export var grid_rows: int = 3
@export var cell_spacing: float = 8.0
@export var margin: float = 16.0
## Internal grid dimensions (synced to grid_columns / grid_rows on rebuild).
var columns: int = 0
var rows: int = 0
@ -49,6 +51,10 @@ var _resize_preview: ColorRect = null # ghost showing new size
var _cell_w: float = 1.0
var _cell_h: float = 1.0
# Grid origin offset (centered within the control area)
var _base_x: float = 0.0
var _base_y: float = 0.0
# Resize edge detection threshold (pixels)
const RESIZE_THRESHOLD: float = 10.0
const EDGE_LEFT: int = 1
@ -161,6 +167,7 @@ func _show_tile_menu(mouse_pos: Vector2) -> void:
vbox.add_child(toolbar)
var settings_btn := _make_menu_button("", "Settings")
settings_btn.pressed.connect(_open_settings.bind(panel))
toolbar.add_child(settings_btn)
var info_btn := _make_menu_button("", "Info")
@ -269,19 +276,36 @@ func _add_tile_from_def(def: Dictionary) -> void:
if instance == null:
return
# Find a free position at the end of the grid
var gs := get_grid_size()
var target_col := 0
var target_row := gs.y
# Try current row first
var dw: int = def.get("default_w", 1)
var dh: int = def.get("default_h", 1)
for c in range(gs.x):
if _span_fits(c, target_row, dw, dh, null):
target_col = c
break
place_module(instance, target_col, target_row, dw, dh)
# Find a free position within the grid bounds
for r in range(grid_rows):
for c in range(grid_columns):
if _span_fits(c, r, dw, dh, null):
place_module(instance, c, r, dw, dh)
return
# No room — try shrinking span to 1x1
if dw > 1 or dh > 1:
for r in range(grid_rows):
for c in range(grid_columns):
if _span_fits(c, r, 1, 1, null):
place_module(instance, c, r, 1, 1)
return
# Grid completely full — queue the instance for cleanup
if is_instance_valid(instance):
instance.queue_free()
func _open_settings(menu_panel: PopupPanel) -> void:
if is_instance_valid(menu_panel):
menu_panel.queue_free()
var settings: PopupPanel = load("res://scenes/settings_menu.tscn").instantiate()
add_child(settings)
settings.exclusive = true
settings.popup_centered(Vector2i(720, 540))
# --------------------------------------------------------------------------
@ -400,17 +424,16 @@ func _clear_occupancy(col: int, row: int, w: int, h: int) -> void:
func _ensure_occupancy_for_span(col: int, row: int, w: int, h: int) -> void:
var needed_rows := row + h
var needed_cols := col + w
var needed_rows := mini(row + h, grid_rows)
var needed_cols := mini(col + w, grid_columns)
while _grid.size() < needed_rows:
_grid.append([])
for r in range(_grid.size()):
while _grid[r].size() < needed_cols:
_grid[r].append(null)
if columns < needed_cols:
columns = needed_cols
if rows < needed_rows:
rows = needed_rows
# Never expand beyond the configured fixed grid
columns = grid_columns
rows = grid_rows
func _is_valid_cell(col: int, row: int) -> bool:
@ -442,10 +465,8 @@ func _cell_at_position(pos: Vector2) -> Vector2i:
if columns == 0 or rows == 0 or _cell_w <= 0 or _cell_h <= 0:
return Vector2i(-1, -1)
var inner_x := margin + cell_spacing
var inner_y := margin + cell_spacing
var col := int((pos.x - inner_x) / (_cell_w + cell_spacing))
var row := int((pos.y - inner_y) / (_cell_h + cell_spacing))
var col := int((pos.x - _base_x) / (_cell_w + cell_spacing))
var row := int((pos.y - _base_y) / (_cell_h + cell_spacing))
col = clampi(col, 0, columns - 1)
row = clampi(row, 0, rows - 1)
return Vector2i(col, row)
@ -758,8 +779,8 @@ func _finish_drag() -> void:
# --------------------------------------------------------------------------
func _get_module_rect(col: int, row: int, w: int, h: int) -> Rect2:
var x := margin + cell_spacing + col * (_cell_w + cell_spacing)
var y := margin + cell_spacing + row * (_cell_h + cell_spacing)
var x := _base_x + col * (_cell_w + cell_spacing)
var y := _base_y + row * (_cell_h + cell_spacing)
var mw := w * _cell_w + (w - 1) * cell_spacing
var mh := h * _cell_h + (h - 1) * cell_spacing
return Rect2(x, y, mw, mh)
@ -805,48 +826,16 @@ func _rebuild_grid() -> void:
if child is PopupMenu or child is PopupPanel:
child.queue_free()
var avail := size - Vector2(margin * 2.0, margin * 2.0)
var new_cols: int
var new_rows: int
if avail.x < cell_min_size.x or avail.y < cell_min_size.y:
if _cells.size() > 0:
_layout_cells()
return
new_cols = 1
new_rows = 1
if new_cols == columns and new_rows == rows and _cells.size() > 0:
_layout_cells()
return
columns = new_cols
rows = new_rows
# If grid dimensions changed, rebuild cells and re-place modules
if columns != grid_columns or rows != grid_rows:
columns = grid_columns
rows = grid_rows
_save_modules()
_teardown_cells()
_build_cells()
_restore_modules()
else:
_layout_cells()
return
new_cols = maxi(1, int(avail.x / (cell_min_size.x + cell_spacing)))
var module_count := _module_list.size()
var min_rows := maxi(1, ceili(float(module_count) / float(new_cols)))
var avail_rows := maxi(1, int(avail.y / (cell_min_size.y + cell_spacing)))
new_rows = maxi(avail_rows, min_rows)
if new_cols == columns and new_rows == rows and _cells.size() > 0:
_layout_cells()
return
columns = new_cols
rows = new_rows
_save_modules()
_teardown_cells()
_build_cells()
_restore_modules()
_layout_cells()
func _cancel_drag() -> void:
@ -955,12 +944,23 @@ func _layout_cells() -> void:
return
_ensure_cells_match_grid()
var inner_w := maxf(0.0, size.x - margin * 2.0 - cell_spacing)
var inner_h := maxf(0.0, size.y - margin * 2.0 - cell_spacing)
var gap_x := cell_spacing * (columns - 1)
var gap_y := cell_spacing * (rows - 1)
_cell_w = maxf(1.0, (inner_w - gap_x) / columns)
_cell_h = maxf(1.0, (inner_h - gap_y) / rows)
var gap_x := cell_spacing * maxi(0, columns - 1)
var gap_y := cell_spacing * maxi(0, rows - 1)
# Compute square cell size that fits within the available area
var avail_x := maxf(0.0, size.x - margin * 2.0)
var avail_y := maxf(0.0, size.y - margin * 2.0)
var cell_w_from_x := maxf(1.0, (avail_x - gap_x) / columns)
var cell_h_from_y := maxf(1.0, (avail_y - gap_y) / rows)
var cell_size := minf(cell_w_from_x, cell_h_from_y)
_cell_w = cell_size
_cell_h = cell_size
# Center the grid within the available space
var grid_w := columns * _cell_w + gap_x
var grid_h := rows * _cell_h + gap_y
_base_x = margin + maxf(0.0, (size.x - margin * 2.0 - grid_w) / 2.0)
_base_y = margin + maxf(0.0, (size.y - margin * 2.0 - grid_h) / 2.0)
# Position cells
for row in range(rows):
@ -970,8 +970,8 @@ func _layout_cells() -> void:
var cell: PanelContainer = _cells[row][col]
if not is_instance_valid(cell):
continue
var x := margin + cell_spacing + col * (_cell_w + cell_spacing)
var y := margin + cell_spacing + row * (_cell_h + cell_spacing)
var x := _base_x + col * (_cell_w + cell_spacing)
var y := _base_y + row * (_cell_h + cell_spacing)
cell.set_position(Vector2(x, y))
cell.set_size(Vector2(_cell_w, _cell_h))
@ -1057,31 +1057,39 @@ func _ensure_occupancy_from_grid() -> void:
if _grid[r][c] != null:
max_c = maxi(max_c, c + 1)
max_r = maxi(max_r, r + 1)
columns = maxi(columns, max_c)
rows = maxi(rows, max_r)
# Clamp to fixed grid — never expand beyond configured size
columns = clampi(maxi(columns, max_c), 1, grid_columns)
rows = clampi(maxi(rows, max_r), 1, grid_rows)
func _find_and_place(module: Control) -> void:
var d := _get_module_grid_data(module)
# Try the desired position first
if _span_fits(d.col, d.row, d.w, d.h, null):
_ensure_occupancy_for_span(d.col, d.row, d.w, d.h)
_occupy_cells(module, d.col, d.row, d.w, d.h)
# Clamp span to grid bounds
var sw := mini(d.w, columns)
var sh := mini(d.h, rows)
# Try the desired position first (clamped)
var try_col := clampi(d.col, 0, columns - sw)
var try_row := clampi(d.row, 0, rows - sh)
if _span_fits(try_col, try_row, sw, sh, null):
_set_module_grid_data(module, try_col, try_row, sw, sh)
_ensure_occupancy_for_span(try_col, try_row, sw, sh)
_occupy_cells(module, try_col, try_row, sw, sh)
return
# Find first available position
# Find first available position within bounds
for r in range(rows):
for c in range(columns):
if _span_fits(c, r, d.w, d.h, null):
_set_module_grid_data(module, c, r, d.w, d.h)
_ensure_occupancy_for_span(c, r, d.w, d.h)
_occupy_cells(module, c, r, d.w, d.h)
var fit_sw := mini(sw, columns - c)
var fit_sh := mini(sh, rows - r)
if _span_fits(c, r, fit_sw, fit_sh, null):
_set_module_grid_data(module, c, r, fit_sw, fit_sh)
_ensure_occupancy_for_span(c, r, fit_sw, fit_sh)
_occupy_cells(module, c, r, fit_sw, fit_sh)
return
# No room — expand grid
var new_col := columns
var new_row := 0
_ensure_occupancy_for_span(new_col, new_row, d.w, d.h)
columns = new_col + d.w
_set_module_grid_data(module, new_col, new_row, d.w, d.h)
_occupy_cells(module, new_col, new_row, d.w, d.h)
# No room — place at (0,0), nudging whatever is there
_clear_occupancy(0, 0, sw, sh)
_set_module_grid_data(module, 0, 0, sw, sh)
_ensure_occupancy_for_span(0, 0, sw, sh)
_occupy_cells(module, 0, 0, sw, sh)