@tool extends PanelContainer @onready var grid: DashboardGrid = %DashboardGrid var _tile_instances: Array[Control] = [] var _last_refresh: Dictionary = {} # tile_instance -> last tick ms func _ready() -> void: _set_background() if not Engine.is_editor_hint(): _load_tiles_from_plugins() func _set_background() -> void: var bg: StyleBoxFlat if ConfigManager.has_method("get_color"): bg = StyleBoxFlat.new() bg.bg_color = ConfigManager.get_color("background_color", Color(0.08, 0.08, 0.12)) else: bg = StyleBoxFlat.new() bg.bg_color = Color(0.08, 0.08, 0.12) add_theme_stylebox_override("panel", bg) func _load_tiles_from_plugins() -> void: if not PluginManager.has_method("get_all_tile_defs"): return var all_tile_defs: Array[Dictionary] = PluginManager.get_all_tile_defs() if all_tile_defs.is_empty(): 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) var layout_data: Dictionary = {} if LayoutManager.has_method("get_layout"): layout_data = LayoutManager.get_layout() # Track which tile_defs we've placed var placed_ids: Array[String] = [] # First pass: place tiles that have a saved layout position for tid in layout_data: var def := _find_tile_def(all_tile_defs, tid) if def.is_empty(): # Plugin/tile not installed — skip gracefully continue var instance := PluginManager.instantiate_tile(tid) if instance == null: continue var pos: Dictionary = layout_data[tid] grid.place_module(instance, pos.col, pos.row, pos.w, pos.h) _tile_instances.append(instance) placed_ids.append(tid) # Second pass: place any remaining tile_defs at default positions # Find the next free grid column var next_col: int = 0 var next_row: int = 0 if not placed_ids.is_empty(): var gs := grid.get_grid_size() next_col = gs.x next_row = 0 for def in all_tile_defs: if def.id in placed_ids: continue var instance := PluginManager.instantiate_tile(def.id) if instance == null: continue grid.place_module(instance, next_col, next_row, def.default_w, def.default_h) _tile_instances.append(instance) placed_ids.append(def.id) # Bump default insertion point next_col += def.default_w var gs := grid.get_grid_size() if next_col >= gs.x and gs.x > 0: next_col = 0 next_row = gs.y # Connect auto-save signals and tile tracking grid.module_placed.connect(_on_module_placed) if grid.has_signal("module_resized"): grid.module_resized.connect(_save_layout) # Listen for plugin activation changes 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_tick_timer() -> void: var timer := Timer.new() timer.timeout.connect(_tick_refresh) timer.autostart = true timer.wait_time = 0.1 add_child(timer) func _tick_refresh() -> void: var now := Time.get_ticks_msec() var data: Dictionary = {} for mod in _tile_instances: 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) 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: if not LayoutManager.has_method("set_tile_position"): return # Walk all modules in the grid and store their positions var gs := grid.get_grid_size() for r in range(gs.y): for c in range(gs.x): var mod := grid.get_module_at(c, r) if mod == null: continue var tid: String = mod.get_meta("tile_id", "") if tid.is_empty(): continue var d := _get_module_grid_data(mod) LayoutManager.set_tile_position(tid, d.col, d.row, d.w, d.h) LayoutManager.save_layout() ## Mirror of DashboardGrid._get_module_grid_data for layout saving. func _get_module_grid_data(module: Control) -> Dictionary: return { col = module.get_meta("grid_col", 0), row = module.get_meta("grid_row", 0), w = module.get_meta("grid_w", 1), h = module.get_meta("grid_h", 1), } ## Find a tile def by scoped tile_id. func _find_tile_def(defs: Array[Dictionary], tile_id: String) -> Dictionary: for d in defs: if d.get("id", "") == tile_id: return d return {}