2026-05-21 08:39:15 -04:00
|
|
|
extends Node
|
|
|
|
|
## 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/"
|
|
|
|
|
|
2026-05-21 08:58:04 -04:00
|
|
|
## Current layout name (e.g. "Main").
|
|
|
|
|
var current_layout_name: String = "Main"
|
2026-05-21 08:39:15 -04:00
|
|
|
|
|
|
|
|
## 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):
|
2026-05-21 08:58:04 -04:00
|
|
|
current_layout_name = "Main"
|
2026-05-21 08:39:15 -04:00
|
|
|
_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)
|