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:
parent
12b45b2685
commit
57b36798b9
24 changed files with 1065 additions and 183 deletions
57
AGENTS.md
57
AGENTS.md
|
|
@ -24,9 +24,11 @@ V Panel is a Godot Engine project that builds a fancy real-time status monitor.
|
||||||
- Modules are direct children of the grid (not cells). Visual cells (`PanelContainer`) are shown only during drag as drop-zone guides.
|
- Modules are direct children of the grid (not cells). Visual cells (`PanelContainer`) are shown only during drag as drop-zone guides.
|
||||||
- Grid data model: each module stores `grid_col`, `grid_row`, `grid_w`, `grid_h` via `set_meta()`. A 2D `_grid[row][col]` array tracks cell occupancy (supports multi-cell spans).
|
- Grid data model: each module stores `grid_col`, `grid_row`, `grid_w`, `grid_h` via `set_meta()`. A 2D `_grid[row][col]` array tracks cell occupancy (supports multi-cell spans).
|
||||||
- `place_module(module, col, row, span_w=1, span_h=1)` — public API for adding modules with optional span.
|
- `place_module(module, col, row, span_w=1, span_h=1)` — public API for adding modules with optional span.
|
||||||
|
- `@export var grid_columns: int` / `@export var grid_rows: int` — fixed grid dimensions. Square cells, centered layout.
|
||||||
- Drag-and-drop: `_begin_drag` → `_update_drag` → `_end_drag`. Occupancy is cleared during drag and restored on drop. Swaps modules when dropping on occupied cells. Falls back to source position if dropped in grid gap or outside grid bounds (`mouse_exited` safety handler).
|
- Drag-and-drop: `_begin_drag` → `_update_drag` → `_end_drag`. Occupancy is cleared during drag and restored on drop. Swaps modules when dropping on occupied cells. Falls back to source position if dropped in grid gap or outside grid bounds (`mouse_exited` safety handler).
|
||||||
- Resize: `_try_begin_resize` detects clicks within 10px of any module edge. `_update_resize_preview` shows a ghost `ColorRect` at the target span. `_end_resize` commits the new span after overlap check. All 4 edges and corners supported.
|
- Resize: `_try_begin_resize` detects clicks within 10px of any module edge. `_update_resize_preview` shows a ghost `ColorRect` at the target span. `_end_resize` commits the new span after overlap check. All 4 edges and corners supported.
|
||||||
- Double-click: opens `PopupMenu` with shader preset names from `ShaderPresets`. Selection applies all preset params to the module's `ShaderMaterial`.
|
- Long-press gesture: 500ms hold, 10px drag threshold. Opens action menu with toolbar (⚙, ℹ, ✕/+) and "Themes ▸" submenu.
|
||||||
|
- `_open_settings(menu_panel)` — opens `settings_menu.tscn` as exclusive modal PopupPanel.
|
||||||
- Grid rebuild (`_rebuild_grid`) preserves module metadata (span data lives on modules). Cells are torn down and rebuilt on column/row changes. Orphaned popups cleaned up during rebuild.
|
- Grid rebuild (`_rebuild_grid`) preserves module metadata (span data lives on modules). Cells are torn down and rebuilt on column/row changes. Orphaned popups cleaned up during rebuild.
|
||||||
|
|
||||||
### Shaders
|
### Shaders
|
||||||
|
|
@ -63,20 +65,33 @@ V Panel is a Godot Engine project that builds a fancy real-time status monitor.
|
||||||
|
|
||||||
### Plugin System
|
### Plugin System
|
||||||
- `autoload/plugin_manager.gd` — autoload singleton, scans `res://plugins/*/plugin.cfg` at startup.
|
- `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).
|
- Each plugin folder contains a `plugin.cfg` INI file with `[plugin]` metadata, `[tile_N]` entries defining tiles, and optional `[settings]` section.
|
||||||
- Tile IDs are scoped as `{plugin_id}/{tile_id}` to guarantee global uniqueness.
|
- 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.
|
- `PluginManager.instantiate_tile(tile_id)` loads the tile scene, instantiates it, and tags the root node with `tile_id` and `tile_config` metadata. Returns null if source plugin is inactive.
|
||||||
|
- `PluginManager.get_plugin_settings(pid)` — returns setting definitions from manifest.
|
||||||
|
- `PluginManager.get_plugin_setting(pid, key, default)` — reads persisted value, falls back to manifest default.
|
||||||
|
- `PluginManager.set_plugin_setting(pid, key, value)` — persists to `config/plugin_settings.cfg`, emits `plugin_setting_changed` signal.
|
||||||
|
- `plugin_active_changed` signal — emitted when plugin activation state changes via `set_plugin_active(pid, active)`.
|
||||||
- `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.
|
- `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.
|
||||||
|
|
||||||
|
### Plugin Settings
|
||||||
|
- Storage: `config/plugin_settings.cfg` using ConfigFile directly (not ConfigManager) to avoid key-flattening conflicts with plugin IDs containing underscores.
|
||||||
|
- Manifest `[settings]` section: `count=N`, followed by `[setting_N]` entries with `key`, `label`, `type` (int/float/bool/string), optional `default`, `min`, `max`, `step`.
|
||||||
|
- ConfigFile.get_value treats `null` as "no default" — use `has_section_key()` before reading optional keys that may not exist in the manifest.
|
||||||
|
- Settings auto-save on every change (no Apply/Cancel pattern).
|
||||||
|
|
||||||
### Layout System
|
### Layout System
|
||||||
- `autoload/layout_manager.gd` — autoload singleton for saving/restoring tile grid positions.
|
- `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}`.
|
- Layout files stored as INI in `res://config/layouts/{name}.cfg`. Each entry maps a `tile_id` to `{col, row, w, h}`. `[layout]` section has `grid_columns`/`grid_rows`.
|
||||||
- `save_layout()` writes the current in-memory layout to disk. `load_layout(name)` reads it back.
|
- `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.
|
- `switch_layout(name)` saves current layout, loads the named one, emits signals.
|
||||||
- Current layout name persisted in `ConfigManager` as `layout_current`.
|
- Current layout name persisted in `ConfigManager` as `layout_current`.
|
||||||
- Layout auto-saves on every `module_placed` and `module_resized` signal from `DashboardGrid`.
|
- Layout auto-saves on every `module_placed` and `module_resized` signal from `DashboardGrid`.
|
||||||
|
|
||||||
### Config System
|
### Config System
|
||||||
|
- `autoload/config_manager.gd` — autoload singleton, wraps ConfigFile for app-wide config.
|
||||||
|
- `config/default.cfg` — shipped defaults. `config/config.cfg` — user overrides (gitignored).
|
||||||
|
- `ConfigFile` reserved words (`default`, case-insensitive) cannot be used as any value.
|
||||||
|
|
||||||
### 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.
|
||||||
|
|
@ -84,6 +99,19 @@ V Panel is a Godot Engine project that builds a fancy real-time status monitor.
|
||||||
- Flow: Black overlay reveal (0.4s) → font-size animation with TRANS_BACK overshoot (2s) + parallel fade-in (1.8s) → hold (1s) → splash text fades out (1s, EASE_IN) → dashboard instantiated and fades in (1s, EASE_OUT) → reparent to root, queue_free splash.
|
- Flow: Black overlay reveal (0.4s) → font-size animation with TRANS_BACK overshoot (2s) + parallel fade-in (1.8s) → hold (1s) → splash text fades out (1s, EASE_IN) → dashboard instantiated and fades in (1s, EASE_OUT) → reparent to root, queue_free splash.
|
||||||
- Dashboard background (`PanelContainer`) and splash background (`ColorRect`) both read from `ConfigManager.get_color("background_color")` for a seamless crossfade — no fade-to-black transition.
|
- Dashboard background (`PanelContainer`) and splash background (`ColorRect`) both read from `ConfigManager.get_color("background_color")` for a seamless crossfade — no fade-to-black transition.
|
||||||
|
|
||||||
|
### Settings Menu
|
||||||
|
- `scenes/settings_menu.tscn` + `scenes/settings_menu.gd` — `PopupPanel` with `TabContainer` (General, Plugins, About tabs).
|
||||||
|
- Opens as exclusive modal via the ⚙ cog in the tile action menu.
|
||||||
|
- Plugins tab: dynamic list showing plugin name, version, Active checkbox, and ⚙ settings cog (only shown if plugin has setting definitions).
|
||||||
|
- About tab: project name, version `0.1.0-alpha`, contact, description.
|
||||||
|
- `scenes/plugin_settings_popup.tscn` + `.gd` — per-plugin settings popup, opened via cog in Plugins tab. Builds UI dynamically from setting definitions (SpinBox for int/float, CheckBox for bool, LineEdit for string). Settings auto-save on every change.
|
||||||
|
|
||||||
|
### Per-Tile Refresh
|
||||||
|
- `scenes/dashboard.gd` — per-tile refresh tracking. Base timer ticks at 100ms. Each tile has `update_interval_ms` cached as metadata via `_cache_tile_interval()`.
|
||||||
|
- `_tick_refresh()` checks `now - last_refresh >= interval` per tile. Interval defaults to 1000ms, configurable via plugin settings.
|
||||||
|
- `_on_plugin_setting_changed()` re-caches intervals on live setting changes.
|
||||||
|
- Tween durations in tiles are dynamic: `minf(hardcoded_duration, update_interval_ms / divisor)` — CPU/Memory use divisor 2000, Testing uses 1500.
|
||||||
|
|
||||||
### Project Structure
|
### Project Structure
|
||||||
```
|
```
|
||||||
res://
|
res://
|
||||||
|
|
@ -94,24 +122,30 @@ res://
|
||||||
│ └── 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
|
│ ├── plugin_manager.gd # Plugin scanning, tile instantiation, settings CRUD
|
||||||
│ └── layout_manager.gd # Layout save/load/switch
|
│ └── 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)
|
||||||
|
│ ├── plugin_settings.cfg # Plugin settings storage (auto-created)
|
||||||
│ └── layouts/ # Saved layout files (*.cfg)
|
│ └── layouts/ # Saved layout files (*.cfg)
|
||||||
├── plugins/ # Plugin folders
|
├── plugins/ # Plugin folders
|
||||||
│ └── system_monitor/ # Built-in system monitoring plugin
|
│ ├── local_system_monitor/ # CPU + Memory tiles
|
||||||
|
│ │ ├── plugin.cfg
|
||||||
|
│ │ └── tiles/
|
||||||
|
│ │ ├── cpu/
|
||||||
|
│ │ └── memory/
|
||||||
|
│ └── test/ # Testing tile
|
||||||
│ ├── plugin.cfg
|
│ ├── plugin.cfg
|
||||||
│ └── tiles/
|
│ └── tiles/
|
||||||
│ ├── cpu/
|
|
||||||
│ ├── memory/
|
|
||||||
│ └── testing/
|
│ └── 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)
|
||||||
|
│ ├── settings_menu.tscn # Settings popup
|
||||||
|
│ └── plugin_settings_popup.tscn # Per-plugin settings popup
|
||||||
├── scripts/ # Shared utility scripts
|
├── scripts/ # Shared utility scripts
|
||||||
│ ├── dashboard_grid.gd # Responsive grid with drag, resize, preset popup
|
│ ├── dashboard_grid.gd # Responsive grid with drag, resize, long-press, action menu
|
||||||
│ ├── 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)
|
│ ├── plugin_tile.gd # Base class for plugin tiles (extends ModuleBase)
|
||||||
|
|
@ -129,6 +163,9 @@ res://
|
||||||
- `mouse_exited` signal connected on the grid — aborts drag/resize if the mouse leaves the grid area.
|
- `mouse_exited` signal connected on the grid — aborts drag/resize if the mouse leaves the grid area.
|
||||||
- `_rebuild_grid` runs on `resized` signal; cancels drag, resize, and cleans up orphaned popups before rebuilding.
|
- `_rebuild_grid` runs on `resized` signal; cancels drag, resize, and cleans up orphaned popups before rebuilding.
|
||||||
- Shader `corner_radius` is in UV space (0–0.5) where 0.05 ≈ 12px on a 240px‑tall cell.
|
- Shader `corner_radius` is in UV space (0–0.5) where 0.05 ≈ 12px on a 240px‑tall cell.
|
||||||
|
- ConfigFile `get_value(section, key, null)` treats `null` as "no default" — use `has_section_key()` for optional fields.
|
||||||
|
- `Button.custom_minimum_size` is the writable property (not `minimum_size` or `custom_min_size`).
|
||||||
|
- `default` is a reserved ConfigFile keyword case-insensitively — cannot be used as any value.
|
||||||
|
|
||||||
### Git Workflow
|
### Git Workflow
|
||||||
- Main branch: `main`
|
- Main branch: `main`
|
||||||
|
|
|
||||||
29
README.md
29
README.md
|
|
@ -9,31 +9,40 @@ V Panel is a visually rich, real-time status monitoring dashboard built entirely
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Animated splash screen** — Fullscreen splash with "V" and "Panel" text using Orbitron variable font. Font-size zoom animation via `tween_method` with overshoot (TRANS_BACK), then crossfades to dashboard with no black flash.
|
- **Animated splash screen** — Fullscreen splash with "V" and "Panel" text using Orbitron variable font. Font-size zoom animation via `tween_method` with overshoot (TRANS_BACK), then crossfades to dashboard with no black flash.
|
||||||
- **Responsive grid dashboard** — Modules auto-arrange in a dynamic grid that adapts to window size. Drag-and-drop to rearrange.
|
- **Responsive grid dashboard** — Fixed `grid_columns`/`grid_rows` (settable via @export). Square cells, centered layout. Drag-and-drop to rearrange.
|
||||||
- **Variable-size modules** — Modules can span multiple grid cells (1×1, 2×1, 2×2, etc.). Drag edges/corners to resize interactively.
|
- **Variable-size modules** — Modules can span multiple grid cells (1×1, 2×1, 2×2, etc.). Drag edges/corners to resize interactively.
|
||||||
- **Shader-based vial fill** — Each module uses a custom `vial_fill.gdshader` instead of progress bars. Features sum-of-sines water surface with edge-damped meniscus, wave distortion, ripple rings, swirl, HSV colour shifting, top-down lighting, sparkle effects, 3D subsurface scattering, gaussian surface foam, and wave-slope specular highlights.
|
- **Shader-based vial fill** — Each module uses a custom `vial_fill.gdshader` instead of progress bars. Features sum-of-sines water surface with edge-damped meniscus, wave distortion, ripple rings, swirl, HSV colour shifting, top-down lighting, sparkle effects, 3D subsurface scattering, gaussian surface foam, and wave-slope specular highlights.
|
||||||
- **Shader preset system** — Double-click any module to open a popup menu with 7 visual presets (Vivid Vial, Emerald Deep, Lava Flow, Neon Dream, Deep Purple, Rainbow Swirl, Frostbite). Presets control all visual shader parameters.
|
- **Shader preset system** — Long-press (500ms) or double-click any module to open an action menu with 7 visual presets (Vivid Vial, Emerald Deep, Lava Flow, Neon Dream, Deep Purple, Rainbow Swirl, Frostbite).
|
||||||
- **INI-based configuration** — Uses Godot's built-in `ConfigFile` class for human-friendly `.cfg` files. `config/default.cfg` for shipped defaults, `config/config.cfg` for user overrides (gitignored). Background color, module visibility, refresh interval, and theme are configurable.
|
- **Plugin architecture** — Tiles are defined as plugins under `plugins/*/plugin.cfg`. Each plugin declares metadata, tile definitions, and optional settings. Tile IDs scoped as `{plugin_id}/{tile_id}`.
|
||||||
|
- **Plugin settings** — Per-plugin configurable values stored in `config/plugin_settings.cfg`. Settings defined in manifest `[settings]` section (int/float/bool/string types with min/max constraints).
|
||||||
|
- **Per-tile refresh intervals** — Each tile has its own configurable refresh interval (default 1000ms). Base tick runs at 100ms, tiles refresh only when their interval elapses.
|
||||||
|
- **Animation scaling** — Tween durations dynamically clamp based on each tile's refresh interval to prevent animation from outlasting the update window.
|
||||||
|
- **Layout persistence** — Named layouts saved/restored as INI files in `config/layouts/`. Grid dimensions (columns/rows) stored per-layout. Auto-saves on drag/resize.
|
||||||
|
- **Settings menu** — Modal popup with General, Plugins, and About tabs. Plugin activation toggles, per-plugin settings popups with dynamic UI controls.
|
||||||
|
- **Plugin activation** — Plugins can be deactivated from Settings → Plugins tab. Deactivation saves layout, removes tiles. Reactivation places tiles at saved positions if cells are free.
|
||||||
|
- **INI-based configuration** — Uses Godot's built-in `ConfigFile` class for human-friendly `.cfg` files. `config/default.cfg` for shipped defaults, `config/config.cfg` for user overrides (gitignored).
|
||||||
- **Live system monitoring** — Reads CPU usage from `/proc/stat` and memory usage from `/proc/meminfo` on Linux. Extensible module system for adding new collectors.
|
- **Live system monitoring** — Reads CPU usage from `/proc/stat` and memory usage from `/proc/meminfo` on Linux. Extensible module system for adding new collectors.
|
||||||
- **Smooth animations** — All fill level transitions use tweens with cubic easing.
|
- **Smooth animations** — All fill level transitions use tweens with cubic easing.
|
||||||
|
|
||||||
## Modules
|
## Modules
|
||||||
|
|
||||||
| Module | Data Source | Description |
|
| Module | Plugin | Data Source | Description |
|
||||||
|--------|-------------|-------------|
|
|--------|--------|-------------|-------------|
|
||||||
| CPU | `/proc/stat` | Real-time CPU usage percentage |
|
| CPU | `local_system_monitor` | `/proc/stat` | Real-time CPU usage percentage |
|
||||||
| Memory | `/proc/meminfo` | Real-time memory usage percentage |
|
| Memory | `local_system_monitor` | `/proc/meminfo` | Real-time memory usage percentage |
|
||||||
| Testing | N/A (cycles 0–100%) | Shader visual testing with stepped fill levels |
|
| Testing | `test` | N/A (cycles 0–100%) | Shader visual testing with stepped fill levels |
|
||||||
|
|
||||||
## Controls
|
## Controls
|
||||||
|
|
||||||
- **Drag module** — Click and drag to rearrange modules on the grid
|
- **Drag module** — Click and drag to rearrange modules on the grid
|
||||||
- **Resize module** — Click and drag any edge or corner to resize (snaps to cell grid)
|
- **Resize module** — Click and drag any edge or corner to resize (snaps to cell grid)
|
||||||
- **Shader presets** — Double-click a module to open the preset selection popup
|
- **Action menu** — Long-press (500ms hold) or double-click a module to open settings, info, remove, and theme presets
|
||||||
|
- **Add tile** — Long-press empty grid space to add new tiles
|
||||||
|
- **Settings** — Press ⚙ in action menu for settings menu with plugin management
|
||||||
|
|
||||||
## Project Status
|
## Project Status
|
||||||
|
|
||||||
Active development. Core framework, drag-and-drop with resize, CPU/memory collectors, shader-based vial fill with 3D surface effects, preset system, animated splash screen with crossfade transition, and INI-based config system are implemented. More system modules (disk, network) are planned.
|
Version 0.1.0-alpha. Plugin architecture, settings framework, layout persistence, per-tile refresh intervals, and animation scaling are implemented. More system modules (disk, network) are planned.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,10 @@ var current_layout_name: String = "Main"
|
||||||
## The active layout data: tile_id -> {col, row, w, h}
|
## The active layout data: tile_id -> {col, row, w, h}
|
||||||
var _layout: Dictionary = {}
|
var _layout: Dictionary = {}
|
||||||
|
|
||||||
|
## Grid dimensions for this layout.
|
||||||
|
var grid_columns: int = 4
|
||||||
|
var grid_rows: int = 3
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
if not DirAccess.dir_exists_absolute(LAYOUT_DIR):
|
if not DirAccess.dir_exists_absolute(LAYOUT_DIR):
|
||||||
|
|
@ -64,6 +68,17 @@ func remove_tile(tile_id: String) -> void:
|
||||||
_layout.erase(tile_id)
|
_layout.erase(tile_id)
|
||||||
|
|
||||||
|
|
||||||
|
## Set grid dimensions stored in this layout.
|
||||||
|
func set_grid_size(cols: int, rows: int) -> void:
|
||||||
|
grid_columns = cols
|
||||||
|
grid_rows = rows
|
||||||
|
|
||||||
|
|
||||||
|
## Get grid dimensions for the current layout.
|
||||||
|
func get_grid_size() -> Dictionary:
|
||||||
|
return { columns = grid_columns, rows = grid_rows }
|
||||||
|
|
||||||
|
|
||||||
## Save the current layout to config/layouts/{name}.cfg
|
## Save the current layout to config/layouts/{name}.cfg
|
||||||
func save_layout(layout_name: String = "") -> void:
|
func save_layout(layout_name: String = "") -> void:
|
||||||
if layout_name.is_empty():
|
if layout_name.is_empty():
|
||||||
|
|
@ -72,6 +87,8 @@ func save_layout(layout_name: String = "") -> void:
|
||||||
var cfg := ConfigFile.new()
|
var cfg := ConfigFile.new()
|
||||||
cfg.set_value("layout", "name", layout_name)
|
cfg.set_value("layout", "name", layout_name)
|
||||||
cfg.set_value("layout", "saved_at", Time.get_datetime_string_from_system())
|
cfg.set_value("layout", "saved_at", Time.get_datetime_string_from_system())
|
||||||
|
cfg.set_value("layout", "grid_columns", grid_columns)
|
||||||
|
cfg.set_value("layout", "grid_rows", grid_rows)
|
||||||
|
|
||||||
var tile_ids: Array = _layout.keys()
|
var tile_ids: Array = _layout.keys()
|
||||||
cfg.set_value("tiles", "count", tile_ids.size())
|
cfg.set_value("tiles", "count", tile_ids.size())
|
||||||
|
|
@ -106,6 +123,9 @@ func load_layout(name: String) -> bool:
|
||||||
_layout.clear()
|
_layout.clear()
|
||||||
current_layout_name = name
|
current_layout_name = name
|
||||||
|
|
||||||
|
grid_columns = cfg.get_value("layout", "grid_columns", 4)
|
||||||
|
grid_rows = cfg.get_value("layout", "grid_rows", 3)
|
||||||
|
|
||||||
var tile_count: int = cfg.get_value("tiles", "count", 0)
|
var tile_count: int = cfg.get_value("tiles", "count", 0)
|
||||||
for i in range(tile_count):
|
for i in range(tile_count):
|
||||||
var section := "tile_%d" % i
|
var section := "tile_%d" % i
|
||||||
|
|
|
||||||
|
|
@ -5,19 +5,30 @@ extends Node
|
||||||
|
|
||||||
signal plugin_loaded(plugin_id: String, plugin_name: String)
|
signal plugin_loaded(plugin_id: String, plugin_name: String)
|
||||||
signal plugin_load_failed(plugin_id: String, reason: String)
|
signal plugin_load_failed(plugin_id: String, reason: String)
|
||||||
|
signal plugin_active_changed(plugin_id: String, active: bool)
|
||||||
|
signal plugin_setting_changed(plugin_id: String, key: String, value: Variant)
|
||||||
|
|
||||||
## All loaded plugin data: plugin_id -> Dictionary
|
## All loaded plugin data: plugin_id -> Dictionary
|
||||||
var _plugins: Dictionary = {}
|
var _plugins: Dictionary = {}
|
||||||
|
|
||||||
|
## Tracks which plugins are active (plugin_id -> bool). Default true.
|
||||||
|
var _plugin_active: Dictionary = {}
|
||||||
|
|
||||||
## All tile definitions: tile_id -> Dictionary
|
## All tile definitions: tile_id -> Dictionary
|
||||||
var _tiles: Dictionary = {}
|
var _tiles: Dictionary = {}
|
||||||
|
|
||||||
## Plugin-id -> Array[tile_id] for reverse lookup
|
## Plugin-id -> Array[tile_id] for reverse lookup
|
||||||
var _plugin_tiles: Dictionary = {}
|
var _plugin_tiles: Dictionary = {}
|
||||||
|
|
||||||
|
## Per-plugin settings: plugin_id -> { key -> value }
|
||||||
|
var _plugin_settings: Dictionary = {}
|
||||||
|
|
||||||
|
const PLUGIN_SETTINGS_PATH: String = "res://config/plugin_settings.cfg"
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
_scan_plugins()
|
_scan_plugins()
|
||||||
|
_load_plugin_settings()
|
||||||
|
|
||||||
|
|
||||||
## Scan res://plugins/*/plugin.cfg and load all manifests.
|
## Scan res://plugins/*/plugin.cfg and load all manifests.
|
||||||
|
|
@ -57,6 +68,7 @@ func _load_manifest(plugin_id: String, path: String) -> void:
|
||||||
base_path = path.get_base_dir(),
|
base_path = path.get_base_dir(),
|
||||||
}
|
}
|
||||||
_plugins[plugin_id] = plugin_info
|
_plugins[plugin_id] = plugin_info
|
||||||
|
_plugin_active[plugin_id] = true
|
||||||
|
|
||||||
# Load tile definitions
|
# Load tile definitions
|
||||||
var tile_count: int = cfg.get_value("tiles", "count", 0)
|
var tile_count: int = cfg.get_value("tiles", "count", 0)
|
||||||
|
|
@ -91,6 +103,31 @@ func _load_manifest(plugin_id: String, path: String) -> void:
|
||||||
_plugin_tiles[plugin_id] = []
|
_plugin_tiles[plugin_id] = []
|
||||||
_plugin_tiles[plugin_id].append(scoped_id)
|
_plugin_tiles[plugin_id].append(scoped_id)
|
||||||
|
|
||||||
|
# Load setting definitions
|
||||||
|
var setting_defs: Array[Dictionary] = []
|
||||||
|
var setting_count: int = cfg.get_value("settings", "count", 0)
|
||||||
|
for i in range(setting_count):
|
||||||
|
var s_section := "setting_%d" % i
|
||||||
|
var skey: String = cfg.get_value(s_section, "key", "")
|
||||||
|
if skey.is_empty():
|
||||||
|
continue
|
||||||
|
var sd: Dictionary = {
|
||||||
|
key = skey,
|
||||||
|
label = cfg.get_value(s_section, "label", skey),
|
||||||
|
type = cfg.get_value(s_section, "type", "string"),
|
||||||
|
}
|
||||||
|
# Read optional fields that don't have a default value
|
||||||
|
if cfg.has_section_key(s_section, "default"):
|
||||||
|
sd.default = cfg.get_value(s_section, "default")
|
||||||
|
if cfg.has_section_key(s_section, "min"):
|
||||||
|
sd.min = cfg.get_value(s_section, "min")
|
||||||
|
if cfg.has_section_key(s_section, "max"):
|
||||||
|
sd.max = cfg.get_value(s_section, "max")
|
||||||
|
if cfg.has_section_key(s_section, "step"):
|
||||||
|
sd.step = cfg.get_value(s_section, "step")
|
||||||
|
setting_defs.append(sd)
|
||||||
|
plugin_info.settings = setting_defs
|
||||||
|
|
||||||
plugin_loaded.emit(plugin_id, plugin_info.name)
|
plugin_loaded.emit(plugin_id, plugin_info.name)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -107,6 +144,71 @@ func get_plugin(plugin_id: String) -> Dictionary:
|
||||||
return _plugins.get(plugin_id, {})
|
return _plugins.get(plugin_id, {})
|
||||||
|
|
||||||
|
|
||||||
|
## Returns true if the plugin is active (tiles can be instantiated).
|
||||||
|
func is_plugin_active(plugin_id: String) -> bool:
|
||||||
|
return _plugin_active.get(plugin_id, true)
|
||||||
|
|
||||||
|
|
||||||
|
## Enable or disable a plugin. When disabled, tiles cannot be instantiated.
|
||||||
|
func set_plugin_active(plugin_id: String, active: bool) -> void:
|
||||||
|
if _plugin_active.get(plugin_id, true) == active:
|
||||||
|
return
|
||||||
|
_plugin_active[plugin_id] = active
|
||||||
|
plugin_active_changed.emit(plugin_id, active)
|
||||||
|
|
||||||
|
|
||||||
|
## Returns setting definitions for a plugin (from its manifest).
|
||||||
|
func get_plugin_settings(plugin_id: String) -> Array[Dictionary]:
|
||||||
|
var info: Dictionary = _plugins.get(plugin_id, {})
|
||||||
|
var defs: Array = info.get("settings", [])
|
||||||
|
return defs.duplicate()
|
||||||
|
|
||||||
|
|
||||||
|
## Read a plugin setting value, falling back to the default from the manifest.
|
||||||
|
func get_plugin_setting(plugin_id: String, key: String, default_value: Variant = null) -> Variant:
|
||||||
|
if _plugin_settings.has(plugin_id) and _plugin_settings[plugin_id].has(key):
|
||||||
|
return _plugin_settings[plugin_id][key]
|
||||||
|
# Fall back to manifest default
|
||||||
|
var info: Dictionary = _plugins.get(plugin_id, {})
|
||||||
|
for sd in info.get("settings", []):
|
||||||
|
if sd.get("key", "") == key:
|
||||||
|
return sd.get("default", default_value)
|
||||||
|
return default_value
|
||||||
|
|
||||||
|
|
||||||
|
## Persist a plugin setting and emit change signal.
|
||||||
|
func set_plugin_setting(plugin_id: String, key: String, value: Variant) -> void:
|
||||||
|
if not _plugin_settings.has(plugin_id):
|
||||||
|
_plugin_settings[plugin_id] = {}
|
||||||
|
if _plugin_settings[plugin_id].get(key) == value:
|
||||||
|
return
|
||||||
|
_plugin_settings[plugin_id][key] = value
|
||||||
|
_save_plugin_settings()
|
||||||
|
plugin_setting_changed.emit(plugin_id, key, value)
|
||||||
|
|
||||||
|
|
||||||
|
func _load_plugin_settings() -> void:
|
||||||
|
var cfg := ConfigFile.new()
|
||||||
|
if cfg.load(PLUGIN_SETTINGS_PATH) != OK:
|
||||||
|
return
|
||||||
|
for section in cfg.get_sections():
|
||||||
|
_plugin_settings[section] = {}
|
||||||
|
for key in cfg.get_section_keys(section):
|
||||||
|
_plugin_settings[section][key] = cfg.get_value(section, key)
|
||||||
|
|
||||||
|
|
||||||
|
func _save_plugin_settings() -> void:
|
||||||
|
var cfg := ConfigFile.new()
|
||||||
|
for pid in _plugin_settings:
|
||||||
|
for key in _plugin_settings[pid]:
|
||||||
|
cfg.set_value(pid, key, _plugin_settings[pid][key])
|
||||||
|
# Only write if there's data
|
||||||
|
if cfg.get_sections().is_empty():
|
||||||
|
return
|
||||||
|
DirAccess.make_dir_recursive_absolute("res://config")
|
||||||
|
cfg.save(PLUGIN_SETTINGS_PATH)
|
||||||
|
|
||||||
|
|
||||||
## Returns all tile definitions across all plugins.
|
## Returns all tile definitions across all plugins.
|
||||||
func get_all_tile_defs() -> Array[Dictionary]:
|
func get_all_tile_defs() -> Array[Dictionary]:
|
||||||
var result: Array[Dictionary] = []
|
var result: Array[Dictionary] = []
|
||||||
|
|
@ -134,6 +236,10 @@ func instantiate_tile(tile_id: String) -> Control:
|
||||||
var def: Dictionary = _tiles.get(tile_id, {})
|
var def: Dictionary = _tiles.get(tile_id, {})
|
||||||
if def.is_empty():
|
if def.is_empty():
|
||||||
return null
|
return null
|
||||||
|
# Don't instantiate tiles from inactive plugins
|
||||||
|
var pid: String = def.get("plugin_id", "")
|
||||||
|
if not pid.is_empty() and not is_plugin_active(pid):
|
||||||
|
return null
|
||||||
var scene_path: String = def.get("scene", "")
|
var scene_path: String = def.get("scene", "")
|
||||||
if scene_path.is_empty():
|
if scene_path.is_empty():
|
||||||
return null
|
return null
|
||||||
|
|
|
||||||
|
|
@ -9,3 +9,10 @@ show_disk=true
|
||||||
|
|
||||||
[performance]
|
[performance]
|
||||||
refresh_interval=1.0
|
refresh_interval=1.0
|
||||||
|
|
||||||
|
[grid]
|
||||||
|
columns=4
|
||||||
|
rows=3
|
||||||
|
|
||||||
|
[project]
|
||||||
|
version=0.1.0-alpha
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
[layout]
|
[layout]
|
||||||
|
|
||||||
name="Main"
|
name="Main"
|
||||||
saved_at="2026-05-21T09:16:49"
|
saved_at="2026-05-21T12:08:02"
|
||||||
|
grid_columns=4
|
||||||
|
grid_rows=3
|
||||||
|
|
||||||
[tiles]
|
[tiles]
|
||||||
|
|
||||||
|
|
@ -9,24 +11,24 @@ count=3
|
||||||
|
|
||||||
[tile_0]
|
[tile_0]
|
||||||
|
|
||||||
id="system_monitor/cpu"
|
id="local_system_monitor/cpu"
|
||||||
col=1
|
col=3
|
||||||
row=0
|
row=0
|
||||||
w=1
|
w=1
|
||||||
h=1
|
h=1
|
||||||
|
|
||||||
[tile_1]
|
[tile_1]
|
||||||
|
|
||||||
id="system_monitor/memory"
|
id="local_system_monitor/memory"
|
||||||
col=2
|
col=1
|
||||||
row=0
|
row=0
|
||||||
w=1
|
w=1
|
||||||
h=1
|
h=1
|
||||||
|
|
||||||
[tile_2]
|
[tile_2]
|
||||||
|
|
||||||
id="system_monitor/testing"
|
id="test/testing"
|
||||||
col=0
|
col=2
|
||||||
row=0
|
row=0
|
||||||
w=1
|
w=1
|
||||||
h=3
|
h=1
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ count=3
|
||||||
|
|
||||||
[tile_0]
|
[tile_0]
|
||||||
|
|
||||||
id="system_monitor/cpu"
|
id="local_system_monitor/cpu"
|
||||||
col=0
|
col=0
|
||||||
row=0
|
row=0
|
||||||
w=1
|
w=1
|
||||||
|
|
@ -17,7 +17,7 @@ h=1
|
||||||
|
|
||||||
[tile_1]
|
[tile_1]
|
||||||
|
|
||||||
id="system_monitor/testing"
|
id="test/testing"
|
||||||
col=1
|
col=1
|
||||||
row=0
|
row=0
|
||||||
w=1
|
w=1
|
||||||
|
|
@ -25,7 +25,7 @@ h=1
|
||||||
|
|
||||||
[tile_2]
|
[tile_2]
|
||||||
|
|
||||||
id="system_monitor/memory"
|
id="local_system_monitor/memory"
|
||||||
col=2
|
col=2
|
||||||
row=0
|
row=0
|
||||||
w=1
|
w=1
|
||||||
|
|
|
||||||
49
plugins/local_system_monitor/plugin.cfg
Normal file
49
plugins/local_system_monitor/plugin.cfg
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
[plugin]
|
||||||
|
name="Local System Monitor"
|
||||||
|
version="0.1.0"
|
||||||
|
description="Monitors local system resources: CPU and memory"
|
||||||
|
author="Fifthdread"
|
||||||
|
|
||||||
|
[tiles]
|
||||||
|
count=2
|
||||||
|
|
||||||
|
[tile_0]
|
||||||
|
id="cpu"
|
||||||
|
name="CPU Monitor"
|
||||||
|
scene="res://plugins/local_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/local_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
|
||||||
|
|
||||||
|
[settings]
|
||||||
|
count=2
|
||||||
|
|
||||||
|
[setting_0]
|
||||||
|
key="cpu_update_interval_ms"
|
||||||
|
label="CPU Update Interval"
|
||||||
|
type="int"
|
||||||
|
default=1000
|
||||||
|
min=100
|
||||||
|
max=60000
|
||||||
|
|
||||||
|
[setting_1]
|
||||||
|
key="memory_update_interval_ms"
|
||||||
|
label="Memory Update Interval"
|
||||||
|
type="int"
|
||||||
|
default=1000
|
||||||
|
min=100
|
||||||
|
max=60000
|
||||||
|
|
@ -25,12 +25,14 @@ func refresh(data: Dictionary) -> void:
|
||||||
var pct: int = roundi(usage)
|
var pct: int = roundi(usage)
|
||||||
label.text = "%d%%" % pct
|
label.text = "%d%%" % pct
|
||||||
|
|
||||||
# Smoothly tween the vial fill
|
# Smoothly tween the vial fill — clamp duration so animation never outlasts the refresh interval
|
||||||
var target: float = usage / 100.0
|
var target: float = usage / 100.0
|
||||||
|
var update_ms: int = data.get("update_interval_ms", 1000)
|
||||||
|
var duration: float = minf(0.4, update_ms / 2000.0)
|
||||||
if _fill_tween and _fill_tween.is_valid():
|
if _fill_tween and _fill_tween.is_valid():
|
||||||
_fill_tween.kill()
|
_fill_tween.kill()
|
||||||
_fill_tween = create_tween()
|
_fill_tween = create_tween()
|
||||||
_fill_tween.tween_method(_set_fill, _displayed_fill, target, 0.4).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_CUBIC)
|
_fill_tween.tween_method(_set_fill, _displayed_fill, target, duration).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_CUBIC)
|
||||||
|
|
||||||
|
|
||||||
func _set_fill(value: float) -> void:
|
func _set_fill(value: float) -> void:
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[gd_scene format=3 uid="uid://bq3bs2hb4r7fb"]
|
[gd_scene format=3 uid="uid://bq3bs2hb4r7fb"]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://plugins/system_monitor/tiles/cpu/cpu_tile.gd" id="1"]
|
[ext_resource type="Script" path="res://plugins/local_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="CpuTile" type="PanelContainer"]
|
[node name="CpuTile" type="PanelContainer"]
|
||||||
|
|
@ -25,12 +25,14 @@ func refresh(data: Dictionary) -> void:
|
||||||
var pct: int = roundi(usage)
|
var pct: int = roundi(usage)
|
||||||
label.text = "%d%%" % pct
|
label.text = "%d%%" % pct
|
||||||
|
|
||||||
# Smoothly tween the vial fill
|
# Smoothly tween the vial fill — clamp duration so animation never outlasts the refresh interval
|
||||||
var target: float = usage / 100.0
|
var target: float = usage / 100.0
|
||||||
|
var update_ms: int = data.get("update_interval_ms", 1000)
|
||||||
|
var duration: float = minf(0.4, update_ms / 2000.0)
|
||||||
if _fill_tween and _fill_tween.is_valid():
|
if _fill_tween and _fill_tween.is_valid():
|
||||||
_fill_tween.kill()
|
_fill_tween.kill()
|
||||||
_fill_tween = create_tween()
|
_fill_tween = create_tween()
|
||||||
_fill_tween.tween_method(_set_fill, _displayed_fill, target, 0.4).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_CUBIC)
|
_fill_tween.tween_method(_set_fill, _displayed_fill, target, duration).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_CUBIC)
|
||||||
|
|
||||||
|
|
||||||
func _set_fill(value: float) -> void:
|
func _set_fill(value: float) -> void:
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[gd_scene format=3 uid="uid://d2d4uqrd2hh3d"]
|
[gd_scene format=3 uid="uid://d2d4uqrd2hh3d"]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://plugins/system_monitor/tiles/memory/memory_tile.gd" id="1"]
|
[ext_resource type="Script" path="res://plugins/local_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="MemoryTile" type="PanelContainer"]
|
[node name="MemoryTile" type="PanelContainer"]
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
[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
|
|
||||||
30
plugins/test/plugin.cfg
Normal file
30
plugins/test/plugin.cfg
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
[plugin]
|
||||||
|
name="Test"
|
||||||
|
version="0.1.0"
|
||||||
|
description="Development testing tiles and framework experiments"
|
||||||
|
author="Fifthdread"
|
||||||
|
|
||||||
|
[tiles]
|
||||||
|
count=1
|
||||||
|
|
||||||
|
[tile_0]
|
||||||
|
id="testing"
|
||||||
|
name="Testing"
|
||||||
|
scene="res://plugins/test/tiles/testing/testing_tile.tscn"
|
||||||
|
min_w=1
|
||||||
|
min_h=1
|
||||||
|
max_w=4
|
||||||
|
max_h=4
|
||||||
|
default_w=1
|
||||||
|
default_h=1
|
||||||
|
|
||||||
|
[settings]
|
||||||
|
count=1
|
||||||
|
|
||||||
|
[setting_0]
|
||||||
|
key="testing_update_interval_ms"
|
||||||
|
label="Testing Update Interval"
|
||||||
|
type="int"
|
||||||
|
default=1000
|
||||||
|
min=100
|
||||||
|
max=60000
|
||||||
|
|
@ -17,16 +17,19 @@ func initialize() -> void:
|
||||||
_style()
|
_style()
|
||||||
|
|
||||||
|
|
||||||
func refresh(_data: Dictionary) -> void:
|
func refresh(data: Dictionary) -> void:
|
||||||
_cycle = (_cycle + 1) % _levels.size()
|
_cycle = (_cycle + 1) % _levels.size()
|
||||||
var target: float = _levels[_cycle]
|
var target: float = _levels[_cycle]
|
||||||
|
|
||||||
label.text = "%d%%" % roundi(target * 100.0)
|
label.text = "%d%%" % roundi(target * 100.0)
|
||||||
|
|
||||||
|
# Clamp duration so animation never outlasts the refresh interval
|
||||||
|
var update_ms: int = data.get("update_interval_ms", 1000)
|
||||||
|
var duration: float = minf(0.6, update_ms / 1500.0)
|
||||||
if _fill_tween and _fill_tween.is_valid():
|
if _fill_tween and _fill_tween.is_valid():
|
||||||
_fill_tween.kill()
|
_fill_tween.kill()
|
||||||
_fill_tween = create_tween()
|
_fill_tween = create_tween()
|
||||||
_fill_tween.tween_method(_set_fill, _displayed_fill, target, 0.6).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_CUBIC)
|
_fill_tween.tween_method(_set_fill, _displayed_fill, target, duration).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_CUBIC)
|
||||||
|
|
||||||
|
|
||||||
func _set_fill(value: float) -> void:
|
func _set_fill(value: float) -> void:
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[gd_scene format=3]
|
[gd_scene format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://plugins/system_monitor/tiles/testing/testing_tile.gd" id="1"]
|
[ext_resource type="Script" path="res://plugins/test/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="TestingTile" type="PanelContainer"]
|
[node name="TestingTile" type="PanelContainer"]
|
||||||
|
|
@ -5,6 +5,7 @@ extends PanelContainer
|
||||||
@onready var grid: DashboardGrid = %DashboardGrid
|
@onready var grid: DashboardGrid = %DashboardGrid
|
||||||
|
|
||||||
var _tile_instances: Array[Control] = []
|
var _tile_instances: Array[Control] = []
|
||||||
|
var _last_refresh: Dictionary = {} # tile_instance -> last tick ms
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
|
@ -32,6 +33,13 @@ func _load_tiles_from_plugins() -> void:
|
||||||
if all_tile_defs.is_empty():
|
if all_tile_defs.is_empty():
|
||||||
return
|
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)
|
# Load saved layout (if any)
|
||||||
var layout_data: Dictionary = {}
|
var layout_data: Dictionary = {}
|
||||||
if LayoutManager.has_method("get_layout"):
|
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
|
# First pass: place tiles that have a saved layout position
|
||||||
for tid in layout_data:
|
for tid in layout_data:
|
||||||
var pos: Dictionary = layout_data[tid]
|
|
||||||
var def := _find_tile_def(all_tile_defs, tid)
|
var def := _find_tile_def(all_tile_defs, tid)
|
||||||
if def.is_empty():
|
if def.is_empty():
|
||||||
|
# Plugin/tile not installed — skip gracefully
|
||||||
continue
|
continue
|
||||||
var instance := PluginManager.instantiate_tile(tid)
|
var instance := PluginManager.instantiate_tile(tid)
|
||||||
if instance == null:
|
if instance == null:
|
||||||
continue
|
continue
|
||||||
|
var pos: Dictionary = layout_data[tid]
|
||||||
grid.place_module(instance, pos.col, pos.row, pos.w, pos.h)
|
grid.place_module(instance, pos.col, pos.row, pos.w, pos.h)
|
||||||
_tile_instances.append(instance)
|
_tile_instances.append(instance)
|
||||||
placed_ids.append(tid)
|
placed_ids.append(tid)
|
||||||
|
|
@ -79,36 +88,152 @@ func _load_tiles_from_plugins() -> void:
|
||||||
next_col = 0
|
next_col = 0
|
||||||
next_row = gs.y
|
next_row = gs.y
|
||||||
|
|
||||||
# Connect auto-save signals
|
# Connect auto-save signals and tile tracking
|
||||||
grid.module_placed.connect(_save_layout)
|
grid.module_placed.connect(_on_module_placed)
|
||||||
if grid.has_signal("module_resized"):
|
if grid.has_signal("module_resized"):
|
||||||
grid.module_resized.connect(_save_layout)
|
grid.module_resized.connect(_save_layout)
|
||||||
|
|
||||||
# Start refresh timer
|
# Listen for plugin activation changes
|
||||||
_start_refresh_timer()
|
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:
|
func _start_tick_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(_tick_refresh)
|
||||||
timer.autostart = true
|
timer.autostart = true
|
||||||
timer.wait_time = interval
|
timer.wait_time = 0.1
|
||||||
add_child(timer)
|
add_child(timer)
|
||||||
|
|
||||||
|
|
||||||
func _refresh_modules() -> void:
|
func _tick_refresh() -> void:
|
||||||
|
var now := Time.get_ticks_msec()
|
||||||
var data: Dictionary = {}
|
var data: Dictionary = {}
|
||||||
for mod in _tile_instances:
|
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)
|
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:
|
func _save_layout(_mod: Control = null, _a: int = 0, _b: int = 0) -> void:
|
||||||
if not LayoutManager.has_method("set_tile_position"):
|
if not LayoutManager.has_method("set_tile_position"):
|
||||||
return
|
return
|
||||||
|
|
|
||||||
195
scenes/plugin_settings_popup.gd
Normal file
195
scenes/plugin_settings_popup.gd
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
extends PopupPanel
|
||||||
|
|
||||||
|
## Dynamic settings popup for a specific plugin.
|
||||||
|
## Touch-friendly: large close button, exclusive modal, ample control sizes.
|
||||||
|
|
||||||
|
const CLOSE_BTN_SIZE: Vector2 = Vector2(56, 56)
|
||||||
|
const WINDOW_CORNER_RADIUS: float = 12.0
|
||||||
|
|
||||||
|
var _plugin_id: String = ""
|
||||||
|
var _controls: Dictionary = {} # setting_key -> Control
|
||||||
|
|
||||||
|
|
||||||
|
func setup(plugin_id: String) -> void:
|
||||||
|
_plugin_id = plugin_id
|
||||||
|
exclusive = true
|
||||||
|
_build_ui()
|
||||||
|
|
||||||
|
|
||||||
|
func _build_ui() -> void:
|
||||||
|
# Window background with rounded corners
|
||||||
|
var bg := StyleBoxFlat.new()
|
||||||
|
bg.bg_color = Color(0.12, 0.12, 0.16, 1.0)
|
||||||
|
bg.corner_radius_top_left = WINDOW_CORNER_RADIUS
|
||||||
|
bg.corner_radius_top_right = WINDOW_CORNER_RADIUS
|
||||||
|
bg.corner_radius_bottom_left = WINDOW_CORNER_RADIUS
|
||||||
|
bg.corner_radius_bottom_right = WINDOW_CORNER_RADIUS
|
||||||
|
add_theme_stylebox_override("panel", bg)
|
||||||
|
|
||||||
|
# Content area — leave right margin for the overlaid close button
|
||||||
|
var mc := MarginContainer.new()
|
||||||
|
mc.add_theme_constant_override("margin_left", 16)
|
||||||
|
mc.add_theme_constant_override("margin_right", 16 + CLOSE_BTN_SIZE.x)
|
||||||
|
mc.add_theme_constant_override("margin_top", 12)
|
||||||
|
mc.add_theme_constant_override("margin_bottom", 12)
|
||||||
|
add_child(mc)
|
||||||
|
|
||||||
|
var vbox := VBoxContainer.new()
|
||||||
|
vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
vbox.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
mc.add_child(vbox)
|
||||||
|
|
||||||
|
# Title bar
|
||||||
|
var title_bar := HBoxContainer.new()
|
||||||
|
var title_label := Label.new()
|
||||||
|
var info: Dictionary = PluginManager.get_plugin(_plugin_id)
|
||||||
|
title_label.text = "%s Settings" % info.get("name", _plugin_id)
|
||||||
|
title_label.add_theme_font_size_override("font_size", 20)
|
||||||
|
title_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
title_bar.add_child(title_label)
|
||||||
|
vbox.add_child(title_bar)
|
||||||
|
|
||||||
|
vbox.add_child(HSeparator.new())
|
||||||
|
|
||||||
|
# Setting rows
|
||||||
|
var settings: Array[Dictionary] = PluginManager.get_plugin_settings(_plugin_id)
|
||||||
|
if settings.is_empty():
|
||||||
|
var empty := Label.new()
|
||||||
|
empty.text = "No configurable settings for this plugin."
|
||||||
|
empty.modulate = Color(0.5, 0.5, 0.55)
|
||||||
|
empty.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
vbox.add_child(empty)
|
||||||
|
return
|
||||||
|
|
||||||
|
var inner := MarginContainer.new()
|
||||||
|
inner.add_theme_constant_override("margin_left", 4)
|
||||||
|
inner.add_theme_constant_override("margin_right", 4)
|
||||||
|
inner.add_theme_constant_override("margin_top", 10)
|
||||||
|
inner.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
vbox.add_child(inner)
|
||||||
|
|
||||||
|
var scroll := ScrollContainer.new()
|
||||||
|
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
inner.add_child(scroll)
|
||||||
|
|
||||||
|
var form := VBoxContainer.new()
|
||||||
|
form.add_theme_constant_override("separation", 14)
|
||||||
|
form.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
scroll.add_child(form)
|
||||||
|
|
||||||
|
for sd in settings:
|
||||||
|
var key: String = sd.get("key", "")
|
||||||
|
if key.is_empty():
|
||||||
|
continue
|
||||||
|
var label_text: String = sd.get("label", key)
|
||||||
|
var stype: String = sd.get("type", "string")
|
||||||
|
var default_val: Variant = sd.get("default", null)
|
||||||
|
var current_val: Variant = PluginManager.get_plugin_setting(_plugin_id, key, default_val)
|
||||||
|
|
||||||
|
var row := HBoxContainer.new()
|
||||||
|
row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
|
||||||
|
var lbl := Label.new()
|
||||||
|
lbl.text = label_text
|
||||||
|
lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
lbl.mouse_filter = Control.MOUSE_FILTER_PASS
|
||||||
|
row.add_child(lbl)
|
||||||
|
|
||||||
|
var ctrl := _build_control(stype, key, current_val, sd)
|
||||||
|
if ctrl != null:
|
||||||
|
ctrl.custom_minimum_size = Vector2(180, 40)
|
||||||
|
_controls[key] = ctrl
|
||||||
|
row.add_child(ctrl)
|
||||||
|
|
||||||
|
form.add_child(row)
|
||||||
|
|
||||||
|
# Red corner close button (overlaid, anchored top-right, flush to edge)
|
||||||
|
var close_button := Button.new()
|
||||||
|
close_button.text = "✕"
|
||||||
|
close_button.add_theme_font_size_override("font_size", 22)
|
||||||
|
close_button.flat = true
|
||||||
|
close_button.pressed.connect(_close)
|
||||||
|
close_button.custom_minimum_size = CLOSE_BTN_SIZE
|
||||||
|
|
||||||
|
var btn_style := StyleBoxFlat.new()
|
||||||
|
btn_style.bg_color = Color(1.0, 0.0, 0.0, 1.0)
|
||||||
|
btn_style.corner_radius_top_right = WINDOW_CORNER_RADIUS
|
||||||
|
btn_style.corner_radius_bottom_left = WINDOW_CORNER_RADIUS
|
||||||
|
btn_style.corner_radius_top_left = 0
|
||||||
|
btn_style.corner_radius_bottom_right = 0
|
||||||
|
btn_style.set_corner_radius(Corner.CORNER_TOP_RIGHT, WINDOW_CORNER_RADIUS)
|
||||||
|
btn_style.set_corner_radius(Corner.CORNER_BOTTOM_LEFT, WINDOW_CORNER_RADIUS)
|
||||||
|
close_button.add_theme_stylebox_override("normal", btn_style)
|
||||||
|
|
||||||
|
var hover_style := StyleBoxFlat.new()
|
||||||
|
hover_style.bg_color = Color(0.85, 0.0, 0.0, 1.0)
|
||||||
|
hover_style.corner_radius_top_right = WINDOW_CORNER_RADIUS
|
||||||
|
hover_style.corner_radius_bottom_left = WINDOW_CORNER_RADIUS
|
||||||
|
hover_style.corner_radius_top_left = 0
|
||||||
|
hover_style.corner_radius_bottom_right = 0
|
||||||
|
close_button.add_theme_stylebox_override("hover", hover_style)
|
||||||
|
|
||||||
|
var pressed_style := StyleBoxFlat.new()
|
||||||
|
pressed_style.bg_color = Color(0.7, 0.0, 0.0, 1.0)
|
||||||
|
pressed_style.corner_radius_top_right = WINDOW_CORNER_RADIUS
|
||||||
|
pressed_style.corner_radius_bottom_left = WINDOW_CORNER_RADIUS
|
||||||
|
pressed_style.corner_radius_top_left = 0
|
||||||
|
pressed_style.corner_radius_bottom_right = 0
|
||||||
|
close_button.add_theme_stylebox_override("pressed", pressed_style)
|
||||||
|
|
||||||
|
close_button.set_anchors_preset(Control.PRESET_TOP_RIGHT)
|
||||||
|
add_child(close_button)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_control(stype: String, key: String, val: Variant, defn: Dictionary) -> Control:
|
||||||
|
match stype:
|
||||||
|
"int":
|
||||||
|
var sb := SpinBox.new()
|
||||||
|
sb.min_value = defn.get("min", -999999)
|
||||||
|
sb.max_value = defn.get("max", 999999)
|
||||||
|
sb.step = 1
|
||||||
|
sb.value = int(val) if val != null else int(defn.get("default", 0))
|
||||||
|
sb.custom_minimum_size = Vector2(0, 40)
|
||||||
|
sb.value_changed.connect(_on_value_changed.bind(key))
|
||||||
|
return sb
|
||||||
|
|
||||||
|
"float":
|
||||||
|
var sb := SpinBox.new()
|
||||||
|
sb.min_value = defn.get("min", -999999.0)
|
||||||
|
sb.max_value = defn.get("max", 999999.0)
|
||||||
|
sb.step = defn.get("step", 0.1)
|
||||||
|
sb.value = float(val) if val != null else float(defn.get("default", 0.0))
|
||||||
|
sb.custom_minimum_size = Vector2(0, 40)
|
||||||
|
sb.value_changed.connect(_on_value_changed.bind(key))
|
||||||
|
return sb
|
||||||
|
|
||||||
|
"bool":
|
||||||
|
var cb := CheckBox.new()
|
||||||
|
cb.button_pressed = bool(val) if val != null else bool(defn.get("default", false))
|
||||||
|
cb.custom_minimum_size = Vector2(0, 40)
|
||||||
|
cb.toggled.connect(_on_toggled.bind(key))
|
||||||
|
return cb
|
||||||
|
|
||||||
|
_:
|
||||||
|
# string / default
|
||||||
|
var le := LineEdit.new()
|
||||||
|
le.text = str(val) if val != null else str(defn.get("default", ""))
|
||||||
|
le.custom_minimum_size = Vector2(0, 40)
|
||||||
|
le.text_changed.connect(_on_text_changed.bind(key))
|
||||||
|
return le
|
||||||
|
|
||||||
|
|
||||||
|
func _on_value_changed(value: float, key: String) -> void:
|
||||||
|
PluginManager.set_plugin_setting(_plugin_id, key, value)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_toggled(checked: bool, key: String) -> void:
|
||||||
|
PluginManager.set_plugin_setting(_plugin_id, key, checked)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_text_changed(text: String, key: String) -> void:
|
||||||
|
PluginManager.set_plugin_setting(_plugin_id, key, text)
|
||||||
|
|
||||||
|
|
||||||
|
func _close() -> void:
|
||||||
|
queue_free()
|
||||||
6
scenes/plugin_settings_popup.tscn
Normal file
6
scenes/plugin_settings_popup.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scenes/plugin_settings_popup.gd" id="1"]
|
||||||
|
|
||||||
|
[node name="PluginSettingsPopup" type="PopupPanel"]
|
||||||
|
script = ExtResource("1")
|
||||||
316
scenes/settings_menu.gd
Normal file
316
scenes/settings_menu.gd
Normal file
|
|
@ -0,0 +1,316 @@
|
||||||
|
extends PopupPanel
|
||||||
|
|
||||||
|
## Settings menu with tabbed interface: General, Plugins, About.
|
||||||
|
## Touch-friendly: large close button, ample spacing, exclusive modal.
|
||||||
|
|
||||||
|
const VERSION: String = "0.1.0-alpha"
|
||||||
|
const CONTACT_EMAIL: String = "5d@fifthdread.com"
|
||||||
|
|
||||||
|
const CLOSE_BTN_SIZE: Vector2 = Vector2(56, 56)
|
||||||
|
const WINDOW_CORNER_RADIUS: float = 12.0
|
||||||
|
|
||||||
|
var _tab_container: TabContainer
|
||||||
|
var _close_button: Button
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
exclusive = true
|
||||||
|
_build_ui()
|
||||||
|
_popup_centered_clamp(Vector2i(720, 540))
|
||||||
|
|
||||||
|
|
||||||
|
## Clamp popup to fit within screen, handling multi-monitor edge cases.
|
||||||
|
func _popup_centered_clamp(min_size: Vector2i) -> void:
|
||||||
|
popup_centered(min_size)
|
||||||
|
# Ensure the popup doesn't extend past screen edges
|
||||||
|
var screen_rect := DisplayServer.screen_get_usable_rect(
|
||||||
|
DisplayServer.window_get_current_screen()
|
||||||
|
)
|
||||||
|
var max_pos := Vector2(
|
||||||
|
screen_rect.position.x + screen_rect.size.x - size.x,
|
||||||
|
screen_rect.position.y + screen_rect.size.y - size.y
|
||||||
|
)
|
||||||
|
position = Vector2i(
|
||||||
|
mini(maxi(position.x, screen_rect.position.x), max_pos.x),
|
||||||
|
mini(maxi(position.y, screen_rect.position.y), max_pos.y)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_ui() -> void:
|
||||||
|
# Window background with rounded corners
|
||||||
|
var bg_style := StyleBoxFlat.new()
|
||||||
|
bg_style.bg_color = Color(0.12, 0.12, 0.16, 1.0)
|
||||||
|
bg_style.corner_radius_top_left = WINDOW_CORNER_RADIUS
|
||||||
|
bg_style.corner_radius_top_right = WINDOW_CORNER_RADIUS
|
||||||
|
bg_style.corner_radius_bottom_left = WINDOW_CORNER_RADIUS
|
||||||
|
bg_style.corner_radius_bottom_right = WINDOW_CORNER_RADIUS
|
||||||
|
add_theme_stylebox_override("panel", bg_style)
|
||||||
|
|
||||||
|
# Content area — leave right margin for the overlaid close button
|
||||||
|
var mc := MarginContainer.new()
|
||||||
|
mc.add_theme_constant_override("margin_left", 16)
|
||||||
|
mc.add_theme_constant_override("margin_right", 16 + CLOSE_BTN_SIZE.x)
|
||||||
|
mc.add_theme_constant_override("margin_top", 12)
|
||||||
|
mc.add_theme_constant_override("margin_bottom", 12)
|
||||||
|
add_child(mc)
|
||||||
|
|
||||||
|
var vbox := VBoxContainer.new()
|
||||||
|
vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
vbox.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
mc.add_child(vbox)
|
||||||
|
|
||||||
|
# Title bar (no close button — it's overlaid in the corner)
|
||||||
|
var title_bar := HBoxContainer.new()
|
||||||
|
var title_label := Label.new()
|
||||||
|
title_label.text = "Settings"
|
||||||
|
title_label.add_theme_font_size_override("font_size", 24)
|
||||||
|
title_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
title_bar.add_child(title_label)
|
||||||
|
vbox.add_child(title_bar)
|
||||||
|
|
||||||
|
# Tab container
|
||||||
|
_tab_container = TabContainer.new()
|
||||||
|
_tab_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
vbox.add_child(_tab_container)
|
||||||
|
|
||||||
|
# Build tabs
|
||||||
|
_build_general_tab()
|
||||||
|
_build_plugins_tab()
|
||||||
|
_build_about_tab()
|
||||||
|
|
||||||
|
# Red corner close button (overlaid, anchored top-right, flush to edge)
|
||||||
|
_close_button = Button.new()
|
||||||
|
_close_button.text = "✕"
|
||||||
|
_close_button.add_theme_font_size_override("font_size", 22)
|
||||||
|
_close_button.flat = true
|
||||||
|
_close_button.pressed.connect(_close)
|
||||||
|
_close_button.custom_minimum_size = CLOSE_BTN_SIZE
|
||||||
|
|
||||||
|
var btn_style := StyleBoxFlat.new()
|
||||||
|
btn_style.bg_color = Color(1.0, 0.0, 0.0, 1.0)
|
||||||
|
btn_style.corner_radius_top_right = WINDOW_CORNER_RADIUS
|
||||||
|
btn_style.corner_radius_bottom_left = WINDOW_CORNER_RADIUS
|
||||||
|
btn_style.corner_radius_top_left = 0
|
||||||
|
btn_style.corner_radius_bottom_right = 0
|
||||||
|
btn_style.set_corner_radius(Corner.CORNER_TOP_RIGHT, WINDOW_CORNER_RADIUS)
|
||||||
|
btn_style.set_corner_radius(Corner.CORNER_BOTTOM_LEFT, WINDOW_CORNER_RADIUS)
|
||||||
|
_close_button.add_theme_stylebox_override("normal", btn_style)
|
||||||
|
|
||||||
|
var hover_style := StyleBoxFlat.new()
|
||||||
|
hover_style.bg_color = Color(0.85, 0.0, 0.0, 1.0)
|
||||||
|
hover_style.corner_radius_top_right = WINDOW_CORNER_RADIUS
|
||||||
|
hover_style.corner_radius_bottom_left = WINDOW_CORNER_RADIUS
|
||||||
|
hover_style.corner_radius_top_left = 0
|
||||||
|
hover_style.corner_radius_bottom_right = 0
|
||||||
|
_close_button.add_theme_stylebox_override("hover", hover_style)
|
||||||
|
|
||||||
|
var pressed_style := StyleBoxFlat.new()
|
||||||
|
pressed_style.bg_color = Color(0.7, 0.0, 0.0, 1.0)
|
||||||
|
pressed_style.corner_radius_top_right = WINDOW_CORNER_RADIUS
|
||||||
|
pressed_style.corner_radius_bottom_left = WINDOW_CORNER_RADIUS
|
||||||
|
pressed_style.corner_radius_top_left = 0
|
||||||
|
pressed_style.corner_radius_bottom_right = 0
|
||||||
|
_close_button.add_theme_stylebox_override("pressed", pressed_style)
|
||||||
|
|
||||||
|
_close_button.set_anchors_preset(Control.PRESET_TOP_RIGHT)
|
||||||
|
add_child(_close_button)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_general_tab() -> void:
|
||||||
|
var tab := VBoxContainer.new()
|
||||||
|
tab.name = "General"
|
||||||
|
|
||||||
|
var inner := MarginContainer.new()
|
||||||
|
inner.add_theme_constant_override("margin_left", 4)
|
||||||
|
inner.add_theme_constant_override("margin_right", 4)
|
||||||
|
inner.add_theme_constant_override("margin_top", 12)
|
||||||
|
tab.add_child(inner)
|
||||||
|
|
||||||
|
var vbox := VBoxContainer.new()
|
||||||
|
vbox.add_theme_constant_override("separation", 14)
|
||||||
|
inner.add_child(vbox)
|
||||||
|
|
||||||
|
for i in range(1, 4):
|
||||||
|
var row := HBoxContainer.new()
|
||||||
|
row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
|
||||||
|
var label := Label.new()
|
||||||
|
label.text = "Setting %d:" % i
|
||||||
|
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
row.add_child(label)
|
||||||
|
|
||||||
|
var val := Label.new()
|
||||||
|
val.text = "[configure %d]" % i
|
||||||
|
val.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||||
|
val.modulate = Color(0.6, 0.6, 0.65)
|
||||||
|
val.custom_minimum_size = Vector2(200, 0)
|
||||||
|
row.add_child(val)
|
||||||
|
|
||||||
|
vbox.add_child(row)
|
||||||
|
|
||||||
|
_tab_container.add_child(tab)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_plugins_tab() -> void:
|
||||||
|
var tab := VBoxContainer.new()
|
||||||
|
tab.name = "Plugins"
|
||||||
|
|
||||||
|
var inner := MarginContainer.new()
|
||||||
|
inner.add_theme_constant_override("margin_left", 4)
|
||||||
|
inner.add_theme_constant_override("margin_right", 4)
|
||||||
|
inner.add_theme_constant_override("margin_top", 12)
|
||||||
|
tab.add_child(inner)
|
||||||
|
|
||||||
|
var vbox := VBoxContainer.new()
|
||||||
|
vbox.add_theme_constant_override("separation", 10)
|
||||||
|
inner.add_child(vbox)
|
||||||
|
|
||||||
|
# Header row
|
||||||
|
var header := HBoxContainer.new()
|
||||||
|
header.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
|
||||||
|
var hdr_name := Label.new()
|
||||||
|
hdr_name.text = "Plugin"
|
||||||
|
hdr_name.add_theme_font_size_override("font_size", 14)
|
||||||
|
hdr_name.modulate = Color(0.6, 0.6, 0.65)
|
||||||
|
hdr_name.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
header.add_child(hdr_name)
|
||||||
|
|
||||||
|
var hdr_ver := Label.new()
|
||||||
|
hdr_ver.text = "Version"
|
||||||
|
hdr_ver.add_theme_font_size_override("font_size", 14)
|
||||||
|
hdr_ver.modulate = Color(0.6, 0.6, 0.65)
|
||||||
|
hdr_ver.custom_minimum_size = Vector2(80, 0)
|
||||||
|
header.add_child(hdr_ver)
|
||||||
|
|
||||||
|
var hdr_active := Label.new()
|
||||||
|
hdr_active.text = "Active"
|
||||||
|
hdr_active.add_theme_font_size_override("font_size", 14)
|
||||||
|
hdr_active.modulate = Color(0.5, 0.5, 0.55)
|
||||||
|
hdr_active.custom_minimum_size = Vector2(60, 0)
|
||||||
|
hdr_active.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
header.add_child(hdr_active)
|
||||||
|
|
||||||
|
vbox.add_child(header)
|
||||||
|
vbox.add_child(HSeparator.new())
|
||||||
|
|
||||||
|
# Plugin rows
|
||||||
|
if not PluginManager.has_method("get_plugin_ids"):
|
||||||
|
_tab_container.add_child(tab)
|
||||||
|
return
|
||||||
|
|
||||||
|
var plugin_ids: PackedStringArray = PluginManager.get_plugin_ids()
|
||||||
|
if plugin_ids.is_empty():
|
||||||
|
var empty := Label.new()
|
||||||
|
empty.text = "No plugins loaded."
|
||||||
|
empty.modulate = Color(0.5, 0.5, 0.55)
|
||||||
|
empty.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
vbox.add_child(empty)
|
||||||
|
_tab_container.add_child(tab)
|
||||||
|
return
|
||||||
|
|
||||||
|
for pid in plugin_ids:
|
||||||
|
var info: Dictionary = PluginManager.get_plugin(pid)
|
||||||
|
var setting_defs := PluginManager.get_plugin_settings(pid) if PluginManager.has_method("get_plugin_settings") else []
|
||||||
|
var row := HBoxContainer.new()
|
||||||
|
row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
row.add_theme_constant_override("separation", 8)
|
||||||
|
|
||||||
|
var name_lbl := Label.new()
|
||||||
|
name_lbl.text = info.get("name", pid)
|
||||||
|
name_lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
row.add_child(name_lbl)
|
||||||
|
|
||||||
|
var ver_lbl := Label.new()
|
||||||
|
ver_lbl.text = "v%s" % info.get("version", "?")
|
||||||
|
ver_lbl.custom_minimum_size = Vector2(80, 0)
|
||||||
|
row.add_child(ver_lbl)
|
||||||
|
|
||||||
|
var cb := CheckBox.new()
|
||||||
|
cb.button_pressed = PluginManager.is_plugin_active(pid)
|
||||||
|
cb.toggled.connect(_on_plugin_toggled.bind(pid))
|
||||||
|
cb.custom_minimum_size = Vector2(60, 48)
|
||||||
|
cb.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||||
|
row.add_child(cb)
|
||||||
|
|
||||||
|
if not setting_defs.is_empty():
|
||||||
|
var cog := Button.new()
|
||||||
|
cog.text = "⚙"
|
||||||
|
cog.flat = true
|
||||||
|
cog.tooltip_text = "Settings for %s" % info.get("name", pid)
|
||||||
|
cog.custom_minimum_size = Vector2(44, 44)
|
||||||
|
cog.add_theme_font_size_override("font_size", 20)
|
||||||
|
cog.pressed.connect(_on_plugin_settings_pressed.bind(pid))
|
||||||
|
row.add_child(cog)
|
||||||
|
|
||||||
|
vbox.add_child(row)
|
||||||
|
|
||||||
|
_tab_container.add_child(tab)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_plugin_toggled(checked: bool, plugin_id: String) -> void:
|
||||||
|
if PluginManager.has_method("set_plugin_active"):
|
||||||
|
PluginManager.set_plugin_active(plugin_id, checked)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_plugin_settings_pressed(plugin_id: String) -> void:
|
||||||
|
var popup := preload("res://scenes/plugin_settings_popup.tscn").instantiate()
|
||||||
|
popup.setup(plugin_id)
|
||||||
|
add_sibling(popup)
|
||||||
|
popup.popup_centered(Vector2i(720, 540))
|
||||||
|
|
||||||
|
|
||||||
|
func _build_about_tab() -> void:
|
||||||
|
var tab := VBoxContainer.new()
|
||||||
|
tab.name = "About"
|
||||||
|
|
||||||
|
var center := VBoxContainer.new()
|
||||||
|
center.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
|
center.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
center.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
tab.add_child(center)
|
||||||
|
|
||||||
|
# Logo
|
||||||
|
var tex := load("res://assets/icons/icon.svg")
|
||||||
|
if tex != null:
|
||||||
|
var logo := TextureRect.new()
|
||||||
|
logo.texture = tex
|
||||||
|
logo.custom_minimum_size = Vector2(80, 80)
|
||||||
|
logo.expand_mode = TextureRect.EXPAND_FIT_WIDTH
|
||||||
|
logo.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||||
|
logo.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||||
|
center.add_child(logo)
|
||||||
|
|
||||||
|
center.add_spacer(false)
|
||||||
|
|
||||||
|
var name_lbl := Label.new()
|
||||||
|
name_lbl.text = "V Panel"
|
||||||
|
name_lbl.add_theme_font_size_override("font_size", 28)
|
||||||
|
name_lbl.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||||
|
center.add_child(name_lbl)
|
||||||
|
|
||||||
|
center.add_spacer(false)
|
||||||
|
|
||||||
|
var ver_lbl := Label.new()
|
||||||
|
ver_lbl.text = "Version %s" % VERSION
|
||||||
|
ver_lbl.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||||
|
center.add_child(ver_lbl)
|
||||||
|
|
||||||
|
var contact_lbl := Label.new()
|
||||||
|
contact_lbl.text = "Contact: %s" % CONTACT_EMAIL
|
||||||
|
contact_lbl.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||||
|
center.add_child(contact_lbl)
|
||||||
|
|
||||||
|
center.add_spacer(false)
|
||||||
|
|
||||||
|
var desc_lbl := Label.new()
|
||||||
|
desc_lbl.text = "A real-time system status monitor built with Godot 4"
|
||||||
|
desc_lbl.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||||
|
desc_lbl.modulate = Color(0.6, 0.6, 0.65)
|
||||||
|
center.add_child(desc_lbl)
|
||||||
|
|
||||||
|
_tab_container.add_child(tab)
|
||||||
|
|
||||||
|
|
||||||
|
func _close() -> void:
|
||||||
|
queue_free()
|
||||||
6
scenes/settings_menu.tscn
Normal file
6
scenes/settings_menu.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scenes/settings_menu.gd" id="1"]
|
||||||
|
|
||||||
|
[node name="SettingsMenu" type="PopupPanel"]
|
||||||
|
script = ExtResource("1")
|
||||||
|
|
@ -7,10 +7,12 @@ 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)
|
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 cell_spacing: float = 8.0
|
||||||
@export var margin: float = 16.0
|
@export var margin: float = 16.0
|
||||||
|
|
||||||
|
## Internal grid dimensions (synced to grid_columns / grid_rows on rebuild).
|
||||||
var columns: int = 0
|
var columns: int = 0
|
||||||
var rows: 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_w: float = 1.0
|
||||||
var _cell_h: 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)
|
# Resize edge detection threshold (pixels)
|
||||||
const RESIZE_THRESHOLD: float = 10.0
|
const RESIZE_THRESHOLD: float = 10.0
|
||||||
const EDGE_LEFT: int = 1
|
const EDGE_LEFT: int = 1
|
||||||
|
|
@ -161,6 +167,7 @@ func _show_tile_menu(mouse_pos: Vector2) -> void:
|
||||||
vbox.add_child(toolbar)
|
vbox.add_child(toolbar)
|
||||||
|
|
||||||
var settings_btn := _make_menu_button("⚙", "Settings")
|
var settings_btn := _make_menu_button("⚙", "Settings")
|
||||||
|
settings_btn.pressed.connect(_open_settings.bind(panel))
|
||||||
toolbar.add_child(settings_btn)
|
toolbar.add_child(settings_btn)
|
||||||
|
|
||||||
var info_btn := _make_menu_button("ℹ", "Info")
|
var info_btn := _make_menu_button("ℹ", "Info")
|
||||||
|
|
@ -269,19 +276,36 @@ func _add_tile_from_def(def: Dictionary) -> void:
|
||||||
if instance == null:
|
if instance == null:
|
||||||
return
|
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 dw: int = def.get("default_w", 1)
|
||||||
var dh: int = def.get("default_h", 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:
|
func _ensure_occupancy_for_span(col: int, row: int, w: int, h: int) -> void:
|
||||||
var needed_rows := row + h
|
var needed_rows := mini(row + h, grid_rows)
|
||||||
var needed_cols := col + w
|
var needed_cols := mini(col + w, grid_columns)
|
||||||
while _grid.size() < needed_rows:
|
while _grid.size() < needed_rows:
|
||||||
_grid.append([])
|
_grid.append([])
|
||||||
for r in range(_grid.size()):
|
for r in range(_grid.size()):
|
||||||
while _grid[r].size() < needed_cols:
|
while _grid[r].size() < needed_cols:
|
||||||
_grid[r].append(null)
|
_grid[r].append(null)
|
||||||
if columns < needed_cols:
|
# Never expand beyond the configured fixed grid
|
||||||
columns = needed_cols
|
columns = grid_columns
|
||||||
if rows < needed_rows:
|
rows = grid_rows
|
||||||
rows = needed_rows
|
|
||||||
|
|
||||||
|
|
||||||
func _is_valid_cell(col: int, row: int) -> bool:
|
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:
|
if columns == 0 or rows == 0 or _cell_w <= 0 or _cell_h <= 0:
|
||||||
return Vector2i(-1, -1)
|
return Vector2i(-1, -1)
|
||||||
|
|
||||||
var inner_x := margin + cell_spacing
|
var col := int((pos.x - _base_x) / (_cell_w + cell_spacing))
|
||||||
var inner_y := margin + cell_spacing
|
var row := int((pos.y - _base_y) / (_cell_h + cell_spacing))
|
||||||
var col := int((pos.x - inner_x) / (_cell_w + cell_spacing))
|
|
||||||
var row := int((pos.y - inner_y) / (_cell_h + cell_spacing))
|
|
||||||
col = clampi(col, 0, columns - 1)
|
col = clampi(col, 0, columns - 1)
|
||||||
row = clampi(row, 0, rows - 1)
|
row = clampi(row, 0, rows - 1)
|
||||||
return Vector2i(col, row)
|
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:
|
func _get_module_rect(col: int, row: int, w: int, h: int) -> Rect2:
|
||||||
var x := margin + cell_spacing + col * (_cell_w + cell_spacing)
|
var x := _base_x + col * (_cell_w + cell_spacing)
|
||||||
var y := margin + cell_spacing + row * (_cell_h + cell_spacing)
|
var y := _base_y + row * (_cell_h + cell_spacing)
|
||||||
var mw := w * _cell_w + (w - 1) * cell_spacing
|
var mw := w * _cell_w + (w - 1) * cell_spacing
|
||||||
var mh := h * _cell_h + (h - 1) * cell_spacing
|
var mh := h * _cell_h + (h - 1) * cell_spacing
|
||||||
return Rect2(x, y, mw, mh)
|
return Rect2(x, y, mw, mh)
|
||||||
|
|
@ -805,48 +826,16 @@ func _rebuild_grid() -> void:
|
||||||
if child is PopupMenu or child is PopupPanel:
|
if child is PopupMenu or child is PopupPanel:
|
||||||
child.queue_free()
|
child.queue_free()
|
||||||
|
|
||||||
var avail := size - Vector2(margin * 2.0, margin * 2.0)
|
# If grid dimensions changed, rebuild cells and re-place modules
|
||||||
|
if columns != grid_columns or rows != grid_rows:
|
||||||
var new_cols: int
|
columns = grid_columns
|
||||||
var new_rows: int
|
rows = grid_rows
|
||||||
|
|
||||||
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
|
|
||||||
_save_modules()
|
_save_modules()
|
||||||
_teardown_cells()
|
_teardown_cells()
|
||||||
_build_cells()
|
_build_cells()
|
||||||
_restore_modules()
|
_restore_modules()
|
||||||
|
else:
|
||||||
_layout_cells()
|
_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:
|
func _cancel_drag() -> void:
|
||||||
|
|
@ -955,12 +944,23 @@ func _layout_cells() -> void:
|
||||||
return
|
return
|
||||||
_ensure_cells_match_grid()
|
_ensure_cells_match_grid()
|
||||||
|
|
||||||
var inner_w := maxf(0.0, size.x - margin * 2.0 - cell_spacing)
|
var gap_x := cell_spacing * maxi(0, columns - 1)
|
||||||
var inner_h := maxf(0.0, size.y - margin * 2.0 - cell_spacing)
|
var gap_y := cell_spacing * maxi(0, rows - 1)
|
||||||
var gap_x := cell_spacing * (columns - 1)
|
|
||||||
var gap_y := cell_spacing * (rows - 1)
|
# Compute square cell size that fits within the available area
|
||||||
_cell_w = maxf(1.0, (inner_w - gap_x) / columns)
|
var avail_x := maxf(0.0, size.x - margin * 2.0)
|
||||||
_cell_h = maxf(1.0, (inner_h - gap_y) / rows)
|
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
|
# Position cells
|
||||||
for row in range(rows):
|
for row in range(rows):
|
||||||
|
|
@ -970,8 +970,8 @@ func _layout_cells() -> void:
|
||||||
var cell: PanelContainer = _cells[row][col]
|
var cell: PanelContainer = _cells[row][col]
|
||||||
if not is_instance_valid(cell):
|
if not is_instance_valid(cell):
|
||||||
continue
|
continue
|
||||||
var x := margin + cell_spacing + col * (_cell_w + cell_spacing)
|
var x := _base_x + col * (_cell_w + cell_spacing)
|
||||||
var y := margin + cell_spacing + row * (_cell_h + cell_spacing)
|
var y := _base_y + row * (_cell_h + cell_spacing)
|
||||||
cell.set_position(Vector2(x, y))
|
cell.set_position(Vector2(x, y))
|
||||||
cell.set_size(Vector2(_cell_w, _cell_h))
|
cell.set_size(Vector2(_cell_w, _cell_h))
|
||||||
|
|
||||||
|
|
@ -1057,31 +1057,39 @@ func _ensure_occupancy_from_grid() -> void:
|
||||||
if _grid[r][c] != null:
|
if _grid[r][c] != null:
|
||||||
max_c = maxi(max_c, c + 1)
|
max_c = maxi(max_c, c + 1)
|
||||||
max_r = maxi(max_r, r + 1)
|
max_r = maxi(max_r, r + 1)
|
||||||
columns = maxi(columns, max_c)
|
# Clamp to fixed grid — never expand beyond configured size
|
||||||
rows = maxi(rows, max_r)
|
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:
|
func _find_and_place(module: Control) -> void:
|
||||||
var d := _get_module_grid_data(module)
|
var d := _get_module_grid_data(module)
|
||||||
# Try the desired position first
|
# Clamp span to grid bounds
|
||||||
if _span_fits(d.col, d.row, d.w, d.h, null):
|
var sw := mini(d.w, columns)
|
||||||
_ensure_occupancy_for_span(d.col, d.row, d.w, d.h)
|
var sh := mini(d.h, rows)
|
||||||
_occupy_cells(module, d.col, d.row, d.w, d.h)
|
|
||||||
|
# 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
|
return
|
||||||
|
|
||||||
# Find first available position
|
# Find first available position within bounds
|
||||||
for r in range(rows):
|
for r in range(rows):
|
||||||
for c in range(columns):
|
for c in range(columns):
|
||||||
if _span_fits(c, r, d.w, d.h, null):
|
var fit_sw := mini(sw, columns - c)
|
||||||
_set_module_grid_data(module, c, r, d.w, d.h)
|
var fit_sh := mini(sh, rows - r)
|
||||||
_ensure_occupancy_for_span(c, r, d.w, d.h)
|
if _span_fits(c, r, fit_sw, fit_sh, null):
|
||||||
_occupy_cells(module, c, r, d.w, d.h)
|
_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
|
return
|
||||||
|
|
||||||
# No room — expand grid
|
# No room — place at (0,0), nudging whatever is there
|
||||||
var new_col := columns
|
_clear_occupancy(0, 0, sw, sh)
|
||||||
var new_row := 0
|
_set_module_grid_data(module, 0, 0, sw, sh)
|
||||||
_ensure_occupancy_for_span(new_col, new_row, d.w, d.h)
|
_ensure_occupancy_for_span(0, 0, sw, sh)
|
||||||
columns = new_col + d.w
|
_occupy_cells(module, 0, 0, sw, sh)
|
||||||
_set_module_grid_data(module, new_col, new_row, d.w, d.h)
|
|
||||||
_occupy_cells(module, new_col, new_row, d.w, d.h)
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue