using System; using System.Collections.Generic; using Cthangover.Core.Mods; using Cthangover.Core.UI; using Cthangover.Core.UI.Theming; using Cthangover.Core.Scenes; using Cthangover.Core.Utils; using Godot; using Cthangover.Mods.Interface.Base.Lists; using Cthangover.Mods.Interface.Theming; namespace Mods.ModManager { /// /// Single entry point of the mod UI (the manifest main_menu button /// opens this hub instead of two separate screens). A slim header — back /// button, title and a two-position segment control "Mods | Collections" — /// hosts the child screens: the embedded /// (default tab) and, on the collections tab, the /// plus the scheme editor /// as its second level. The selected /// tab lives in a static field, so it survives closing and reopening the /// hub within one game session. /// /// The hub also carries the mockup's cross links: a collection link on a /// mod card switches to the collections tab, a "not found" problem in the /// editor jumps back to the mods tab with the id filled into the search /// box (). The back button and Escape /// walk back one level at a time — editor → gallery, an open mod card → the /// mods grid — and only then leave to the menu. /// public partial class ModHubScreen : ModWidget { private const string TabMods = "mods"; private const string TabCollections = "collections"; private const string ModsScene = "scenes/mod_manager_screen.tscn"; private const string EditorScene = "scenes/mod_collections_screen.tscn"; private const string HelpScene = "scenes/components/mod_help_panel.tscn"; /// Active tab, remembered for the whole game session (mockup: "state is remembered"). private static string _activeTab = TabMods; private Button _back; private Label _title; private HBoxContainer _segment; private Button _tabMods; private Button _tabCollections; private LineEdit _gallerySearch; private Button _galleryRefresh; private PanelContainer _bannerPanel; private Label _bannerLabel; private Control _body; private ModManagerScreen _mods; private CollectionsGallery _gallery; private ModCollectionsScreen _editor; private ModHelpPanel _help; private Button _helpButton; private bool _editorOpen; /// Mods installed this session, shared by the gallery and the editor so both stay honest before a restart. private readonly HashSet _sessionInstalled = new(StringComparer.OrdinalIgnoreCase); protected override void Construct() { MouseFilter = MouseFilterEnum.Stop; CthangoverTheme.Apply(this); BuildHeader(); BuildBanner(); BuildBody(); BuildHelp(); ShowTab(_activeTab); } /* ═══════════════ Header and body ═══════════════ */ private void BuildHeader() { var root = GetNode("Margin/Root"); root.AddThemeConstantOverride("separation", UiTokens.Space3); var header = UiKit.Row(UiTokens.Space3); root.AddChild(header); _back = UiKit.Button(TranslationServer.Translate("mod_manager/hub_back_to_menu")); _back.Pressed += OnBackPressed; header.AddChild(_back); _title = UiKit.Label(TranslationServer.Translate("mod_manager/menu_title"), UiTokens.FontLead, UiTokens.Ink); _title.Uppercase = true; header.AddChild(_title); _segment = UiKit.Row(0); var segmentPanel = new PanelContainer { MouseFilter = Control.MouseFilterEnum.Ignore }; segmentPanel.AddThemeStyleboxOverride("panel", UiKit.Frame(UiTokens.RadiusSm, UiTokens.Well, UiTokens.Line)); segmentPanel.AddChild(_segment); header.AddChild(segmentPanel); var group = new ButtonGroup(); _tabMods = UiKit.Button(TranslationServer.Translate("mod_manager/hub_tab_mods"), ButtonKind.Tab); _tabMods.ToggleMode = true; _tabMods.ButtonGroup = group; _tabMods.Pressed += () => ShowTab(TabMods); ModHelpPanel.Attach(_tabMods, "mods_tab"); _segment.AddChild(_tabMods); _tabCollections = UiKit.Button(TranslationServer.Translate("mod_manager/hub_tab_collections"), ButtonKind.Tab); _tabCollections.ToggleMode = true; _tabCollections.ButtonGroup = group; _tabCollections.Pressed += () => ShowTab(TabCollections); ModHelpPanel.Attach(_tabCollections, "collections_tab"); _segment.AddChild(_tabCollections); header.AddChild(UiKit.Spacer()); _gallerySearch = new LineEdit { PlaceholderText = TranslationServer.Translate("mod_manager/gallery_search_placeholder"), CustomMinimumSize = new Vector2(220, 0), SizeFlagsVertical = Control.SizeFlags.ShrinkCenter, }; _gallerySearch.TextChanged += OnGallerySearchChanged; ModHelpPanel.Attach(_gallerySearch, "collection_search"); header.AddChild(_gallerySearch); _galleryRefresh = UiKit.Button(TranslationServer.Translate("mod_manager/refresh")); _galleryRefresh.Pressed += OnGalleryRefreshPressed; ModHelpPanel.Attach(_galleryRefresh, "refresh"); header.AddChild(_galleryRefresh); _helpButton = UiKit.IconButton("?", TranslationServer.Translate("mod_manager/help_open")); _helpButton.Name = "HelpButton"; _helpButton.ToggleMode = true; _helpButton.Pressed += OnHelpPressed; header.AddChild(_helpButton); } /// Builds the non-modal inspection layer above every hub level. private void BuildHelp() { _help = TscnScenes.LoadAndBuild(HelpScene) as ModHelpPanel; if (_help == null) { GameLogger.Log("MOD_HUB", $"Failed to build '{HelpScene}'", LogLevel.Error); return; } AddChild(_help); _help.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect); _help.EnsureConstructed(); _help.Visible = false; } private void OnHelpPressed() { if (_help == null) return; if (_help.Visible) _help.Close(); else _help.Open(); _helpButton.ButtonPressed = _help.Visible; } private void BuildBanner() { var root = GetNode("Margin/Root"); _bannerPanel = new PanelContainer { Visible = false }; _bannerPanel.AddThemeStyleboxOverride("panel", ModManagerTheme.MakeBox( UiTokens.Alpha(UiTokens.Warning, 0.15f), UiTokens.Alpha(UiTokens.Warning, 0.45f), UiTokens.RadiusMd, UiTokens.Space4, UiTokens.Space2)); _bannerLabel = UiKit.Label(string.Empty, UiTokens.FontBodySmall, UiTokens.Warning); _bannerLabel.HorizontalAlignment = HorizontalAlignment.Center; _bannerPanel.AddChild(_bannerLabel); root.AddChild(_bannerPanel); } private void BuildBody() { _body = new Control { SizeFlagsVertical = Control.SizeFlags.ExpandFill, SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, }; GetNode("Margin/Root").AddChild(_body); } /* ═══════════════ Tabs and levels ═══════════════ */ private void ShowTab(string tab) { _activeTab = tab; _editorOpen = false; if (tab == TabMods) { EnsureMods(); _mods.Visible = true; if (_gallery != null) _gallery.Visible = false; if (_editor != null) _editor.Visible = false; } else { EnsureGallery(); _gallery.Visible = true; if (_mods != null) _mods.Visible = false; if (_editor != null) _editor.Visible = false; } UpdateHeader(); } /// Leaves the editor back to the games gallery (crumbs, back button, Escape). private void ShowGallery() { _editorOpen = false; if (_editor != null) _editor.Visible = false; EnsureGallery(); _gallery.Visible = true; UpdateHeader(); } /// /// Deep link used by the main menu's game picker ("Configure"): shows /// the collections tab with the scheme editor open on the game. /// public void OpenCollectionEditor(string collectionId) => OpenEditor(collectionId, false); /// Opens the editor on the second level of the collections tab. private void OpenEditor(string collectionId, bool newGame) { EnsureGallery(); EnsureEditor(); _gallery.Visible = false; if (_mods != null) _mods.Visible = false; _editor.Visible = true; _editorOpen = true; if (newGame) _editor.OpenNewGame(); else _editor.OpenCollection(collectionId); UpdateHeader(); } private void UpdateHeader() { if (_editorOpen && _editor != null) { _back.Text = TranslationServer.Translate("mod_manager/hub_back_to_gallery"); _title.Text = _editor.CurrentCollectionTitle ?? TranslationServer.Translate("mod_manager/collections_title"); _segment.Visible = false; _gallerySearch.Visible = false; _galleryRefresh.Visible = false; return; } _back.Text = TranslationServer.Translate(ModOpen ? "mod_manager/hub_back_to_mods" : "mod_manager/hub_back_to_menu"); _title.Text = TranslationServer.Translate("mod_manager/menu_title"); _segment.Visible = true; _tabMods.ButtonPressed = _activeTab == TabMods; _tabCollections.ButtonPressed = _activeTab == TabCollections; var collections = _activeTab == TabCollections; _gallerySearch.Visible = collections; _galleryRefresh.Visible = collections; } /// Back walks one level up: scheme editor → games gallery, mod card → mods, then the menu. private void OnBackPressed() { if (_editorOpen) ShowGallery(); else if (ModOpen) _mods.CloseDetail(); else QueueFree(); } /* ═══════════════ Hosted screens ═══════════════ */ private void EnsureMods() { if (_mods != null) return; _mods = TscnScenes.LoadAndBuild(ModsScene) as ModManagerScreen; if (_mods == null) { GameLogger.Log("MOD_HUB", $"Failed to build '{ModsScene}'", LogLevel.Error); return; } _mods.Embedded = true; _body.AddChild(_mods); _mods.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect); _mods.EnsureConstructed(); _mods.CollectionsLinkRequested += () => ShowTab(TabCollections); _mods.DetailToggled += UpdateHeader; } private bool ModOpen => _activeTab == TabMods && _mods != null && _mods.IsDetailOpen; private void EnsureGallery() { if (_gallery != null) return; _gallery = new CollectionsGallery(); _gallery.SetSessionInstalled(_sessionInstalled); _gallery.StatusMessage += ShowBanner; _gallery.EditSchemeRequested += id => OpenEditor(id, newGame: false); _gallery.NewGameRequested += () => OpenEditor(null, newGame: true); _body.AddChild(_gallery); _gallery.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect); _gallery.Refresh(); } private void EnsureEditor() { if (_editor != null) return; _editor = TscnScenes.LoadAndBuild(EditorScene) as ModCollectionsScreen; if (_editor == null) { GameLogger.Log("MOD_HUB", $"Failed to build '{EditorScene}'", LogLevel.Error); return; } _editor.Embedded = true; _editor.SetSessionInstalled(_sessionInstalled); _editor.GalleryRequested += ShowGallery; _editor.OpenManagerRequested += OnOpenManagerRequested; _editor.ViewChanged += UpdateHeader; _body.AddChild(_editor); _editor.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect); _editor.EnsureConstructed(); } private void OnOpenManagerRequested(string searchText) { ShowTab(TabMods); _mods?.SetSearch(searchText); } /* ═══════════════ Gallery header controls ═══════════════ */ private void OnGallerySearchChanged(string text) { _gallery?.ApplyFilter(text); } private async void OnGalleryRefreshPressed() { if (_galleryRefresh.Disabled) return; _galleryRefresh.Disabled = true; ShowBanner(TranslationServer.Translate("mod_manager/loading")); try { await CatalogService.RefreshAsync(forceRefresh: true); } catch (Exception ex) { GameLogger.Log("MOD_HUB", $"Catalog refresh failed: {ex.Message}", LogLevel.Warning); } if (!GodotObject.IsInstanceValid(this)) return; _galleryRefresh.Disabled = false; ShowBanner(null); } private void ShowBanner(string text) { if (!GodotObject.IsInstanceValid(_bannerPanel)) return; _bannerPanel.Visible = !string.IsNullOrEmpty(text); if (!string.IsNullOrEmpty(text)) _bannerLabel.Text = text; } /* ═══════════════ Escape chain ═══════════════ */ /// F1 explains the focused control; Escape leaves help before walking back a level. public override void _UnhandledInput(InputEvent @event) { if (@event is InputEventKey key && key.Pressed && !key.Echo && key.Keycode == Key.F1) { if (_help != null) { _help.Open(); _help.ShowFor(GetViewport().GuiGetFocusOwner()); _helpButton.ButtonPressed = true; } GetViewport().SetInputAsHandled(); return; } if (!@event.IsActionPressed("ui_cancel")) return; if (_help != null && _help.Visible) { _help.Close(); _helpButton.ButtonPressed = false; GetViewport().SetInputAsHandled(); return; } if (_editorOpen) { ShowGallery(); GetViewport().SetInputAsHandled(); } else if (ModOpen) { _mods.CloseDetail(); GetViewport().SetInputAsHandled(); } else if (_activeTab == TabCollections) { ShowTab(TabMods); GetViewport().SetInputAsHandled(); } } } }