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
This commit is contained in:
parent
63af41ea61
commit
f43676e46c
19 changed files with 528 additions and 39 deletions
41
AGENTS.md
41
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.
|
- 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.
|
- 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
|
### 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
|
### 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.
|
- `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/
|
│ ├── icons/
|
||||||
│ └── textures/ # noise_100.png (tileable Perlin noise for shader distortion)
|
│ └── textures/ # noise_100.png (tileable Perlin noise for shader distortion)
|
||||||
├── autoload/ # Singleton/autoload scripts
|
├── 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)
|
├── config/ # Configuration files (INI format)
|
||||||
│ ├── default.cfg # Shipped defaults, tracked in git
|
│ ├── default.cfg # Shipped defaults, tracked in git
|
||||||
│ └── config.cfg # User overrides, gitignored (auto-generated)
|
│ ├── config.cfg # User overrides, gitignored (auto-generated)
|
||||||
├── panels/ # Individual status panels (modules)
|
│ └── layouts/ # Saved layout files (*.cfg)
|
||||||
│ ├── cpu/
|
├── plugins/ # Plugin folders
|
||||||
│ ├── memory/
|
│ └── system_monitor/ # Built-in system monitoring plugin
|
||||||
│ ├── network/
|
│ ├── plugin.cfg
|
||||||
│ ├── disk/
|
│ └── tiles/
|
||||||
│ └── testing/
|
│ ├── cpu/
|
||||||
|
│ ├── memory/
|
||||||
|
│ └── testing/
|
||||||
├── scenes/ # Root scenes
|
├── scenes/ # Root scenes
|
||||||
│ ├── splash.tscn # Animated splash → dashboard transition
|
│ ├── splash.tscn # Animated splash → dashboard transition
|
||||||
│ └── dashboard.tscn # Main dashboard (PanelContainer root)
|
│ └── dashboard.tscn # Main dashboard (PanelContainer root)
|
||||||
|
|
@ -100,6 +114,7 @@ res://
|
||||||
│ ├── dashboard_grid.gd # Responsive grid with drag, resize, preset popup
|
│ ├── dashboard_grid.gd # Responsive grid with drag, resize, preset popup
|
||||||
│ ├── module_base.gd # Base class for all modules (mouse_filter IGNORE)
|
│ ├── module_base.gd # Base class for all modules (mouse_filter IGNORE)
|
||||||
│ ├── panel_base.gd # Base class for panels
|
│ ├── 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
|
│ └── shader_presets.gd # Shader preset definitions and apply function
|
||||||
├── themes/ # Theme definitions and style resources
|
├── themes/ # Theme definitions and style resources
|
||||||
├── shaders/ # Custom shader materials
|
├── shaders/ # Custom shader materials
|
||||||
|
|
|
||||||
150
autoload/layout_manager.gd
Normal file
150
autoload/layout_manager.gd
Normal file
|
|
@ -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)
|
||||||
154
autoload/plugin_manager.gd
Normal file
154
autoload/plugin_manager.gd
Normal file
|
|
@ -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
|
||||||
|
|
@ -12,3 +12,6 @@ refresh_interval=1.0
|
||||||
|
|
||||||
[general]
|
[general]
|
||||||
theme=default
|
theme=default
|
||||||
|
|
||||||
|
[layout]
|
||||||
|
current="default"
|
||||||
|
|
|
||||||
0
plugins/.gitkeep
Normal file
0
plugins/.gitkeep
Normal file
41
plugins/system_monitor/plugin.cfg
Normal file
41
plugins/system_monitor/plugin.cfg
Normal file
|
|
@ -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
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
extends ModuleBase
|
extends PluginTile
|
||||||
class_name CpuModule
|
|
||||||
|
|
||||||
|
|
||||||
@onready var title_label: Label = %Title
|
@onready var title_label: Label = %Title
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
[gd_scene format=3 uid="uid://bq3bs2hb4r7fb"]
|
[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"]
|
[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
|
anchors_preset = 0
|
||||||
script = ExtResource("1")
|
script = ExtResource("1")
|
||||||
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
extends ModuleBase
|
extends PluginTile
|
||||||
class_name MemoryModule
|
|
||||||
|
|
||||||
|
|
||||||
@onready var title_label: Label = %Title
|
@onready var title_label: Label = %Title
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
[gd_scene format=3 uid="uid://d2d4uqrd2hh3d"]
|
[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"]
|
[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
|
anchors_preset = 0
|
||||||
script = ExtResource("1")
|
script = ExtResource("1")
|
||||||
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
extends ModuleBase
|
extends PluginTile
|
||||||
class_name TestingModule
|
|
||||||
|
|
||||||
|
|
||||||
@onready var title_label: Label = %Title
|
@onready var title_label: Label = %Title
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
[gd_scene format=3]
|
[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"]
|
[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
|
anchors_preset = 0
|
||||||
script = ExtResource("1")
|
script = ExtResource("1")
|
||||||
|
|
||||||
|
|
@ -20,6 +20,8 @@ config/icon="res://assets/icons/icon.svg"
|
||||||
[autoload]
|
[autoload]
|
||||||
|
|
||||||
ConfigManager="*res://autoload/config_manager.gd"
|
ConfigManager="*res://autoload/config_manager.gd"
|
||||||
|
PluginManager="*res://autoload/plugin_manager.gd"
|
||||||
|
LayoutManager="*res://autoload/layout_manager.gd"
|
||||||
|
|
||||||
[display]
|
[display]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,13 @@ extends PanelContainer
|
||||||
|
|
||||||
@onready var grid: DashboardGrid = %DashboardGrid
|
@onready var grid: DashboardGrid = %DashboardGrid
|
||||||
|
|
||||||
var _modules: Array = []
|
var _tile_instances: Array[Control] = []
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
_set_background()
|
_set_background()
|
||||||
if not Engine.is_editor_hint():
|
if not Engine.is_editor_hint():
|
||||||
_add_modules()
|
_load_tiles_from_plugins()
|
||||||
|
|
||||||
|
|
||||||
func _set_background() -> void:
|
func _set_background() -> void:
|
||||||
|
|
@ -24,28 +24,124 @@ func _set_background() -> void:
|
||||||
add_theme_stylebox_override("panel", bg)
|
add_theme_stylebox_override("panel", bg)
|
||||||
|
|
||||||
|
|
||||||
func _add_modules() -> void:
|
func _load_tiles_from_plugins() -> void:
|
||||||
var cpu := preload("res://panels/cpu/cpu_module.tscn").instantiate()
|
if not PluginManager.has_method("get_all_tile_defs"):
|
||||||
grid.place_module(cpu, 0, 0)
|
return
|
||||||
_modules.append(cpu)
|
|
||||||
|
|
||||||
var mem := preload("res://panels/memory/memory_module.tscn").instantiate()
|
var all_tile_defs: Array[Dictionary] = PluginManager.get_all_tile_defs()
|
||||||
grid.place_module(mem, 1, 0)
|
if all_tile_defs.is_empty():
|
||||||
_modules.append(mem)
|
return
|
||||||
|
|
||||||
var test := preload("res://panels/testing/testing_module.tscn").instantiate()
|
# Load saved layout (if any)
|
||||||
grid.place_module(test, 2, 0)
|
var layout_data: Dictionary = {}
|
||||||
_modules.append(test)
|
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()
|
var timer := Timer.new()
|
||||||
timer.timeout.connect(_refresh_modules)
|
timer.timeout.connect(_refresh_modules)
|
||||||
timer.autostart = true
|
timer.autostart = true
|
||||||
timer.wait_time = 1.0
|
timer.wait_time = interval
|
||||||
add_child(timer)
|
add_child(timer)
|
||||||
|
|
||||||
|
|
||||||
func _refresh_modules() -> void:
|
func _refresh_modules() -> void:
|
||||||
var data: Dictionary = {}
|
var data: Dictionary = {}
|
||||||
for mod in _modules:
|
for mod in _tile_instances:
|
||||||
if is_instance_valid(mod) and mod.has_method("refresh"):
|
if is_instance_valid(mod) and mod.has_method("refresh"):
|
||||||
mod.refresh(data)
|
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 {}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ class_name DashboardGrid
|
||||||
|
|
||||||
signal module_placed(module: Control, col: int, row: int)
|
signal module_placed(module: Control, col: int, row: int)
|
||||||
signal module_removed(module: Control)
|
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_min_size: Vector2 = Vector2(300, 240)
|
||||||
@export var cell_spacing: float = 8.0
|
@export var cell_spacing: float = 8.0
|
||||||
|
|
@ -161,6 +162,12 @@ func remove_module(module: Control) -> void:
|
||||||
_layout_cells()
|
_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:
|
func get_grid_size() -> Vector2i:
|
||||||
return Vector2i(columns, rows)
|
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)
|
_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)
|
_occupy_cells(_resize_module, _resize_origin_col, _resize_origin_row, _resize_origin_w, _resize_origin_h)
|
||||||
_layout_cells()
|
_layout_cells()
|
||||||
|
module_resized.emit(_resize_module, _resize_origin_col, _resize_origin_row, _resize_origin_w, _resize_origin_h)
|
||||||
|
|
||||||
_is_resizing = false
|
_is_resizing = false
|
||||||
_resize_module = null
|
_resize_module = null
|
||||||
|
|
|
||||||
23
scripts/plugin_tile.gd
Normal file
23
scripts/plugin_tile.gd
Normal file
|
|
@ -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)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue