From f43676e46cf5bfd474c3e5d2bff6412e01cb04f8 Mon Sep 17 00:00:00 2001 From: Eric Smith <5d@fifthdread.com> Date: Thu, 21 May 2026 08:39:15 -0400 Subject: [PATCH] add plugin system with plugin/layout managers - plugin_manager: scans res://plugins/*/plugin.cfg, loads tile definitions - plugin_tile: new base class for plugin tiles, extends ModuleBase - layout_manager: save/restore tile grid positions per named layout - migrate existing cpu/memory/testing tiles into system_monitor plugin - add module_resized signal to DashboardGrid for auto-save - dashboard reads from PluginManager + LayoutManager instead of hardcoded paths - project.godot registers PluginManager and LayoutManager autoloads - AGENTS.md updated with new structure and conventions --- AGENTS.md | 41 +++-- autoload/layout_manager.gd | 150 +++++++++++++++++ autoload/plugin_manager.gd | 154 ++++++++++++++++++ config/default.cfg | 3 + {panels => config/layouts}/.gitkeep | 0 plugins/.gitkeep | 0 plugins/system_monitor/plugin.cfg | 41 +++++ .../tiles}/cpu/cpu_collector.gd | 0 .../system_monitor/tiles/cpu/cpu_tile.gd | 3 +- .../system_monitor/tiles/cpu/cpu_tile.tscn | 4 +- .../tiles}/memory/memory_collector.gd | 0 .../tiles/memory/memory_tile.gd | 3 +- .../tiles/memory/memory_tile.tscn | 4 +- .../tiles/testing/testing_tile.gd | 3 +- .../tiles/testing/testing_tile.tscn | 4 +- project.godot | 2 + scenes/dashboard.gd | 124 ++++++++++++-- scripts/dashboard_grid.gd | 8 + scripts/plugin_tile.gd | 23 +++ 19 files changed, 528 insertions(+), 39 deletions(-) create mode 100644 autoload/layout_manager.gd create mode 100644 autoload/plugin_manager.gd rename {panels => config/layouts}/.gitkeep (100%) create mode 100644 plugins/.gitkeep create mode 100644 plugins/system_monitor/plugin.cfg rename {panels => plugins/system_monitor/tiles}/cpu/cpu_collector.gd (100%) rename panels/cpu/cpu_module.gd => plugins/system_monitor/tiles/cpu/cpu_tile.gd (98%) rename panels/cpu/cpu_module.tscn => plugins/system_monitor/tiles/cpu/cpu_tile.tscn (89%) rename {panels => plugins/system_monitor/tiles}/memory/memory_collector.gd (100%) rename panels/memory/memory_module.gd => plugins/system_monitor/tiles/memory/memory_tile.gd (98%) rename panels/memory/memory_module.tscn => plugins/system_monitor/tiles/memory/memory_tile.tscn (88%) rename panels/testing/testing_module.gd => plugins/system_monitor/tiles/testing/testing_tile.gd (97%) rename panels/testing/testing_module.tscn => plugins/system_monitor/tiles/testing/testing_tile.tscn (87%) create mode 100644 scripts/plugin_tile.gd diff --git a/AGENTS.md b/AGENTS.md index 154b341..1a2b09e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -61,12 +61,22 @@ V Panel is a Godot Engine project that builds a fancy real-time status monitor. - All percentage labels get a 3px black outline (`outline_size` / `font_outline_color`) for legibility against the liquid shader background. - Title labels get a 2px outline. +### Plugin System +- `autoload/plugin_manager.gd` — autoload singleton, scans `res://plugins/*/plugin.cfg` at startup. +- Each plugin folder contains a `plugin.cfg` INI file with `[plugin]` metadata and `[tile_N]` entries defining tiles (id, name, scene path, min/max grid spans). +- Tile IDs are scoped as `{plugin_id}/{tile_id}` to guarantee global uniqueness. +- `PluginManager.instantiate_tile(tile_id)` loads the tile scene, instantiates it, and tags the root node with `tile_id` and `tile_config` metadata. +- `scripts/plugin_tile.gd` — class_name `PluginTile`, extends `ModuleBase`. Plugin tiles extend this instead of ModuleBase directly. Stores `tile_id` and `tile_config` properties, also written to node metadata for grid/layout access. + +### Layout System +- `autoload/layout_manager.gd` — autoload singleton for saving/restoring tile grid positions. +- Layout files stored as INI in `res://config/layouts/{name}.cfg`. Each entry maps a `tile_id` to `{col, row, w, h}`. +- `save_layout()` writes the current in-memory layout to disk. `load_layout(name)` reads it back. +- `switch_layout(name)` saves current layout, loads the named one, emits signals. +- Current layout name persisted in `ConfigManager` as `layout_current`. +- Layout auto-saves on every `module_placed` and `module_resized` signal from `DashboardGrid`. + ### Config System -- `autoload/config_manager.gd` — autoload singleton using Godot's built-in `ConfigFile` class for INI-style config files. -- Two files: `res://config/default.cfg` (shipped defaults, tracked) and `res://config/config.cfg` (user overrides, gitignored). User overrides are merged on top of defaults per-key. -- Keys are flattened as `{section}_{key}` for simple `get_setting(key, default)` lookups (e.g., `[background]` + `color` => `"background_color"`). -- `get_color(key, default_color)` parses `"r, g, b"` string values from config into `Color`. -- `set_setting(key, value)` diffs against defaults and writes only the diff to `config.cfg` automatically. ### Splash Screen - `scenes/splash.tscn` + `scenes/splash.gd` — entry point (set in `project.godot` as `main_scene`), handles fullscreen, font-size zoom animation, crossfade to dashboard. @@ -83,16 +93,20 @@ res:// │ ├── icons/ │ └── textures/ # noise_100.png (tileable Perlin noise for shader distortion) ├── autoload/ # Singleton/autoload scripts -│ └── config_manager.gd # INI config via ConfigFile +│ ├── config_manager.gd # INI config via ConfigFile +│ ├── plugin_manager.gd # Plugin scanning, tile instantiation +│ └── layout_manager.gd # Layout save/load/switch ├── config/ # Configuration files (INI format) │ ├── default.cfg # Shipped defaults, tracked in git -│ └── config.cfg # User overrides, gitignored (auto-generated) -├── panels/ # Individual status panels (modules) -│ ├── cpu/ -│ ├── memory/ -│ ├── network/ -│ ├── disk/ -│ └── testing/ +│ ├── config.cfg # User overrides, gitignored (auto-generated) +│ └── layouts/ # Saved layout files (*.cfg) +├── plugins/ # Plugin folders +│ └── system_monitor/ # Built-in system monitoring plugin +│ ├── plugin.cfg +│ └── tiles/ +│ ├── cpu/ +│ ├── memory/ +│ └── testing/ ├── scenes/ # Root scenes │ ├── splash.tscn # Animated splash → dashboard transition │ └── dashboard.tscn # Main dashboard (PanelContainer root) @@ -100,6 +114,7 @@ res:// │ ├── dashboard_grid.gd # Responsive grid with drag, resize, preset popup │ ├── module_base.gd # Base class for all modules (mouse_filter IGNORE) │ ├── panel_base.gd # Base class for panels +│ ├── plugin_tile.gd # Base class for plugin tiles (extends ModuleBase) │ └── shader_presets.gd # Shader preset definitions and apply function ├── themes/ # Theme definitions and style resources ├── shaders/ # Custom shader materials diff --git a/autoload/layout_manager.gd b/autoload/layout_manager.gd new file mode 100644 index 0000000..a69465d --- /dev/null +++ b/autoload/layout_manager.gd @@ -0,0 +1,150 @@ +extends Node +class_name LayoutManager +## Manages saved dashboard layouts. +## +## Layouts map tile_id -> {col, row, w, h}. The current layout is loaded +## on startup and auto-saved when modules are placed or resized. +## Supports multiple named layouts with a simple file-per-layout scheme. + + +signal layout_changed(layout_name: String) +signal layout_saved(layout_name: String) +signal layout_loaded(layout_name: String) + +const LAYOUT_DIR: String = "res://config/layouts/" + +## Current layout name (e.g. "default"). +var current_layout_name: String = "default" + +## The active layout data: tile_id -> {col, row, w, h} +var _layout: Dictionary = {} + + +func _ready() -> void: + if not DirAccess.dir_exists_absolute(LAYOUT_DIR): + DirAccess.make_dir_recursive_absolute(LAYOUT_DIR) + + var configured_name: String = _get_configured_layout() + if not configured_name.is_empty(): + current_layout_name = configured_name + + if not load_layout(current_layout_name): + current_layout_name = "default" + _layout.clear() + + +## Return the layout name from config, or empty. +func _get_configured_layout() -> String: + if ConfigManager.has_method("get_setting"): + var val: Variant = ConfigManager.get_setting("layout_current", "") + if val is String and not val.is_empty(): + return val + return "" + + +## Set the current layout name (does not load or save). +func set_current_layout(name: String) -> void: + current_layout_name = name + if ConfigManager.has_method("set_setting"): + ConfigManager.set_setting("layout_current", name) + layout_changed.emit(name) + + +## Get tile_id -> {col, row, w, h} for the current layout. +func get_layout() -> Dictionary: + return _layout.duplicate(true) + + +## Set a single tile's position in the current layout. +func set_tile_position(tile_id: String, col: int, row: int, w: int, h: int) -> void: + _layout[tile_id] = {col = col, row = row, w = w, h = h} + + +## Remove a tile from the current layout. +func remove_tile(tile_id: String) -> void: + _layout.erase(tile_id) + + +## Save the current layout to config/layouts/{name}.cfg +func save_layout(name: String = "") -> void: + if name.is_empty(): + name = current_layout_name + + var cfg := ConfigFile.new() + cfg.set_value("layout", "name", name) + cfg.set_value("layout", "saved_at", Time.get_datetime_string_from_system()) + + var tile_ids: Array = _layout.keys() + cfg.set_value("tiles", "count", tile_ids.size()) + for i in tile_ids.size(): + var tid: String = tile_ids[i] as String + var entry: Dictionary = _layout[tid] + var section := "tile_%d" % i + cfg.set_value(section, "id", tid) + cfg.set_value(section, "col", entry.get("col", 0)) + cfg.set_value(section, "row", entry.get("row", 0)) + cfg.set_value(section, "w", entry.get("w", 1)) + cfg.set_value(section, "h", entry.get("h", 1)) + + var path := LAYOUT_DIR.path_join(name + ".cfg") + var result := cfg.save(path) + if result == OK: + current_layout_name = name + layout_saved.emit(name) + + +## Load a saved layout, replacing the current in-memory layout. +## Returns true if the file existed and was parsed. +func load_layout(name: String) -> bool: + var path := LAYOUT_DIR.path_join(name + ".cfg") + if not FileAccess.file_exists(path): + return false + + var cfg := ConfigFile.new() + if cfg.load(path) != OK: + return false + + _layout.clear() + current_layout_name = name + + var tile_count: int = cfg.get_value("tiles", "count", 0) + for i in range(tile_count): + var section := "tile_%d" % i + var tid: String = cfg.get_value(section, "id", "") + if tid.is_empty(): + continue + _layout[tid] = { + col = cfg.get_value(section, "col", 0), + row = cfg.get_value(section, "row", 0), + w = cfg.get_value(section, "w", 1), + h = cfg.get_value(section, "h", 1), + } + + layout_loaded.emit(name) + return true + + +## Get a list of all saved layout names. +func get_layout_names() -> PackedStringArray: + var dir := DirAccess.open(LAYOUT_DIR) + if dir == null: + return PackedStringArray() + + var names: PackedStringArray = [] + dir.list_dir_begin() + var entry: String = dir.get_next() + while entry != "": + if entry.ends_with(".cfg") and not entry.begins_with("."): + names.append(entry.trim_suffix(".cfg")) + entry = dir.get_next() + dir.list_dir_end() + return names + + +## Switch to a different layout: saves current, loads new, emits signal. +func switch_layout(name: String) -> void: + if name == current_layout_name: + return + save_layout(current_layout_name) + load_layout(name) + set_current_layout(name) diff --git a/autoload/plugin_manager.gd b/autoload/plugin_manager.gd new file mode 100644 index 0000000..4b69fff --- /dev/null +++ b/autoload/plugin_manager.gd @@ -0,0 +1,154 @@ +extends Node +class_name PluginManager +## Scans res://plugins/ for plugin manifests (plugin.cfg) and provides +## access to tile definitions. Instantiate tiles by tile_id. + + +signal plugin_loaded(plugin_id: String, plugin_name: String) +signal plugin_load_failed(plugin_id: String, reason: String) + +## All loaded plugin data: plugin_id -> Dictionary +var _plugins: Dictionary = {} + +## All tile definitions: tile_id -> Dictionary +var _tiles: Dictionary = {} + +## Plugin-id -> Array[tile_id] for reverse lookup +var _plugin_tiles: Dictionary = {} + + +func _ready() -> void: + _scan_plugins() + + +## Scan res://plugins/*/plugin.cfg and load all manifests. +func _scan_plugins() -> void: + var dir := DirAccess.open("res://plugins") + if dir == null: + return + + dir.list_dir_begin() + var entry: String = dir.get_next() + while entry != "": + if entry.begins_with("."): + entry = dir.get_next() + continue + if dir.current_is_dir(): + var manifest_path := "res://plugins/%s/plugin.cfg" % entry + if FileAccess.file_exists(manifest_path): + _load_manifest(entry, manifest_path) + else: + plugin_load_failed.emit(entry, "Missing plugin.cfg") + entry = dir.get_next() + dir.list_dir_end() + + +func _load_manifest(plugin_id: String, path: String) -> void: + var cfg := ConfigFile.new() + if cfg.load(path) != OK: + plugin_load_failed.emit(plugin_id, "Failed to parse plugin.cfg") + return + + var plugin_info := { + id = plugin_id, + name = cfg.get_value("plugin", "name", plugin_id), + version = cfg.get_value("plugin", "version", "0.1.0"), + description = cfg.get_value("plugin", "description", ""), + author = cfg.get_value("plugin", "author", ""), + base_path = path.get_base_dir(), + } + _plugins[plugin_id] = plugin_info + + # Load tile definitions + var tile_count: int = cfg.get_value("tiles", "count", 0) + for i in range(tile_count): + var section := "tile_%d" % i + var tile_id: String = cfg.get_value(section, "id", "") + if tile_id.is_empty(): + continue + + # Scoped tile-id: "plugin_id/tile_id" ensures global uniqueness + var scoped_id := "%s/%s" % [plugin_id, tile_id] + + if _tiles.has(scoped_id): + plugin_load_failed.emit(plugin_id, "Duplicate tile id: %s" % scoped_id) + continue + + var tile_def := { + id = scoped_id, + plugin_id = plugin_id, + name = cfg.get_value(section, "name", tile_id), + scene = cfg.get_value(section, "scene", ""), + min_w = cfg.get_value(section, "min_w", 1), + min_h = cfg.get_value(section, "min_h", 1), + max_w = cfg.get_value(section, "max_w", 4), + max_h = cfg.get_value(section, "max_h", 4), + default_w = cfg.get_value(section, "default_w", 1), + default_h = cfg.get_value(section, "default_h", 1), + } + _tiles[scoped_id] = tile_def + + if not _plugin_tiles.has(plugin_id): + _plugin_tiles[plugin_id] = [] + _plugin_tiles[plugin_id].append(scoped_id) + + plugin_loaded.emit(plugin_id, plugin_info.name) + + +## Returns a list of all loaded plugin IDs. +func get_plugin_ids() -> PackedStringArray: + var result: PackedStringArray = [] + for pid in _plugins: + result.append(pid) + return result + + +## Returns plugin info Dictionary for the given plugin_id, or null. +func get_plugin(plugin_id: String) -> Dictionary: + return _plugins.get(plugin_id, {}) + + +## Returns all tile definitions across all plugins. +func get_all_tile_defs() -> Array[Dictionary]: + var result: Array[Dictionary] = [] + for tid in _tiles: + result.append(_tiles[tid]) + return result + + +## Returns a single tile definition by scoped tile_id, or null. +func get_tile_def(tile_id: String) -> Dictionary: + return _tiles.get(tile_id, {}) + + +## Returns tile IDs belonging to a specific plugin. +func get_plugin_tile_ids(plugin_id: String) -> PackedStringArray: + var result: PackedStringArray = [] + for tid in _plugin_tiles.get(plugin_id, []): + result.append(tid) + return result + + +## Instantiate a tile by its scoped tile_id. +## Returns the root Control node, or null on failure. +func instantiate_tile(tile_id: String) -> Control: + var def: Dictionary = _tiles.get(tile_id, {}) + if def.is_empty(): + return null + var scene_path: String = def.get("scene", "") + if scene_path.is_empty(): + return null + var scene := load(scene_path) as PackedScene + if scene == null: + return null + var instance: Control = scene.instantiate() as Control + if instance == null: + return null + + # Tag the instance so the layout manager can correlate it + if instance.has_method("set_tile_id"): + instance.set_tile_id(tile_id) + if instance.has_method("set_tile_config"): + instance.set_tile_config(def) + + return instance diff --git a/config/default.cfg b/config/default.cfg index 5dbc23d..61b4e84 100644 --- a/config/default.cfg +++ b/config/default.cfg @@ -12,3 +12,6 @@ refresh_interval=1.0 [general] theme=default + +[layout] +current="default" diff --git a/panels/.gitkeep b/config/layouts/.gitkeep similarity index 100% rename from panels/.gitkeep rename to config/layouts/.gitkeep diff --git a/plugins/.gitkeep b/plugins/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/plugins/system_monitor/plugin.cfg b/plugins/system_monitor/plugin.cfg new file mode 100644 index 0000000..a9910c8 --- /dev/null +++ b/plugins/system_monitor/plugin.cfg @@ -0,0 +1,41 @@ +[plugin] +name="System Monitor" +version="1.0.0" +description="Monitors system resources: CPU and memory usage" +author="Fifthdread" + +[tiles] +count=3 + +[tile_0] +id="cpu" +name="CPU Monitor" +scene="res://plugins/system_monitor/tiles/cpu/cpu_tile.tscn" +min_w=1 +min_h=1 +max_w=4 +max_h=4 +default_w=1 +default_h=1 + +[tile_1] +id="memory" +name="Memory Monitor" +scene="res://plugins/system_monitor/tiles/memory/memory_tile.tscn" +min_w=1 +min_h=1 +max_w=4 +max_h=4 +default_w=1 +default_h=1 + +[tile_2] +id="testing" +name="Testing" +scene="res://plugins/system_monitor/tiles/testing/testing_tile.tscn" +min_w=1 +min_h=1 +max_w=4 +max_h=4 +default_w=1 +default_h=1 diff --git a/panels/cpu/cpu_collector.gd b/plugins/system_monitor/tiles/cpu/cpu_collector.gd similarity index 100% rename from panels/cpu/cpu_collector.gd rename to plugins/system_monitor/tiles/cpu/cpu_collector.gd diff --git a/panels/cpu/cpu_module.gd b/plugins/system_monitor/tiles/cpu/cpu_tile.gd similarity index 98% rename from panels/cpu/cpu_module.gd rename to plugins/system_monitor/tiles/cpu/cpu_tile.gd index 6c06d2c..7c49564 100644 --- a/panels/cpu/cpu_module.gd +++ b/plugins/system_monitor/tiles/cpu/cpu_tile.gd @@ -1,5 +1,4 @@ -extends ModuleBase -class_name CpuModule +extends PluginTile @onready var title_label: Label = %Title diff --git a/panels/cpu/cpu_module.tscn b/plugins/system_monitor/tiles/cpu/cpu_tile.tscn similarity index 89% rename from panels/cpu/cpu_module.tscn rename to plugins/system_monitor/tiles/cpu/cpu_tile.tscn index 481e1bc..1979127 100644 --- a/panels/cpu/cpu_module.tscn +++ b/plugins/system_monitor/tiles/cpu/cpu_tile.tscn @@ -1,9 +1,9 @@ [gd_scene format=3 uid="uid://bq3bs2hb4r7fb"] -[ext_resource type="Script" path="res://panels/cpu/cpu_module.gd" id="1"] +[ext_resource type="Script" path="res://plugins/system_monitor/tiles/cpu/cpu_tile.gd" id="1"] [ext_resource type="Shader" path="res://shaders/vial_fill.gdshader" id="2"] -[node name="CpuModule" type="PanelContainer"] +[node name="CpuTile" type="PanelContainer"] anchors_preset = 0 script = ExtResource("1") diff --git a/panels/memory/memory_collector.gd b/plugins/system_monitor/tiles/memory/memory_collector.gd similarity index 100% rename from panels/memory/memory_collector.gd rename to plugins/system_monitor/tiles/memory/memory_collector.gd diff --git a/panels/memory/memory_module.gd b/plugins/system_monitor/tiles/memory/memory_tile.gd similarity index 98% rename from panels/memory/memory_module.gd rename to plugins/system_monitor/tiles/memory/memory_tile.gd index ce56849..0cb57ea 100644 --- a/panels/memory/memory_module.gd +++ b/plugins/system_monitor/tiles/memory/memory_tile.gd @@ -1,5 +1,4 @@ -extends ModuleBase -class_name MemoryModule +extends PluginTile @onready var title_label: Label = %Title diff --git a/panels/memory/memory_module.tscn b/plugins/system_monitor/tiles/memory/memory_tile.tscn similarity index 88% rename from panels/memory/memory_module.tscn rename to plugins/system_monitor/tiles/memory/memory_tile.tscn index 814d36f..2915c70 100644 --- a/panels/memory/memory_module.tscn +++ b/plugins/system_monitor/tiles/memory/memory_tile.tscn @@ -1,9 +1,9 @@ [gd_scene format=3 uid="uid://d2d4uqrd2hh3d"] -[ext_resource type="Script" path="res://panels/memory/memory_module.gd" id="1"] +[ext_resource type="Script" path="res://plugins/system_monitor/tiles/memory/memory_tile.gd" id="1"] [ext_resource type="Shader" path="res://shaders/vial_fill.gdshader" id="2"] -[node name="MemoryModule" type="PanelContainer"] +[node name="MemoryTile" type="PanelContainer"] anchors_preset = 0 script = ExtResource("1") diff --git a/panels/testing/testing_module.gd b/plugins/system_monitor/tiles/testing/testing_tile.gd similarity index 97% rename from panels/testing/testing_module.gd rename to plugins/system_monitor/tiles/testing/testing_tile.gd index fb923ee..1fdd52e 100644 --- a/panels/testing/testing_module.gd +++ b/plugins/system_monitor/tiles/testing/testing_tile.gd @@ -1,5 +1,4 @@ -extends ModuleBase -class_name TestingModule +extends PluginTile @onready var title_label: Label = %Title diff --git a/panels/testing/testing_module.tscn b/plugins/system_monitor/tiles/testing/testing_tile.tscn similarity index 87% rename from panels/testing/testing_module.tscn rename to plugins/system_monitor/tiles/testing/testing_tile.tscn index 196574b..4c453f6 100644 --- a/panels/testing/testing_module.tscn +++ b/plugins/system_monitor/tiles/testing/testing_tile.tscn @@ -1,9 +1,9 @@ [gd_scene format=3] -[ext_resource type="Script" path="res://panels/testing/testing_module.gd" id="1"] +[ext_resource type="Script" path="res://plugins/system_monitor/tiles/testing/testing_tile.gd" id="1"] [ext_resource type="Shader" path="res://shaders/vial_fill.gdshader" id="2"] -[node name="TestingModule" type="PanelContainer"] +[node name="TestingTile" type="PanelContainer"] anchors_preset = 0 script = ExtResource("1") diff --git a/project.godot b/project.godot index 3c9d97e..bfe107d 100644 --- a/project.godot +++ b/project.godot @@ -20,6 +20,8 @@ config/icon="res://assets/icons/icon.svg" [autoload] ConfigManager="*res://autoload/config_manager.gd" +PluginManager="*res://autoload/plugin_manager.gd" +LayoutManager="*res://autoload/layout_manager.gd" [display] diff --git a/scenes/dashboard.gd b/scenes/dashboard.gd index b1244dc..fef891c 100644 --- a/scenes/dashboard.gd +++ b/scenes/dashboard.gd @@ -4,13 +4,13 @@ extends PanelContainer @onready var grid: DashboardGrid = %DashboardGrid -var _modules: Array = [] +var _tile_instances: Array[Control] = [] func _ready() -> void: _set_background() if not Engine.is_editor_hint(): - _add_modules() + _load_tiles_from_plugins() func _set_background() -> void: @@ -24,28 +24,124 @@ func _set_background() -> void: add_theme_stylebox_override("panel", bg) -func _add_modules() -> void: - var cpu := preload("res://panels/cpu/cpu_module.tscn").instantiate() - grid.place_module(cpu, 0, 0) - _modules.append(cpu) +func _load_tiles_from_plugins() -> void: + if not PluginManager.has_method("get_all_tile_defs"): + return - var mem := preload("res://panels/memory/memory_module.tscn").instantiate() - grid.place_module(mem, 1, 0) - _modules.append(mem) + var all_tile_defs: Array[Dictionary] = PluginManager.get_all_tile_defs() + if all_tile_defs.is_empty(): + return - var test := preload("res://panels/testing/testing_module.tscn").instantiate() - grid.place_module(test, 2, 0) - _modules.append(test) + # Load saved layout (if any) + var layout_data: Dictionary = {} + if LayoutManager.has_method("get_layout"): + layout_data = LayoutManager.get_layout() + + # Track which tile_defs we've placed + var placed_ids: Array[String] = [] + + # 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(): + continue + var instance := PluginManager.instantiate_tile(tid) + if instance == null: + continue + grid.place_module(instance, pos.col, pos.row, pos.w, pos.h) + _tile_instances.append(instance) + placed_ids.append(tid) + + # Second pass: place any remaining tile_defs at default positions + # Find the next free grid column + var next_col: int = 0 + var next_row: int = 0 + if not placed_ids.is_empty(): + var gs := grid.get_grid_size() + next_col = gs.x + next_row = 0 + + for def in all_tile_defs: + if def.id in placed_ids: + continue + var instance := PluginManager.instantiate_tile(def.id) + if instance == null: + continue + grid.place_module(instance, next_col, next_row, def.default_w, def.default_h) + _tile_instances.append(instance) + placed_ids.append(def.id) + + # Bump default insertion point + next_col += def.default_w + var gs := grid.get_grid_size() + if next_col >= gs.x and gs.x > 0: + next_col = 0 + next_row = gs.y + + # Connect auto-save signals + grid.module_placed.connect(_save_layout) + if grid.has_signal("module_resized"): + grid.module_resized.connect(_save_layout) + + # Start refresh timer + _start_refresh_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) var timer := Timer.new() timer.timeout.connect(_refresh_modules) timer.autostart = true - timer.wait_time = 1.0 + timer.wait_time = interval add_child(timer) func _refresh_modules() -> void: var data: Dictionary = {} - for mod in _modules: + for mod in _tile_instances: if is_instance_valid(mod) and mod.has_method("refresh"): mod.refresh(data) + + +func _save_layout(_mod: Control = null, _a: int = 0, _b: int = 0) -> void: + if not LayoutManager.has_method("set_tile_position"): + return + + # Walk all modules in the grid and store their positions + var gs := grid.get_grid_size() + for r in range(gs.y): + for c in range(gs.x): + var mod := grid.get_module_at(c, r) + if mod == null: + continue + var tid: String = mod.get_meta("tile_id", "") + if tid.is_empty(): + continue + var d := _get_module_grid_data(mod) + LayoutManager.set_tile_position(tid, d.col, d.row, d.w, d.h) + + LayoutManager.save_layout() + + +## Mirror of DashboardGrid._get_module_grid_data for layout saving. +func _get_module_grid_data(module: Control) -> Dictionary: + return { + col = module.get_meta("grid_col", 0), + row = module.get_meta("grid_row", 0), + w = module.get_meta("grid_w", 1), + h = module.get_meta("grid_h", 1), + } + + +## Find a tile def by scoped tile_id. +func _find_tile_def(defs: Array[Dictionary], tile_id: String) -> Dictionary: + for d in defs: + if d.get("id", "") == tile_id: + return d + return {} diff --git a/scripts/dashboard_grid.gd b/scripts/dashboard_grid.gd index 0f3173b..538fa48 100644 --- a/scripts/dashboard_grid.gd +++ b/scripts/dashboard_grid.gd @@ -5,6 +5,7 @@ class_name DashboardGrid signal module_placed(module: Control, col: int, row: int) signal module_removed(module: Control) +signal module_resized(module: Control, col: int, row: int, w: int, h: int) @export var cell_min_size: Vector2 = Vector2(300, 240) @export var cell_spacing: float = 8.0 @@ -161,6 +162,12 @@ func remove_module(module: Control) -> void: _layout_cells() +func get_module_at(col: int, row: int) -> Control: + if not _is_valid_cell(col, row): + return null + return _grid[row][col] as Control + + func get_grid_size() -> Vector2i: return Vector2i(columns, rows) @@ -421,6 +428,7 @@ func _end_resize(_mouse_pos: Vector2) -> void: _set_module_grid_data(_resize_module, _resize_origin_col, _resize_origin_row, _resize_origin_w, _resize_origin_h) _occupy_cells(_resize_module, _resize_origin_col, _resize_origin_row, _resize_origin_w, _resize_origin_h) _layout_cells() + module_resized.emit(_resize_module, _resize_origin_col, _resize_origin_row, _resize_origin_w, _resize_origin_h) _is_resizing = false _resize_module = null diff --git a/scripts/plugin_tile.gd b/scripts/plugin_tile.gd new file mode 100644 index 0000000..9a4b9bf --- /dev/null +++ b/scripts/plugin_tile.gd @@ -0,0 +1,23 @@ +extends ModuleBase +class_name PluginTile +## Base class for all plugin tiles. +## +## Extends ModuleBase with plugin-aware metadata so the system can +## correlate tile instances with their definitions and layouts. + + +var tile_id: String = "" +var tile_config: Dictionary = {} + + +## Called by PluginManager.instantiate_tile() to tag this instance. +func set_tile_id(value: String) -> void: + tile_id = value + + +## Called by PluginManager.instantiate_tile() to pass the tile definition. +func set_tile_config(config: Dictionary) -> void: + tile_config = config + # Store in meta so grid/layout can read it on any Control + set_meta("tile_id", tile_id) + set_meta("tile_config", config)