Improve collection graph controls, version rules and help
This commit is contained in:
1 parent
b6c61ea3df
commit
7ae31f180c
24 files changed
+2201
-311
No files matched your search
+375
-42
@@ -11,6 +11,7 @@ using Godot;
|
||||
|
||||
using Cthangover.Mods.Interface.Base.Lists;
|
||||
using Cthangover.Mods.Interface.Theming;
|
||||
using Cthangover.Mods.Interface.Tool;
|
||||
namespace Mods.ModManager
|
||||
{
|
||||
/// <summary>
|
||||
@@ -39,7 +40,7 @@ namespace Mods.ModManager
|
||||
private Button _newGame;
|
||||
private Button _apply;
|
||||
private OptionButton _gameSelect;
|
||||
private CheckBox _edgesToggle;
|
||||
private OptionButton _edgesToggle;
|
||||
private Button _pillProblems;
|
||||
private Button _pillPlan;
|
||||
private Label _modsCount;
|
||||
@@ -86,6 +87,17 @@ namespace Mods.ModManager
|
||||
private Button _revert;
|
||||
private Button _delete;
|
||||
private Label _savesInfo;
|
||||
private Button _addMod;
|
||||
private LineEdit _canvasSearch;
|
||||
private Label _zoomLabel;
|
||||
private VBoxContainer _versionEditor;
|
||||
private OptionButton _versionPolicy;
|
||||
private LineEdit _versionValue;
|
||||
private Label _versionRule;
|
||||
private Label _versionFeedback;
|
||||
private ModCollectionModRef _versionReference;
|
||||
private bool _fitAfterBuild = true;
|
||||
private bool _fitQueued;
|
||||
|
||||
/// <summary>Drafts created by "Duplicate", mapped to the game they were copied from (offer to copy its saves).</summary>
|
||||
private readonly Dictionary<string, string> _duplicatedFrom = new(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -134,6 +146,7 @@ namespace Mods.ModManager
|
||||
public void OpenCollection(string collectionId)
|
||||
{
|
||||
_currentId = collectionId;
|
||||
_fitAfterBuild = true;
|
||||
_selectedGroupKey = null;
|
||||
_selectedModId = null;
|
||||
// The gallery re-reads the store on every refresh; re-read it here
|
||||
@@ -163,6 +176,254 @@ namespace Mods.ModManager
|
||||
UnsubscribeEvents();
|
||||
}
|
||||
|
||||
private void BindGraphiteControls()
|
||||
{
|
||||
var tools = "Margin/Root/Tools/";
|
||||
_addMod = GetNode<Button>(tools + "AddMod");
|
||||
_addMod.Text = TranslationServer.Translate("mod_manager/collection_add_mod");
|
||||
_addMod.Pressed += OnAddModPressed;
|
||||
_canvasSearch = GetNode<LineEdit>(tools + "Search");
|
||||
_canvasSearch.PlaceholderText = TranslationServer.Translate("mod_manager/collection_canvas_search");
|
||||
_canvasSearch.TextChanged += _canvas.SetSearch;
|
||||
_zoomLabel = GetNode<Label>(tools + "Zoom");
|
||||
GetNode<Button>(tools + "ZoomOut").Pressed += _canvas.ZoomOut;
|
||||
GetNode<Button>(tools + "ZoomIn").Pressed += _canvas.ZoomIn;
|
||||
var fit = GetNode<Button>(tools + "Fit");
|
||||
fit.Text = TranslationServer.Translate("mod_manager/collection_canvas_fit");
|
||||
fit.Pressed += _canvas.FitWidth;
|
||||
var arrange = GetNode<Button>(tools + "Arrange");
|
||||
arrange.Text = TranslationServer.Translate("mod_manager/collection_canvas_arrange");
|
||||
arrange.Pressed += _canvas.ArrangeGroups;
|
||||
var focus = GetNode<Button>(tools + "Focus");
|
||||
focus.Text = TranslationServer.Translate("mod_manager/collection_canvas_focus");
|
||||
focus.Pressed += _canvas.ShowSelection;
|
||||
_canvas.SetMiniMap(GetNode<CollectionMiniMap>("Margin/Root/Body/CanvasHost/MiniMap"));
|
||||
|
||||
var path = "Margin/Root/Body/Details/DetailScroll/Detail/VersionEditor";
|
||||
_versionEditor = GetNode<VBoxContainer>(path);
|
||||
_versionPolicy = GetNode<OptionButton>(path + "/Fields/Policy");
|
||||
_versionValue = GetNode<LineEdit>(path + "/Fields/Value");
|
||||
_versionRule = GetNode<Label>(path + "/Rule");
|
||||
_versionFeedback = GetNode<Label>(path + "/Feedback");
|
||||
GetNode<Label>(path + "/Caption").Text = TranslationServer.Translate("mod_manager/collection_version_caption");
|
||||
GetNode<Label>(path + "/Hint").Text = TranslationServer.Translate("mod_manager/collection_version_hint");
|
||||
var save = GetNode<Button>(path + "/SaveRule");
|
||||
save.Text = TranslationServer.Translate("mod_manager/collection_version_edit");
|
||||
save.Pressed += OnSaveVersionPressed;
|
||||
_versionPolicy.ItemSelected += OnVersionPolicySelected;
|
||||
_versionValue.TextChanged += OnVersionValueChanged;
|
||||
_versionRule.AddThemeColorOverride("font_color", UiTokens.Accent);
|
||||
_versionFeedback.AddThemeColorOverride("font_color", UiTokens.DangerText);
|
||||
var policies = new[]
|
||||
{
|
||||
"mod_manager/collection_version_any", "mod_manager/collection_version_minimum",
|
||||
"mod_manager/collection_version_exact", "mod_manager/collection_version_branch",
|
||||
"mod_manager/collection_version_maximum", "mod_manager/collection_version_greater",
|
||||
"mod_manager/collection_version_less",
|
||||
};
|
||||
var operators = new[] { "", ">=", "=", "^", "<=", ">", "<" };
|
||||
for (var i = 0; i < policies.Length; i++)
|
||||
{
|
||||
_versionPolicy.AddItem(TranslationServer.Translate(policies[i]));
|
||||
_versionPolicy.SetItemMetadata(i, operators[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGalleryPressed() => GalleryRequested?.Invoke();
|
||||
|
||||
private void OnBuildManagerPressed() => SelectTab("plan");
|
||||
|
||||
private void OnAddModPressed()
|
||||
{
|
||||
var group = _selectedGroupKey == null ? null : FindGroup(_selectedGroupKey);
|
||||
if (group != null)
|
||||
OpenModPicker(group);
|
||||
}
|
||||
|
||||
private void OnCanvasViewChanged()
|
||||
{
|
||||
if (_zoomLabel != null)
|
||||
_zoomLabel.Text = Mathf.RoundToInt(_canvas.Zoom * 100f) + "%";
|
||||
}
|
||||
|
||||
private void OnCanvasFirstFrame()
|
||||
{
|
||||
GetTree().ProcessFrame -= OnCanvasFirstFrame;
|
||||
_fitQueued = false;
|
||||
_fitAfterBuild = false;
|
||||
_canvas.FitWidth();
|
||||
while (_canvas.Zoom < 0.75f)
|
||||
_canvas.ZoomIn();
|
||||
_canvas.ShowSelection();
|
||||
}
|
||||
|
||||
private void BindVersionRule(ModCollectionGroup group, string modId)
|
||||
{
|
||||
foreach (var reference in group.Mods)
|
||||
{
|
||||
if (string.Equals(reference.ModId, modId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_versionReference = reference;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (_versionReference == null)
|
||||
return;
|
||||
|
||||
var owner = EffectiveCollection(_versionReference.CollectionId);
|
||||
foreach (var mechanic in owner?.Mechanics ?? new List<ModCollectionMechanic>())
|
||||
{
|
||||
if (!string.Equals(mechanic.Id, _versionReference.MechanicId, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
foreach (var raw in mechanic.Mods ?? new List<string>())
|
||||
{
|
||||
var requirement = ModDependency.Parse(raw);
|
||||
if (!requirement.IsValid || !string.Equals(requirement.ModId, modId, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
for (var i = 0; i < _versionPolicy.ItemCount; i++)
|
||||
{
|
||||
if (_versionPolicy.GetItemMetadata(i).AsString() == requirement.Operator)
|
||||
_versionPolicy.Selected = i;
|
||||
}
|
||||
_versionValue.Text = requirement.VersionRaw ?? string.Empty;
|
||||
if (requirement.VersionRaw == null
|
||||
&& ModRegistry.Instance.AllDiscoveredMods.TryGetValue(modId, out var installed))
|
||||
_versionValue.Text = installed.Version ?? string.Empty;
|
||||
_versionEditor.Visible = true;
|
||||
_versionEditor.GetNode<Label>("Caption").Text = TranslationServer.Translate("mod_manager/collection_version_caption")
|
||||
+ " · " + group.Name;
|
||||
OnVersionPolicySelected(_versionPolicy.Selected);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnVersionPolicySelected(long index)
|
||||
{
|
||||
var op = _versionPolicy.GetItemMetadata((int)index).AsString();
|
||||
_versionValue.Editable = op.Length > 0;
|
||||
OnVersionValueChanged(_versionValue.Text);
|
||||
}
|
||||
|
||||
private void OnVersionValueChanged(string value)
|
||||
{
|
||||
if (_versionReference == null || _versionPolicy.Selected < 0)
|
||||
return;
|
||||
var op = _versionPolicy.GetItemMetadata(_versionPolicy.Selected).AsString();
|
||||
_versionRule.Text = _versionReference.ModId + op + (op.Length > 0 ? value.Trim() : string.Empty);
|
||||
_versionFeedback.Text = op == "^"
|
||||
? TranslationServer.Translate("mod_manager/collection_version_branch_hint") : string.Empty;
|
||||
_versionFeedback.AddThemeColorOverride("font_color", UiTokens.Muted);
|
||||
foreach (var node in _graph?.Nodes ?? new List<ModCollectionNode>())
|
||||
{
|
||||
if (node.ModId != _versionReference.ModId || node.State != ModCollectionNodeState.VersionMismatch)
|
||||
continue;
|
||||
var requirements = new List<string>();
|
||||
foreach (var requirement in node.Requirements)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(requirement.Operator))
|
||||
requirements.Add(requirement.Operator + requirement.VersionRaw);
|
||||
}
|
||||
_versionFeedback.Text = string.Format(TranslationServer.Translate("mod_manager/installed_version"), node.InstalledVersion)
|
||||
+ "\n" + string.Format(TranslationServer.Translate("mod_manager/collection_requires"), string.Join(", ", requirements));
|
||||
_versionFeedback.AddThemeColorOverride("font_color", UiTokens.DangerText);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSaveVersionPressed()
|
||||
{
|
||||
if (_versionReference == null || _versionPolicy.Selected < 0)
|
||||
return;
|
||||
var group = FindGroup(_selectedGroupKey);
|
||||
if (group == null)
|
||||
return;
|
||||
var reference = _versionReference;
|
||||
var op = _versionPolicy.GetItemMetadata(_versionPolicy.Selected).AsString();
|
||||
var owner = EffectiveCollection(reference.CollectionId);
|
||||
var valid = false;
|
||||
var changed = false;
|
||||
foreach (var mechanic in owner?.Mechanics ?? new List<ModCollectionMechanic>())
|
||||
{
|
||||
if (!string.Equals(mechanic.Id, reference.MechanicId, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
var preview = new ModCollectionMechanic { Mods = new List<string>(mechanic.Mods ?? new List<string>()) };
|
||||
valid = preview.TrySetModVersion(reference.ModId, op, _versionValue.Text);
|
||||
for (var i = 0; i < preview.Mods.Count; i++)
|
||||
{
|
||||
if (preview.Mods[i] != mechanic.Mods[i])
|
||||
changed = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!valid)
|
||||
{
|
||||
_versionFeedback.Text = TranslationServer.Translate("mod_manager/collection_version_invalid");
|
||||
_versionFeedback.AddThemeColorOverride("font_color", UiTokens.DangerText);
|
||||
return;
|
||||
}
|
||||
if (!changed)
|
||||
return;
|
||||
var collection = DraftForEdit(group, reference.CollectionId);
|
||||
if (collection == null)
|
||||
return;
|
||||
foreach (var mechanic in collection.Mechanics ?? new List<ModCollectionMechanic>())
|
||||
{
|
||||
if (!string.Equals(mechanic.Id, reference.MechanicId, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
if (!mechanic.TrySetModVersion(reference.ModId, op, _versionValue.Text))
|
||||
{
|
||||
_versionFeedback.Text = TranslationServer.Translate("mod_manager/collection_version_invalid");
|
||||
_versionFeedback.AddThemeColorOverride("font_color", UiTokens.DangerText);
|
||||
return;
|
||||
}
|
||||
Rebuild();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddModRelationships(string modId)
|
||||
{
|
||||
AddInfoLine(TranslationServer.Translate("mod_manager/collection_role_hint"));
|
||||
foreach (var edge in _graph.Edges)
|
||||
{
|
||||
if (string.Equals(edge.FromModId, modId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var consumer = LookupModTitle(edge.ToModId);
|
||||
AddInfoLine(string.Format(TranslationServer.Translate("mod_manager/collection_used_by"), consumer));
|
||||
}
|
||||
}
|
||||
foreach (var collection in _collections)
|
||||
{
|
||||
if (string.Equals(collection.Id, _currentId, StringComparison.OrdinalIgnoreCase)
|
||||
|| collection.Kind != ModCollectionKind.Game)
|
||||
continue;
|
||||
var effective = EffectiveCollection(collection.Id);
|
||||
foreach (var mechanic in effective?.Mechanics ?? new List<ModCollectionMechanic>())
|
||||
{
|
||||
foreach (var raw in mechanic.Mods ?? new List<string>())
|
||||
{
|
||||
if (!string.Equals(ParseModId(raw), modId, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
AddInfoLine(string.Format(TranslationServer.Translate("mod_manager/collection_other_role"),
|
||||
CollectionText.Resolve(effective.GetLocalizedName(TranslationServer.GetLocale())),
|
||||
CollectionText.Resolve(mechanic.GetLocalizedName(TranslationServer.GetLocale()))));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string LookupModTitle(string modId)
|
||||
{
|
||||
foreach (var node in _graph.Nodes)
|
||||
{
|
||||
if (string.Equals(node.ModId, modId, StringComparison.OrdinalIgnoreCase))
|
||||
return node.Title;
|
||||
}
|
||||
return modId;
|
||||
}
|
||||
|
||||
private void BindSceneNodes()
|
||||
{
|
||||
_close = GetNode<Button>("Margin/Root/Header/Close");
|
||||
@@ -170,7 +431,7 @@ namespace Mods.ModManager
|
||||
_addMechanic = GetNode<Button>("Margin/Root/Header/AddMechanic");
|
||||
_newGame = GetNode<Button>("Margin/Root/Header/NewGame");
|
||||
_gameSelect = GetNode<OptionButton>("Margin/Root/Header/GameSelect");
|
||||
_edgesToggle = GetNode<CheckBox>("Margin/Root/Header/EdgesToggle");
|
||||
_edgesToggle = GetNode<OptionButton>("Margin/Root/Header/EdgesToggle");
|
||||
_pillProblems = GetNode<Button>("Margin/Root/Header/Problems");
|
||||
_pillPlan = GetNode<Button>("Margin/Root/Header/Plan");
|
||||
_modsCount = GetNode<Label>("Margin/Root/Header/ModsCount");
|
||||
@@ -180,7 +441,7 @@ namespace Mods.ModManager
|
||||
_save = GetNode<Button>("Margin/Root/Header/Save");
|
||||
_bannerPanel = GetNode<PanelContainer>("Margin/Root/Banner");
|
||||
_bannerLabel = GetNode<Label>("Margin/Root/Banner/BannerLabel");
|
||||
_canvas = GetNode<CollectionSchemeCanvas>("Margin/Root/Body/CanvasScroll/SchemeCanvas");
|
||||
_canvas = GetNode<CollectionSchemeCanvas>("Margin/Root/Body/CanvasHost/CanvasScroll/SchemeCanvas");
|
||||
|
||||
var detail = "Margin/Root/Body/Details/DetailScroll/Detail/";
|
||||
_detailTitle = GetNode<Label>(detail + "DetailTitle");
|
||||
@@ -199,10 +460,6 @@ namespace Mods.ModManager
|
||||
_newGame.Text = TranslationServer.Translate("mod_manager/collection_new_game");
|
||||
_pillProblems.Text = TranslationServer.Translate("mod_manager/collection_problems");
|
||||
_pillPlan.Text = TranslationServer.Translate("mod_manager/collection_plan");
|
||||
_close.TooltipText = _close.Text;
|
||||
_refresh.TooltipText = _refresh.Text;
|
||||
_addMechanic.TooltipText = _addMechanic.Text;
|
||||
_newGame.TooltipText = _newGame.Text;
|
||||
_gameSelect.TooltipText = TranslationServer.Translate("mod_manager/collection_game_tooltip");
|
||||
_pillProblems.TooltipText = TranslationServer.Translate("mod_manager/collection_problems_tooltip");
|
||||
_pillPlan.TooltipText = TranslationServer.Translate("mod_manager/collection_plan_tooltip");
|
||||
@@ -215,21 +472,34 @@ namespace Mods.ModManager
|
||||
_tabPlan.Text = TranslationServer.Translate("mod_manager/collection_tab_plan");
|
||||
_json.TooltipText = TranslationServer.Translate("mod_manager/collection_json_tooltip");
|
||||
_reset.TooltipText = TranslationServer.Translate("mod_manager/collection_reset_tooltip");
|
||||
_save.TooltipText = _save.Text;
|
||||
_tabInspector.TooltipText = _tabInspector.Text;
|
||||
_tabProblems.TooltipText = _tabProblems.Text;
|
||||
_tabPlan.TooltipText = _tabPlan.Text;
|
||||
_apply.Text = TranslationServer.Translate("mod_manager/collection_apply");
|
||||
_edgesToggle.Text = TranslationServer.Translate("mod_manager/collection_links");
|
||||
_apply.TooltipText = _apply.Text;
|
||||
_edgesToggle.TooltipText = _edgesToggle.Text;
|
||||
_edgesToggle.AddItem(TranslationServer.Translate("mod_manager/collection_links_selected"));
|
||||
_edgesToggle.AddItem(TranslationServer.Translate("mod_manager/collection_links_all"));
|
||||
_edgesToggle.AddItem(TranslationServer.Translate("mod_manager/collection_links_none"));
|
||||
var orderLegend = GetNode<Label>("Margin/Root/OrderLegend");
|
||||
orderLegend.Text = TranslationServer.Translate("mod_manager/collection_order_legend");
|
||||
orderLegend.AddThemeColorOverride("font_color", UiTokens.Faint);
|
||||
orderLegend.SetMeta(ModHelpPanel.HintTranslationMetadata, "collection_dependencies");
|
||||
|
||||
ModHelpPanel.Attach(_refresh, "refresh");
|
||||
ModHelpPanel.Attach(_addMechanic, "collection_add_mechanic");
|
||||
ModHelpPanel.Attach(_newGame, "collection_new");
|
||||
ModHelpPanel.Attach(_gameSelect, "collection_game");
|
||||
ModHelpPanel.Attach(_pillProblems, "collection_problems");
|
||||
ModHelpPanel.Attach(_pillPlan, "collection_plan");
|
||||
ModHelpPanel.Attach(_json, "collection_json");
|
||||
ModHelpPanel.Attach(_reset, "collection_reset");
|
||||
ModHelpPanel.Attach(_apply, "collection_apply");
|
||||
ModHelpPanel.Attach(_edgesToggle, "collection_links");
|
||||
ModHelpPanel.Attach(GetNode<Label>("Margin/Root/OrderLegend"), "load_order");
|
||||
|
||||
var header = _json.GetParent();
|
||||
_savesInfo = new Label { VerticalAlignment = VerticalAlignment.Center };
|
||||
_savesInfo.AddThemeColorOverride("font_color", UiTokens.Muted);
|
||||
_duplicate = new Button { Text = TranslationServer.Translate("mod_manager/collection_duplicate"), TooltipText = TranslationServer.Translate("mod_manager/collection_duplicate") };
|
||||
_revert = new Button { Text = TranslationServer.Translate("mod_manager/collection_revert"), TooltipText = TranslationServer.Translate("mod_manager/collection_revert") };
|
||||
_delete = new Button { Text = TranslationServer.Translate("mod_manager/collection_delete"), TooltipText = TranslationServer.Translate("mod_manager/collection_delete") };
|
||||
_duplicate = new Button { Text = TranslationServer.Translate("mod_manager/collection_duplicate") };
|
||||
_revert = new Button { Text = TranslationServer.Translate("mod_manager/collection_revert") };
|
||||
_delete = new Button { Text = TranslationServer.Translate("mod_manager/collection_delete") };
|
||||
ModHelpPanel.Attach(_delete, "collection_delete");
|
||||
foreach (var control in new Control[] { _savesInfo, _duplicate, _revert, _delete })
|
||||
{
|
||||
header.AddChild(control);
|
||||
@@ -252,6 +522,7 @@ namespace Mods.ModManager
|
||||
_bannerLabel.AddThemeColorOverride("font_color", UiTokens.Warning);
|
||||
_bannerPanel.Visible = false;
|
||||
|
||||
BindGraphiteControls();
|
||||
BuildCrumbs();
|
||||
BuildBuildBar();
|
||||
if (Embedded)
|
||||
@@ -271,7 +542,7 @@ namespace Mods.ModManager
|
||||
|
||||
_crumbCollections = UiKit.Button(TranslationServer.Translate("mod_manager/hub_tab_collections"));
|
||||
_crumbCollections.AddThemeFontSizeOverride("font_size", UiTokens.FontBodySmall);
|
||||
_crumbCollections.Pressed += () => GalleryRequested?.Invoke();
|
||||
_crumbCollections.Pressed += OnGalleryPressed;
|
||||
_crumbCollections.TooltipText = TranslationServer.Translate("mod_manager/gallery_crumb");
|
||||
_crumbs.AddChild(_crumbCollections);
|
||||
_crumbs.AddChild(UiKit.Label("›", UiTokens.FontBodySmall, UiTokens.Faint));
|
||||
@@ -281,10 +552,19 @@ namespace Mods.ModManager
|
||||
_crumbs.AddChild(UiKit.Label(TranslationServer.Translate("mod_manager/collection_crumb_scheme"), UiTokens.FontBodySmall, UiTokens.Body));
|
||||
|
||||
_crumbs.AddChild(UiKit.Spacer());
|
||||
GetNode("Margin/Root/Header/AddMechanic").Reparent(_crumbs);
|
||||
GetNode("Margin/Root/Header/EdgesToggle").Reparent(_crumbs);
|
||||
GetNode("Margin/Root/Header/Problems").Reparent(_crumbs);
|
||||
GetNode("Margin/Root/Header/ModsCount").Reparent(_crumbs);
|
||||
var header = GetNode<HBoxContainer>("Margin/Root/Header");
|
||||
_crumbs.Reparent(header);
|
||||
header.MoveChild(_crumbs, 0);
|
||||
_crumbs.SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
||||
GetNode<Control>("Margin/Root/Header/Spacer").Visible = false;
|
||||
var tools = GetNode<HBoxContainer>("Margin/Root/Tools");
|
||||
_addMechanic.Reparent(tools);
|
||||
tools.MoveChild(_addMechanic, 0);
|
||||
_edgesToggle.Reparent(tools);
|
||||
tools.MoveChild(_edgesToggle, 2);
|
||||
_pillProblems.Reparent(tools);
|
||||
tools.MoveChild(_pillProblems, 3);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -317,13 +597,13 @@ namespace Mods.ModManager
|
||||
row.AddChild(_buildHint);
|
||||
row.AddChild(UiKit.Spacer());
|
||||
|
||||
_buildManager = UiKit.Button(TranslationServer.Translate("mod_manager/build_open_manager"));
|
||||
_buildManager.TooltipText = _buildManager.Text;
|
||||
_buildManager.Pressed += () => OpenManagerRequested?.Invoke(null);
|
||||
_buildManager = UiKit.Button(TranslationServer.Translate("mod_manager/collection_tab_problems"));
|
||||
ModHelpPanel.Attach(_buildManager, "collection_plan");
|
||||
_buildManager.Pressed += OnBuildManagerPressed;
|
||||
row.AddChild(_buildManager);
|
||||
|
||||
_buildApply = UiKit.Button(TranslationServer.Translate("mod_manager/collection_apply"), ButtonKind.Primary);
|
||||
_buildApply.TooltipText = _buildApply.Text;
|
||||
ModHelpPanel.Attach(_buildApply, "collection_apply");
|
||||
_buildApply.Pressed += OnApplyPressed;
|
||||
row.AddChild(_buildApply);
|
||||
}
|
||||
@@ -345,6 +625,7 @@ namespace Mods.ModManager
|
||||
_close.Visible = false;
|
||||
_refresh.Visible = false;
|
||||
_newGame.Visible = false;
|
||||
_pillPlan.Visible = false;
|
||||
}
|
||||
|
||||
private void SubscribeEvents()
|
||||
@@ -353,7 +634,7 @@ namespace Mods.ModManager
|
||||
_refresh.Pressed += OnRefreshPressed;
|
||||
_addMechanic.Pressed += OnAddMechanicPressed;
|
||||
_newGame.Pressed += OnNewGamePressed;
|
||||
_edgesToggle.Toggled += _canvas.SetEdgesVisible;
|
||||
_edgesToggle.ItemSelected += _canvas.SetEdgeMode;
|
||||
_pillProblems.Pressed += () => SelectTab("problems");
|
||||
_pillPlan.Pressed += () => SelectTab("plan");
|
||||
_json.Pressed += OpenJsonDialog;
|
||||
@@ -369,6 +650,7 @@ namespace Mods.ModManager
|
||||
_canvas.ModSelected += OnModSelected;
|
||||
_canvas.GroupCommand += OnGroupCommand;
|
||||
_canvas.LayoutChanged += OnLayoutChanged;
|
||||
_canvas.ViewChanged += OnCanvasViewChanged;
|
||||
|
||||
ModDownloader.OnProgress += OnInstallProgress;
|
||||
CatalogService.OnRefreshed += OnCatalogRefreshed;
|
||||
@@ -378,6 +660,8 @@ namespace Mods.ModManager
|
||||
{
|
||||
ModDownloader.OnProgress -= OnInstallProgress;
|
||||
CatalogService.OnRefreshed -= OnCatalogRefreshed;
|
||||
if (_fitQueued)
|
||||
GetTree().ProcessFrame -= OnCanvasFirstFrame;
|
||||
}
|
||||
|
||||
/* ═══════════════ Data flow ═══════════════ */
|
||||
@@ -396,12 +680,37 @@ namespace Mods.ModManager
|
||||
private void Rebuild()
|
||||
{
|
||||
_graph = Resolve();
|
||||
foreach (var node in _graph.Nodes)
|
||||
{
|
||||
if (ModRegistry.Instance.AllDiscoveredMods.TryGetValue(node.ModId, out var info))
|
||||
node.Title = info.DisplayTitle;
|
||||
}
|
||||
_layout = _currentId == null ? new ModCollectionLayout() : CollectionLayoutStore.Load(_currentId);
|
||||
_plan = ModCollectionPlan.Build(_graph, CatalogService.CachedEntries ?? new List<CatalogExportEntry>());
|
||||
|
||||
if (_selectedGroupKey == null && _graph.Groups.Count > 0)
|
||||
{
|
||||
var initial = _graph.Groups[0];
|
||||
foreach (var group in _graph.Groups)
|
||||
{
|
||||
if (group.Role == "battle" && group.Selected && group.Mods.Count > 0)
|
||||
{
|
||||
initial = group;
|
||||
break;
|
||||
}
|
||||
}
|
||||
_selectedGroupKey = initial.Key;
|
||||
_selectedModId = initial.Mods.Count > 0 ? initial.Mods[0].ModId : null;
|
||||
}
|
||||
|
||||
_canvas.Build(_graph, _layout);
|
||||
_canvas.SetSelection(_selectedGroupKey, _selectedModId);
|
||||
_canvas.SetEdgesVisible(_edgesToggle.ButtonPressed);
|
||||
_canvas.SetEdgeMode(_edgesToggle.Selected);
|
||||
if (_fitAfterBuild && !_fitQueued)
|
||||
{
|
||||
_fitQueued = true;
|
||||
GetTree().ProcessFrame += OnCanvasFirstFrame;
|
||||
}
|
||||
|
||||
UpdateHeader();
|
||||
UpdateInspector();
|
||||
@@ -530,7 +839,7 @@ namespace Mods.ModManager
|
||||
|
||||
var current = EffectiveCollection(_currentId);
|
||||
var isGame = current?.Kind == ModCollectionKind.Game;
|
||||
_savesInfo.Visible = isGame;
|
||||
_savesInfo.Visible = isGame && !Embedded;
|
||||
if (isGame)
|
||||
_savesInfo.Text = string.Format(TranslationServer.Translate("mod_manager/collection_saves"), SaveService.ListSaves(_currentId).Count);
|
||||
_duplicate.Disabled = current == null;
|
||||
@@ -559,6 +868,11 @@ namespace Mods.ModManager
|
||||
_pillPlan.Text = TranslationServer.Translate("mod_manager/collection_plan");
|
||||
return;
|
||||
}
|
||||
if (errors > 0)
|
||||
{
|
||||
_pillPlan.Text = $"{TranslationServer.Translate("mod_manager/collection_problems")}: {errors}";
|
||||
return;
|
||||
}
|
||||
|
||||
var parts = new List<string>();
|
||||
if (_plan.Install.Count > 0)
|
||||
@@ -571,7 +885,7 @@ namespace Mods.ModManager
|
||||
}
|
||||
|
||||
private int CountErrors()
|
||||
=> _graph?.Problems?.Count ?? 0;
|
||||
=> (_graph?.Problems?.Count ?? 0) + (_plan?.Missing.Count ?? 0) + (_plan?.Mismatches.Count ?? 0);
|
||||
|
||||
private void OnGameSelected(long index)
|
||||
{
|
||||
@@ -580,6 +894,7 @@ namespace Mods.ModManager
|
||||
return;
|
||||
|
||||
_currentId = id;
|
||||
_fitAfterBuild = true;
|
||||
_selectedGroupKey = null;
|
||||
_selectedModId = null;
|
||||
Rebuild();
|
||||
@@ -587,6 +902,7 @@ namespace Mods.ModManager
|
||||
|
||||
private void OnNewGamePressed()
|
||||
{
|
||||
_fitAfterBuild = true;
|
||||
var id = UniqueGameId();
|
||||
var draft = new ModCollection
|
||||
{
|
||||
@@ -753,6 +1069,8 @@ namespace Mods.ModManager
|
||||
_tabPlan.ButtonPressed = _tab == "plan";
|
||||
|
||||
ClearChildren(_detailExtra);
|
||||
_versionEditor.Visible = false;
|
||||
_versionReference = null;
|
||||
_apply.Visible = !Embedded && _tab == "plan";
|
||||
|
||||
if (_tab == "problems")
|
||||
@@ -801,6 +1119,7 @@ namespace Mods.ModManager
|
||||
_detailMeta.Text = string.Join(" · ", metaParts);
|
||||
|
||||
_detailDescription.Text = string.IsNullOrEmpty(group.Description) ? "—" : group.Description;
|
||||
AddInfoLine(TranslationServer.Translate("mod_manager/collection_role_hint"));
|
||||
|
||||
var collection = EffectiveCollection(group.CollectionId);
|
||||
if (collection != null)
|
||||
@@ -836,18 +1155,23 @@ namespace Mods.ModManager
|
||||
{
|
||||
var node = _graph?.Nodes?.FirstOrDefault(n => string.Equals(n.ModId, modId, StringComparison.OrdinalIgnoreCase));
|
||||
_detailTitle.Text = node?.Title ?? modId;
|
||||
_detailMeta.Text = $"{modId} · {CollectionStateNames.Translate(node?.State ?? ModCollectionNodeState.Missing)}";
|
||||
_detailMeta.Text = $"{modId} · {node?.InstalledVersion ?? "—"} · {CollectionStateNames.Translate(node?.State ?? ModCollectionNodeState.Missing)}";
|
||||
_detailDescription.Text = LookupModDescription(modId);
|
||||
|
||||
if (node != null && !string.IsNullOrEmpty(node.RequiredVersion))
|
||||
AddInfoLine($"{TranslationServer.Translate("mod_manager/collection_requires")}: {node.RequiredVersion}");
|
||||
if (node != null && !string.IsNullOrEmpty(node.InstalledVersion))
|
||||
AddInfoLine($"{TranslationServer.Translate("mod_manager/installed_version")}: {node.InstalledVersion}");
|
||||
|
||||
var group = _selectedGroupKey == null ? null : FindGroup(_selectedGroupKey);
|
||||
if (group == null && node != null)
|
||||
{
|
||||
foreach (var requirement in node.Requirements)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(requirement.Operator))
|
||||
AddInfoLine(string.Format(TranslationServer.Translate("mod_manager/collection_requires"), requirement.ToString()));
|
||||
}
|
||||
}
|
||||
if (group != null)
|
||||
{
|
||||
AddInfoLine($"{TranslationServer.Translate("mod_manager/collection_in_group")}: {group.Name}");
|
||||
BindVersionRule(group, modId);
|
||||
AddModRelationships(modId);
|
||||
|
||||
var remove = MakeInspectorButton(TranslationServer.Translate("mod_manager/collection_remove_mod"),
|
||||
() => RemoveModFromGroup(group, modId), true);
|
||||
@@ -888,13 +1212,16 @@ namespace Mods.ModManager
|
||||
_detailMeta.Text = string.Empty;
|
||||
_detailDescription.Text = string.Empty;
|
||||
|
||||
if (_graph == null || _graph.Problems.Count == 0)
|
||||
if (_graph == null || CountErrors() == 0)
|
||||
{
|
||||
_detailExtra.AddChild(UiKit.Label(TranslationServer.Translate("mod_manager/collection_problems_none"), UiTokens.FontBody, UiTokens.SuccessText));
|
||||
UpdateApplyButton();
|
||||
return;
|
||||
}
|
||||
|
||||
AddPlanSection("mod_manager/collection_missing", _plan?.Missing, UiTokens.DangerText, findInCatalog: true);
|
||||
AddPlanSection("mod_manager/collection_mismatch", _plan?.Mismatches, UiTokens.DangerText, findInCatalog: true);
|
||||
|
||||
foreach (var problem in _graph.Problems)
|
||||
{
|
||||
var row = UiKit.Column(UiTokens.Space1);
|
||||
@@ -975,7 +1302,7 @@ namespace Mods.ModManager
|
||||
AddPlanSection("mod_manager/collection_install_count", _plan.Install.Select(i => i.ModId), UiTokens.Info);
|
||||
AddPlanSection("mod_manager/collection_enable_count", _plan.Enable, UiTokens.Warning);
|
||||
AddPlanSection("mod_manager/collection_missing", _plan.Missing, UiTokens.Danger, findInCatalog: true);
|
||||
AddPlanSection("mod_manager/collection_mismatch", _plan.Mismatches, UiTokens.Warning);
|
||||
AddPlanSection("mod_manager/collection_mismatch", _plan.Mismatches, UiTokens.Warning, findInCatalog: true);
|
||||
UpdateApplyButton();
|
||||
}
|
||||
|
||||
@@ -1013,7 +1340,7 @@ namespace Mods.ModManager
|
||||
var canApply = !_installing
|
||||
&& _plan != null
|
||||
&& !_plan.IsEmpty
|
||||
&& _plan.Missing.Count == 0
|
||||
&& !_plan.HasProblems
|
||||
&& CountErrors() == 0;
|
||||
_apply.Disabled = !canApply;
|
||||
if (_buildApply != null)
|
||||
@@ -1048,7 +1375,7 @@ namespace Mods.ModManager
|
||||
|
||||
_buildHint.Text = TranslationServer.Translate(blocked
|
||||
? "mod_manager/build_hint_problems"
|
||||
: "mod_manager/collection_plan_ready");
|
||||
: _overrides.Count > 0 ? "mod_manager/collection_unsaved" : "mod_manager/collection_plan_ready");
|
||||
_buildHint.AddThemeColorOverride("font_color", blocked ? UiTokens.Warning : UiTokens.SuccessText);
|
||||
}
|
||||
|
||||
@@ -1095,7 +1422,6 @@ namespace Mods.ModManager
|
||||
private Button MakeInspectorButton(string text, Action action, bool danger = false)
|
||||
{
|
||||
var button = UiKit.Button(text, danger ? ButtonKind.Danger : ButtonKind.Secondary);
|
||||
button.TooltipText = text;
|
||||
button.AddThemeFontSizeOverride("font_size", UiTokens.FontSmall);
|
||||
button.Pressed += action;
|
||||
return button;
|
||||
@@ -1606,12 +1932,17 @@ namespace Mods.ModManager
|
||||
if (collection == null)
|
||||
return;
|
||||
|
||||
var view = new TextEdit
|
||||
var view = new CodeEdit
|
||||
{
|
||||
Text = JsonSerializer.Serialize(collection, JsonOptionsProvider.WriteIndentedOptions),
|
||||
Editable = true,
|
||||
GuttersDrawLineNumbers = true,
|
||||
SyntaxHighlighter = SourceSyntax.CreateJsonHighlighter(),
|
||||
IndentUseSpaces = true,
|
||||
IndentSize = 2,
|
||||
CustomMinimumSize = new Vector2(620, 420),
|
||||
};
|
||||
view.AddThemeFontOverride("font", ToolWindow.GetMonospaceFont());
|
||||
|
||||
var dialog = new ConfirmationDialog
|
||||
{
|
||||
@@ -1798,6 +2129,8 @@ namespace Mods.ModManager
|
||||
}
|
||||
|
||||
include.Ref = forkId;
|
||||
if (string.Equals(_selectedGroupKey, group.Key, StringComparison.OrdinalIgnoreCase))
|
||||
_selectedGroupKey = $"i:{group.ParentCollectionId}:{forkId}";
|
||||
return Draft(forkId);
|
||||
}
|
||||
|
||||
@@ -1849,7 +2182,7 @@ namespace Mods.ModManager
|
||||
private async void OnApplyPressed()
|
||||
{
|
||||
if (_installing || _plan == null || _plan.IsEmpty
|
||||
|| _plan.Missing.Count > 0 || CountErrors() > 0)
|
||||
|| _plan.HasProblems || CountErrors() > 0)
|
||||
return;
|
||||
|
||||
var plan = _plan;
|
||||
|
||||
+27
-23
@@ -103,14 +103,14 @@ namespace Mods.ModManager
|
||||
_tabMods.ToggleMode = true;
|
||||
_tabMods.ButtonGroup = group;
|
||||
_tabMods.Pressed += () => ShowTab(TabMods);
|
||||
_tabMods.TooltipText = _tabMods.Text;
|
||||
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);
|
||||
_tabCollections.TooltipText = _tabCollections.Text;
|
||||
ModHelpPanel.Attach(_tabCollections, "collections_tab");
|
||||
_segment.AddChild(_tabCollections);
|
||||
|
||||
header.AddChild(UiKit.Spacer());
|
||||
@@ -122,24 +122,22 @@ namespace Mods.ModManager
|
||||
SizeFlagsVertical = Control.SizeFlags.ShrinkCenter,
|
||||
};
|
||||
_gallerySearch.TextChanged += OnGallerySearchChanged;
|
||||
_gallerySearch.TooltipText = _gallerySearch.PlaceholderText;
|
||||
ModHelpPanel.Attach(_gallerySearch, "collection_search");
|
||||
header.AddChild(_gallerySearch);
|
||||
|
||||
_galleryRefresh = UiKit.Button(TranslationServer.Translate("mod_manager/refresh"));
|
||||
_galleryRefresh.Pressed += OnGalleryRefreshPressed;
|
||||
_galleryRefresh.TooltipText = _galleryRefresh.Text;
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the full-screen help overlay once; it stays hidden until the
|
||||
/// header's "?" button opens the page of the level the player is on.
|
||||
/// </summary>
|
||||
/// <summary>Builds the non-modal inspection layer above every hub level.</summary>
|
||||
private void BuildHelp()
|
||||
{
|
||||
_help = TscnScenes.LoadAndBuild(HelpScene) as ModHelpPanel;
|
||||
@@ -157,19 +155,14 @@ namespace Mods.ModManager
|
||||
|
||||
private void OnHelpPressed()
|
||||
{
|
||||
_help?.Open(CurrentHelpLevel());
|
||||
}
|
||||
if (_help == null)
|
||||
return;
|
||||
|
||||
/// <summary>The help page follows the hub level: the mods grid, an open mod page, the collections gallery or the scheme editor.</summary>
|
||||
private string CurrentHelpLevel()
|
||||
{
|
||||
if (_editorOpen)
|
||||
return "scheme";
|
||||
if (_activeTab == TabCollections)
|
||||
return "collections";
|
||||
if (ModOpen)
|
||||
return "mod_page";
|
||||
return "mods";
|
||||
if (_help.Visible)
|
||||
_help.Close();
|
||||
else
|
||||
_help.Open();
|
||||
_helpButton.ButtonPressed = _help.Visible;
|
||||
}
|
||||
|
||||
private void BuildBanner()
|
||||
@@ -265,7 +258,6 @@ namespace Mods.ModManager
|
||||
if (_editorOpen && _editor != null)
|
||||
{
|
||||
_back.Text = TranslationServer.Translate("mod_manager/hub_back_to_gallery");
|
||||
_back.TooltipText = _back.Text;
|
||||
_title.Text = _editor.CurrentCollectionTitle ?? TranslationServer.Translate("mod_manager/collections_title");
|
||||
_segment.Visible = false;
|
||||
_gallerySearch.Visible = false;
|
||||
@@ -274,7 +266,6 @@ namespace Mods.ModManager
|
||||
}
|
||||
|
||||
_back.Text = TranslationServer.Translate(ModOpen ? "mod_manager/hub_back_to_mods" : "mod_manager/hub_back_to_menu");
|
||||
_back.TooltipText = _back.Text;
|
||||
_title.Text = TranslationServer.Translate("mod_manager/menu_title");
|
||||
_segment.Visible = true;
|
||||
_tabMods.ButtonPressed = _activeTab == TabMods;
|
||||
@@ -406,15 +397,28 @@ namespace Mods.ModManager
|
||||
|
||||
/* ═══════════════ Escape chain ═══════════════ */
|
||||
|
||||
/// <summary>Escape walks back one level: editor → gallery → mods tab, mod card → mods grid.</summary>
|
||||
/// <summary>F1 explains the focused control; Escape leaves help before walking back a level.</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -117,7 +117,6 @@ namespace Mods.ModManager
|
||||
_bannerLabel = GetNode<Label>("Margin/Root/Banner/BannerLabel");
|
||||
|
||||
close.Text = TranslationServer.Translate("mod_manager/back");
|
||||
close.TooltipText = close.Text;
|
||||
title.Text = TranslationServer.Translate("mod_manager/title");
|
||||
if (Embedded)
|
||||
{
|
||||
@@ -127,9 +126,7 @@ namespace Mods.ModManager
|
||||
title.Visible = false;
|
||||
}
|
||||
_search.PlaceholderText = TranslationServer.Translate("mod_manager/search_placeholder");
|
||||
_search.TooltipText = _search.PlaceholderText;
|
||||
_refreshButton.Text = TranslationServer.Translate("mod_manager/refresh");
|
||||
_refreshButton.TooltipText = _refreshButton.Text;
|
||||
_resultLabel.AddThemeColorOverride("font_color", UiTokens.Muted);
|
||||
|
||||
_sort.AddItem(TranslationServer.Translate("mod_manager/sort_name"));
|
||||
@@ -137,6 +134,9 @@ namespace Mods.ModManager
|
||||
_sort.AddItem(TranslationServer.Translate("mod_manager/sort_status"));
|
||||
_sort.Selected = (int)_filter.Sort;
|
||||
_sort.TooltipText = TranslationServer.Translate("mod_manager/sort_tooltip");
|
||||
ModHelpPanel.Attach(_search, "mod_search");
|
||||
ModHelpPanel.Attach(_sort, "mod_sort");
|
||||
ModHelpPanel.Attach(_refreshButton, "refresh");
|
||||
|
||||
_bannerPanel.AddThemeStyleboxOverride("panel", ModManagerTheme.MakeBox(
|
||||
UiTokens.Alpha(UiTokens.Warning, 0.15f),
|
||||
|
||||
@@ -9,10 +9,9 @@ namespace Mods.ModManager
|
||||
/// live global rectangles, so dragging a chip or a whole group keeps the
|
||||
/// lines attached without recomputing them.
|
||||
/// <para/>
|
||||
/// <see cref="Cross"/> is the "dangerous layout" flag: the dependency's two
|
||||
/// mods live in different mechanics, which means the player separated what
|
||||
/// belongs together. Cross edges are painted amber (red when the selection
|
||||
/// touches them) so the mistake is visible across the whole canvas.
|
||||
/// A dependency may cross group boundaries normally: groups describe its
|
||||
/// purpose in one game, not compatibility. Only <see cref="Problem"/> carries
|
||||
/// an actual availability/version failure and receives warning styling.
|
||||
/// </summary>
|
||||
public class CollectionEdge
|
||||
{
|
||||
@@ -25,6 +24,9 @@ namespace Mods.ModManager
|
||||
/// <summary>True when the two mods belong to different groups.</summary>
|
||||
public bool Cross { get; set; }
|
||||
|
||||
/// <summary>At least one endpoint is missing or has an incompatible installed version.</summary>
|
||||
public bool Problem { get; set; }
|
||||
|
||||
/// <summary>True when the selection touches one of the endpoints.</summary>
|
||||
public bool Active { get; set; }
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace Mods.ModManager
|
||||
if (_canvas == null || !GodotObject.IsInstanceValid(_canvas))
|
||||
return;
|
||||
|
||||
var origin = _canvas.GetGlobalRect().Position;
|
||||
var inverse = _canvas.GetGlobalTransform().AffineInverse();
|
||||
|
||||
foreach (var edge in _edges)
|
||||
{
|
||||
@@ -69,13 +69,16 @@ namespace Mods.ModManager
|
||||
if (!_chips.TryGetValue(edge.ToModId, out var to) || !GodotObject.IsInstanceValid(to))
|
||||
continue;
|
||||
|
||||
var a = from.GetGlobalRect().GetCenter() - origin;
|
||||
var b = to.GetGlobalRect().GetCenter() - origin;
|
||||
var fromRect = from.GetGlobalRect();
|
||||
var toRect = to.GetGlobalRect();
|
||||
var a = inverse * BorderPoint(fromRect, toRect.GetCenter());
|
||||
var b = inverse * BorderPoint(toRect, fromRect.GetCenter());
|
||||
var points = BuildCurve(a, b);
|
||||
var color = edge.Cross ? (edge.Active ? UiTokens.Danger : UiTokens.Warning) : (edge.Active ? UiTokens.Accent : UiTokens.LineStrong);
|
||||
var width = edge.Cross ? (edge.Active ? 3f : 2f) : (edge.Active ? 2.5f : 1.5f);
|
||||
var color = edge.Problem ? UiTokens.DangerText
|
||||
: edge.Active ? UiTokens.Accent : new Color(UiTokens.LineStrong, 0.45f);
|
||||
var width = edge.Active || edge.Problem ? 2f : 1.2f;
|
||||
|
||||
if (edge.Cross)
|
||||
if (edge.Problem)
|
||||
{
|
||||
for (var i = 0; i < points.Count - 1; i++)
|
||||
DrawDashedLine(points[i], points[i + 1], color, width, 5f);
|
||||
@@ -85,9 +88,23 @@ namespace Mods.ModManager
|
||||
{
|
||||
DrawPolyline(points.ToArray(), color, width, true);
|
||||
}
|
||||
DrawCircle(a, 2.5f, color);
|
||||
var direction = (points[^1] - points[^2]).Normalized();
|
||||
var normal = new Vector2(-direction.Y, direction.X);
|
||||
DrawLine(b, b - direction * 8f + normal * 3.5f, color, width, true);
|
||||
DrawLine(b, b - direction * 8f - normal * 3.5f, color, width, true);
|
||||
}
|
||||
}
|
||||
|
||||
private static Vector2 BorderPoint(Rect2 rect, Vector2 target)
|
||||
{
|
||||
var center = rect.GetCenter();
|
||||
var delta = target - center;
|
||||
if (Mathf.Abs(delta.X) >= Mathf.Abs(delta.Y))
|
||||
return new Vector2(delta.X >= 0 ? rect.End.X : rect.Position.X, center.Y);
|
||||
return new Vector2(center.X, delta.Y >= 0 ? rect.End.Y : rect.Position.Y);
|
||||
}
|
||||
|
||||
private static List<Vector2> BuildCurve(Vector2 a, Vector2 b)
|
||||
{
|
||||
var dx = b.X - a.X;
|
||||
|
||||
@@ -158,7 +158,7 @@ namespace Mods.ModManager
|
||||
MouseFilter = MouseFilterEnum.Ignore;
|
||||
_baseBorder = UiTokens.LineCard;
|
||||
|
||||
_style = UiKit.Frame(12, new Color(UiTokens.Surface1, 0.92f), _baseBorder);
|
||||
_style = UiKit.Frame(UiTokens.RadiusLg, new Color(UiTokens.Surface1, 0.92f), _baseBorder);
|
||||
_panel = new PanelContainer { MouseFilter = MouseFilterEnum.Stop };
|
||||
_panel.AddThemeStyleboxOverride("panel", _style);
|
||||
_panel.GuiInput += OnPanelGuiInput;
|
||||
@@ -217,7 +217,7 @@ namespace Mods.ModManager
|
||||
|
||||
_desc = UiKit.Label(string.Empty, UiTokens.FontSmall, UiTokens.Muted);
|
||||
_desc.AutowrapMode = TextServer.AutowrapMode.WordSmart;
|
||||
_desc.MaxLinesVisible = 3;
|
||||
_desc.MaxLinesVisible = 1;
|
||||
_content.AddChild(_desc);
|
||||
|
||||
_watermark = UiKit.Label(string.Empty, 42, new Color(1f, 1f, 1f, 0.04f));
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
namespace Mods.ModManager
|
||||
{
|
||||
using System;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Cthangover.Mods.Interface.Base.Lists;
|
||||
using Cthangover.Mods.Interface.Theming;
|
||||
using Godot;
|
||||
|
||||
/// <summary>
|
||||
/// Scene-positioned overview of the occupied collection canvas. Reads the
|
||||
/// existing logical frames; clicking centers the scroll view without changing
|
||||
/// composition or persisted layout. Drawing uses the signal-based mod path.
|
||||
/// </summary>
|
||||
public partial class CollectionMiniMap : ModWidget
|
||||
{
|
||||
private CollectionSchemeCanvas _canvas;
|
||||
private StyleBoxFlat _style;
|
||||
|
||||
/// <summary>Attaches the live canvas; this control owns no copy of its layout.</summary>
|
||||
public void Bind(CollectionSchemeCanvas canvas)
|
||||
{
|
||||
EnsureConstructed();
|
||||
_canvas = canvas;
|
||||
QueueRedraw();
|
||||
}
|
||||
|
||||
protected override void Construct()
|
||||
{
|
||||
MouseFilter = MouseFilterEnum.Stop;
|
||||
ClipContents = true;
|
||||
_style = UiKit.Frame(UiTokens.RadiusSm, UiTokens.Surface2, UiTokens.LineStrong);
|
||||
GuiInput += OnMapInput;
|
||||
Connect("draw", Callable.From((Action)DrawMap));
|
||||
}
|
||||
|
||||
private void DrawMap()
|
||||
{
|
||||
DrawStyleBox(_style, new Rect2(Vector2.Zero, Size));
|
||||
if (_canvas == null || !GodotObject.IsInstanceValid(_canvas))
|
||||
return;
|
||||
|
||||
var scale = (Size - Vector2.One * 8f) / _canvas.ContentBounds;
|
||||
foreach (var pair in _canvas.GroupFrames)
|
||||
{
|
||||
var frame = pair.Value;
|
||||
if (!frame.X.HasValue || !frame.Y.HasValue)
|
||||
continue;
|
||||
var rect = new Rect2(new Vector2(frame.X.Value, frame.Y.Value) * scale + Vector2.One * 4f,
|
||||
new Vector2(frame.W, frame.H) * scale);
|
||||
DrawRect(rect, UiTokens.Surface3);
|
||||
DrawRect(rect, pair.Key == _canvas.SelectedGroupKey ? UiTokens.Accent : UiTokens.LineStrong, false);
|
||||
}
|
||||
var visible = _canvas.VisibleBounds;
|
||||
DrawRect(new Rect2(visible.Position * scale + Vector2.One * 4f, visible.Size * scale), UiTokens.Accent, false);
|
||||
}
|
||||
|
||||
private void OnMapInput(InputEvent @event)
|
||||
{
|
||||
if (_canvas == null || @event is not InputEventMouseButton button
|
||||
|| !button.Pressed || button.ButtonIndex != MouseButton.Left)
|
||||
return;
|
||||
var scale = (Size - Vector2.One * 8f) / _canvas.ContentBounds;
|
||||
_canvas.CenterOn((button.Position - Vector2.One * 4f) / scale);
|
||||
AcceptEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,11 +32,14 @@ namespace Mods.ModManager
|
||||
/// <summary>Canonical mod id this chip represents.</summary>
|
||||
public string ModId { get; private set; }
|
||||
|
||||
/// <summary>The displayed occurrence's group; a mod may have another chip in another group.</summary>
|
||||
public string GroupKey { get; private set; }
|
||||
|
||||
/// <summary>Raised on left click; the canvas turns it into a selection.</summary>
|
||||
public event Action<string> Selected;
|
||||
public event Action<CollectionModChip> Selected;
|
||||
|
||||
/// <summary>Raised while dragging with the desired position inside the group's mods layer.</summary>
|
||||
public event Action<string, Vector2> Moved;
|
||||
public event Action<CollectionModChip, Vector2> Moved;
|
||||
|
||||
/// <summary>Raised when a drag ends, after the last <see cref="Moved"/>.</summary>
|
||||
public event Action DragFinished;
|
||||
@@ -57,17 +60,20 @@ namespace Mods.ModManager
|
||||
/// the chip is added to the tree; safe to call again to refresh the
|
||||
/// status without rebuilding the node.
|
||||
/// </summary>
|
||||
public void Init(string modId, string role, string title, string version, string stateLabel, Color stateColor, string tooltip)
|
||||
public void Init(string groupKey, string modId, string role, string title, string version, string requirement, string stateLabel, Color stateColor, string tooltip)
|
||||
{
|
||||
EnsureConstructed();
|
||||
|
||||
ModId = modId;
|
||||
GroupKey = groupKey;
|
||||
_stateColor = stateColor;
|
||||
_role.Text = string.IsNullOrEmpty(role) ? string.Empty : role.ToUpperInvariant();
|
||||
_name.Text = title;
|
||||
_meta.Text = string.IsNullOrEmpty(version) ? stateLabel : $"{version} · {stateLabel}";
|
||||
_meta.Text = string.IsNullOrEmpty(version) ? stateLabel
|
||||
: version + (string.IsNullOrEmpty(requirement) ? string.Empty : " · " + requirement);
|
||||
_dot.Color = stateColor;
|
||||
_style.BorderColor = stateColor;
|
||||
_style.BorderColor = stateColor == UiTokens.Danger || stateColor == UiTokens.DangerText || stateColor == UiTokens.Warning
|
||||
? stateColor : UiTokens.LineCard;
|
||||
_panel.TooltipText = tooltip ?? string.Empty;
|
||||
}
|
||||
|
||||
@@ -75,7 +81,10 @@ namespace Mods.ModManager
|
||||
public void SetSelected(bool selected)
|
||||
{
|
||||
EnsureConstructed();
|
||||
_style.BorderColor = selected ? UiTokens.Accent : _stateColor;
|
||||
_style.BorderColor = selected ? UiTokens.Accent : UiTokens.LineCard;
|
||||
_style.BgColor = selected ? UiTokens.SurfaceSelected : UiTokens.Surface1;
|
||||
if (!selected && (_stateColor == UiTokens.Danger || _stateColor == UiTokens.DangerText || _stateColor == UiTokens.Warning))
|
||||
_style.BorderColor = _stateColor;
|
||||
}
|
||||
|
||||
protected override void Construct()
|
||||
@@ -136,7 +145,7 @@ namespace Mods.ModManager
|
||||
{
|
||||
_dragging = true;
|
||||
_moved = false;
|
||||
Selected?.Invoke(ModId);
|
||||
Selected?.Invoke(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -168,7 +177,7 @@ namespace Mods.ModManager
|
||||
_moved = true;
|
||||
|
||||
Position = clamped;
|
||||
Moved?.Invoke(ModId, clamped);
|
||||
Moved?.Invoke(this, clamped);
|
||||
AcceptEvent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,9 @@ namespace Mods.ModManager
|
||||
/// <summary>Raised after any drag or resize ends, so the layout can be flushed to the store.</summary>
|
||||
public event Action LayoutChanged;
|
||||
|
||||
/// <summary>Scale or scroll changed; the toolbar and overview may refresh their presentation.</summary>
|
||||
public event Action ViewChanged;
|
||||
|
||||
/// <summary>Whether dependency lines are painted; toggled from the header.</summary>
|
||||
public bool ShowEdges { get; set; } = true;
|
||||
|
||||
@@ -74,6 +77,116 @@ namespace Mods.ModManager
|
||||
private readonly Dictionary<string, CollectionGroupCard> _cards = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, CollectionModChip> _chips = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, string> _chipGroups = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, CollectionModChip> _instances = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, ModCollectionLayoutRect> _visibleFrames = new(StringComparer.OrdinalIgnoreCase);
|
||||
private Control _content;
|
||||
private CollectionMiniMap _miniMap;
|
||||
private long _edgeMode;
|
||||
private float _zoom = 1f;
|
||||
private string _search = string.Empty;
|
||||
|
||||
/// <summary>Scale of the canvas contents, independent of persisted logical coordinates.</summary>
|
||||
public float Zoom => _zoom;
|
||||
|
||||
/// <summary>Occupied logical bounds, excluding the empty scrollable workspace floor.</summary>
|
||||
public Vector2 ContentBounds { get; private set; } = new(460f, 240f);
|
||||
|
||||
/// <summary>Current visible rectangle in logical coordinates, used by the overview map.</summary>
|
||||
public Rect2 VisibleBounds
|
||||
{
|
||||
get
|
||||
{
|
||||
var scroll = GetParent<ScrollContainer>();
|
||||
return scroll == null ? new Rect2() : new Rect2(
|
||||
new Vector2(scroll.ScrollHorizontal, scroll.ScrollVertical) / _zoom,
|
||||
scroll.Size / _zoom);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Overview rectangles use the same logical geometry as the group frames.</summary>
|
||||
public IReadOnlyDictionary<string, ModCollectionLayoutRect> GroupFrames => _visibleFrames;
|
||||
|
||||
/// <summary>Connects the scene-defined overview control after both controls entered the tree.</summary>
|
||||
public void SetMiniMap(CollectionMiniMap miniMap)
|
||||
{
|
||||
_miniMap = miniMap;
|
||||
_miniMap.Bind(this);
|
||||
}
|
||||
|
||||
/// <summary>0: selected mod only, 1: all dependencies, 2: hidden; group boundaries never change severity.</summary>
|
||||
public void SetEdgeMode(long mode)
|
||||
{
|
||||
_edgeMode = mode;
|
||||
SetEdgesVisible(mode != 2);
|
||||
RebuildEdges();
|
||||
}
|
||||
|
||||
/// <summary>Filters presentation only; hidden search matches do not leave the game's composition.</summary>
|
||||
public void SetSearch(string query)
|
||||
{
|
||||
_search = query?.Trim() ?? string.Empty;
|
||||
foreach (var chip in _instances.Values)
|
||||
{
|
||||
var node = FindNode(chip.ModId);
|
||||
var match = _search.Length == 0 || chip.ModId.Contains(_search, StringComparison.OrdinalIgnoreCase)
|
||||
|| (node?.Title?.Contains(_search, StringComparison.OrdinalIgnoreCase) ?? false);
|
||||
chip.Modulate = new Color(1, 1, 1, match ? 1f : 0.2f);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Increases scale without rewriting any stored group or chip positions.</summary>
|
||||
public void ZoomIn() => SetZoom(_zoom * 1.15f);
|
||||
|
||||
/// <summary>Decreases scale without rewriting any stored group or chip positions.</summary>
|
||||
public void ZoomOut() => SetZoom(_zoom / 1.15f);
|
||||
|
||||
/// <summary>Fits occupied group bounds horizontally; the empty scroll floor does not shrink the scheme.</summary>
|
||||
public void FitWidth()
|
||||
{
|
||||
var scroll = GetParent<ScrollContainer>();
|
||||
if (scroll == null || scroll.Size.X <= 0f)
|
||||
return;
|
||||
SetZoom(Mathf.Min(1f, (scroll.Size.X - 24f) / Mathf.Max(1f, ContentBounds.X)));
|
||||
scroll.ScrollHorizontal = 0;
|
||||
scroll.ScrollVertical = 0;
|
||||
RefreshViewport();
|
||||
}
|
||||
|
||||
/// <summary>Re-packs the current groups; composition and individual chip locations stay unchanged.</summary>
|
||||
public void ArrangeGroups()
|
||||
{
|
||||
var defaults = ComputeDefaultLayout();
|
||||
foreach (var pair in defaults)
|
||||
{
|
||||
var rect = Rect(pair.Key);
|
||||
rect.X = pair.Value.X;
|
||||
rect.Y = pair.Value.Y;
|
||||
_cards[pair.Key].SetFrame(new Rect2(pair.Value, new Vector2(rect.W, rect.H)));
|
||||
}
|
||||
UpdateCanvasSize();
|
||||
RebuildEdges();
|
||||
LayoutChanged?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>Centers the scroll view on a logical canvas position (selection or overview click).</summary>
|
||||
public void CenterOn(Vector2 position)
|
||||
{
|
||||
var scroll = GetParent<ScrollContainer>();
|
||||
if (scroll == null)
|
||||
return;
|
||||
scroll.ScrollHorizontal = Mathf.RoundToInt(position.X * _zoom - scroll.Size.X / 2f);
|
||||
scroll.ScrollVertical = Mathf.RoundToInt(position.Y * _zoom - scroll.Size.Y / 2f);
|
||||
RefreshViewport();
|
||||
}
|
||||
|
||||
/// <summary>Reveals the selected occurrence, keeping another group's copy from stealing focus.</summary>
|
||||
public void ShowSelection()
|
||||
{
|
||||
if (SelectedModId != null && _instances.TryGetValue(SelectedGroupKey + "|" + SelectedModId, out var chip))
|
||||
CenterOn(chip.Position + _cards[chip.GroupKey].Position + chip.Size / 2f);
|
||||
else if (SelectedGroupKey != null && _cards.TryGetValue(SelectedGroupKey, out var card))
|
||||
CenterOn(card.Position + card.Size / 2f);
|
||||
}
|
||||
|
||||
private bool _panning;
|
||||
private Vector2 _panStartMouse;
|
||||
@@ -94,7 +207,7 @@ namespace Mods.ModManager
|
||||
_layout.Groups ??= new Dictionary<string, ModCollectionLayoutRect>();
|
||||
_layout.Mods ??= new Dictionary<string, ModCollectionLayoutPoint>();
|
||||
|
||||
foreach (var child in GetChildren())
|
||||
foreach (var child in _content.GetChildren())
|
||||
{
|
||||
if (child != _edges)
|
||||
child.QueueFree();
|
||||
@@ -103,6 +216,8 @@ namespace Mods.ModManager
|
||||
_cards.Clear();
|
||||
_chips.Clear();
|
||||
_chipGroups.Clear();
|
||||
_instances.Clear();
|
||||
_visibleFrames.Clear();
|
||||
|
||||
if (graph != null && graph.Groups.Count > 0)
|
||||
{
|
||||
@@ -110,11 +225,12 @@ namespace Mods.ModManager
|
||||
BuildCards();
|
||||
}
|
||||
|
||||
_edges.SetEdges(null, _chips, this);
|
||||
MoveChild(_edges, GetChildCount() - 1);
|
||||
_edges.SetEdges(null, _chips, _content);
|
||||
_content.MoveChild(_edges, _content.GetChildCount() - 1);
|
||||
_edges.Visible = ShowEdges;
|
||||
UpdateCanvasSize();
|
||||
RebuildEdges();
|
||||
SetSearch(_search);
|
||||
}
|
||||
|
||||
/// <summary>Highlights one group and/or one mod; pass nulls to clear.</summary>
|
||||
@@ -127,8 +243,20 @@ namespace Mods.ModManager
|
||||
foreach (var pair in _cards)
|
||||
pair.Value.SetSelected(string.Equals(pair.Key, groupKey, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
foreach (var pair in _chips)
|
||||
pair.Value.SetSelected(string.Equals(pair.Key, modId, StringComparison.OrdinalIgnoreCase));
|
||||
foreach (var pair in _chipGroups)
|
||||
{
|
||||
if (_instances.TryGetValue(pair.Value + "|" + pair.Key, out var primary))
|
||||
_chips[pair.Key] = primary;
|
||||
}
|
||||
|
||||
foreach (var chip in _instances.Values)
|
||||
{
|
||||
var selected = string.Equals(chip.GroupKey, groupKey, StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(chip.ModId, modId, StringComparison.OrdinalIgnoreCase);
|
||||
chip.SetSelected(selected);
|
||||
if (selected)
|
||||
_chips[chip.ModId] = chip;
|
||||
}
|
||||
|
||||
RebuildEdges();
|
||||
}
|
||||
@@ -145,15 +273,42 @@ namespace Mods.ModManager
|
||||
|
||||
/// <summary>Finds the group key owning a rendered mod chip, for inspector navigation.</summary>
|
||||
public string GetGroupKeyOfMod(string modId)
|
||||
=> modId != null && _chipGroups.TryGetValue(modId, out var key) ? key : null;
|
||||
{
|
||||
if (modId != null && string.Equals(modId, SelectedModId, StringComparison.OrdinalIgnoreCase)
|
||||
&& SelectedGroupKey != null)
|
||||
return SelectedGroupKey;
|
||||
return modId != null && _chipGroups.TryGetValue(modId, out var key) ? key : null;
|
||||
}
|
||||
|
||||
protected override void Construct()
|
||||
{
|
||||
MouseFilter = MouseFilterEnum.Stop;
|
||||
GuiInput += OnCanvasGuiInput;
|
||||
_content = new Control { MouseFilter = MouseFilterEnum.Ignore };
|
||||
AddChild(_content);
|
||||
_content.Connect("draw", Callable.From((Action)DrawGrid));
|
||||
_edges = new CollectionEdgeLayer();
|
||||
AddChild(_edges);
|
||||
_content.AddChild(_edges);
|
||||
_edges.EnsureConstructed();
|
||||
var scroll = GetParent<ScrollContainer>();
|
||||
if (scroll != null)
|
||||
{
|
||||
scroll.GetHScrollBar().ValueChanged += OnScrollChanged;
|
||||
scroll.GetVScrollBar().ValueChanged += OnScrollChanged;
|
||||
scroll.Resized += RefreshViewport;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Destruct()
|
||||
{
|
||||
GuiInput -= OnCanvasGuiInput;
|
||||
var scroll = GetParent<ScrollContainer>();
|
||||
if (scroll != null && GodotObject.IsInstanceValid(scroll))
|
||||
{
|
||||
scroll.GetHScrollBar().ValueChanged -= OnScrollChanged;
|
||||
scroll.GetVScrollBar().ValueChanged -= OnScrollChanged;
|
||||
scroll.Resized -= RefreshViewport;
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildCards()
|
||||
@@ -162,7 +317,7 @@ namespace Mods.ModManager
|
||||
{
|
||||
var rect = _layout.Groups[group.Key];
|
||||
var card = new CollectionGroupCard();
|
||||
AddChild(card);
|
||||
_content.AddChild(card);
|
||||
|
||||
var summary = Summarize(group);
|
||||
card.Init(group, summary.Label, summary.Color, string.Equals(group.Key, SelectedGroupKey, StringComparison.OrdinalIgnoreCase));
|
||||
@@ -174,6 +329,7 @@ namespace Mods.ModManager
|
||||
card.Resized += OnCardResized;
|
||||
card.DragFinished += OnDragFinished;
|
||||
_cards[group.Key] = card;
|
||||
_visibleFrames[group.Key] = rect;
|
||||
|
||||
foreach (var mod in group.Mods)
|
||||
{
|
||||
@@ -183,11 +339,14 @@ namespace Mods.ModManager
|
||||
card.ModsLayer.AddChild(chip);
|
||||
|
||||
var node = FindNode(mod.ModId);
|
||||
var requirement = ModDependency.Parse(mod.Requirement);
|
||||
chip.Init(
|
||||
group.Key,
|
||||
mod.ModId,
|
||||
mod.Role,
|
||||
node?.Title ?? mod.ModId,
|
||||
VersionOf(node),
|
||||
requirement.IsValid && requirement.VersionRaw != null ? requirement.Operator + requirement.VersionRaw : null,
|
||||
StateLabel(node),
|
||||
StateColor(node),
|
||||
BuildModTooltip(mod, node));
|
||||
@@ -195,6 +354,7 @@ namespace Mods.ModManager
|
||||
chip.Selected += OnChipSelected;
|
||||
chip.Moved += OnChipMoved;
|
||||
chip.DragFinished += OnDragFinished;
|
||||
_instances[key] = chip;
|
||||
|
||||
if (!_chips.ContainsKey(mod.ModId))
|
||||
{
|
||||
@@ -396,11 +556,11 @@ namespace Mods.ModManager
|
||||
GroupSelected?.Invoke(groupKey);
|
||||
}
|
||||
|
||||
private void OnChipSelected(string modId)
|
||||
private void OnChipSelected(CollectionModChip chip)
|
||||
{
|
||||
SelectedModId = modId;
|
||||
SelectedGroupKey = null;
|
||||
ModSelected?.Invoke(modId);
|
||||
SelectedModId = chip.ModId;
|
||||
SelectedGroupKey = chip.GroupKey;
|
||||
ModSelected?.Invoke(chip.ModId);
|
||||
}
|
||||
|
||||
private void OnCardCommand(string groupKey, string command)
|
||||
@@ -427,12 +587,9 @@ namespace Mods.ModManager
|
||||
RebuildEdges();
|
||||
}
|
||||
|
||||
private void OnChipMoved(string modId, Vector2 position)
|
||||
private void OnChipMoved(CollectionModChip chip, Vector2 position)
|
||||
{
|
||||
if (!_chipGroups.TryGetValue(modId, out var groupKey))
|
||||
return;
|
||||
|
||||
var key = groupKey + "|" + modId;
|
||||
var key = chip.GroupKey + "|" + chip.ModId;
|
||||
if (!_layout.Mods.TryGetValue(key, out var point) || point == null)
|
||||
{
|
||||
point = new ModCollectionLayoutPoint();
|
||||
@@ -472,11 +629,15 @@ namespace Mods.ModManager
|
||||
var size = new Vector2(
|
||||
Mathf.Max(maxX + GapX + 160f, MinCanvasWidth),
|
||||
Mathf.Max(maxY + GapY + 120f, MinCanvasHeight));
|
||||
ContentBounds = new Vector2(Mathf.Max(460f, maxX + GapX), Mathf.Max(240f, maxY + GapY));
|
||||
// Drives the canvas rect through the surrounding ScrollContainer.
|
||||
// _edges carries FullRect anchors (CollectionEdgeLayer.Construct),
|
||||
// so it follows this resize automatically; assigning its Size
|
||||
// directly would be overridden and logs an anchors warning.
|
||||
CustomMinimumSize = size;
|
||||
_content.Size = size;
|
||||
_content.Scale = Vector2.One * _zoom;
|
||||
CustomMinimumSize = size * _zoom;
|
||||
RefreshViewport();
|
||||
}
|
||||
|
||||
private void RebuildEdges()
|
||||
@@ -491,18 +652,60 @@ namespace Mods.ModManager
|
||||
|
||||
var fromGroup = GetGroupKeyOfMod(edge.FromModId);
|
||||
var toGroup = GetGroupKeyOfMod(edge.ToModId);
|
||||
var active = string.Equals(edge.FromModId, SelectedModId, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(edge.ToModId, SelectedModId, StringComparison.OrdinalIgnoreCase);
|
||||
if (_edgeMode == 0 && !active)
|
||||
continue;
|
||||
edges.Add(new CollectionEdge
|
||||
{
|
||||
FromModId = edge.FromModId,
|
||||
ToModId = edge.ToModId,
|
||||
Cross = !string.Equals(fromGroup, toGroup, StringComparison.OrdinalIgnoreCase),
|
||||
Active = string.Equals(edge.FromModId, SelectedModId, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(edge.ToModId, SelectedModId, StringComparison.OrdinalIgnoreCase),
|
||||
Active = active,
|
||||
Problem = HasProblem(edge.FromModId) || HasProblem(edge.ToModId),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_edges.SetEdges(edges, _chips, this);
|
||||
_edges.SetEdges(edges, _chips, _content);
|
||||
RefreshViewport();
|
||||
}
|
||||
|
||||
private bool HasProblem(string modId)
|
||||
{
|
||||
var node = FindNode(modId);
|
||||
return node == null || node.State == ModCollectionNodeState.Missing
|
||||
|| node.State == ModCollectionNodeState.VersionMismatch;
|
||||
}
|
||||
|
||||
private void SetZoom(float value)
|
||||
{
|
||||
var scroll = GetParent<ScrollContainer>();
|
||||
var center = scroll == null ? Vector2.Zero : VisibleBounds.GetCenter();
|
||||
_zoom = Mathf.Clamp(value, 0.3f, 1.5f);
|
||||
UpdateCanvasSize();
|
||||
CenterOn(center);
|
||||
}
|
||||
|
||||
private void OnScrollChanged(double value) => RefreshViewport();
|
||||
|
||||
private void RefreshViewport()
|
||||
{
|
||||
_content?.QueueRedraw();
|
||||
_miniMap?.QueueRedraw();
|
||||
ViewChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void DrawGrid()
|
||||
{
|
||||
var visible = VisibleBounds;
|
||||
var start = visible.Position.Snapped(new Vector2(20f, 20f));
|
||||
var color = new Color(UiTokens.LineCard, 0.55f);
|
||||
for (var x = start.X; x <= visible.End.X + 20f; x += 20f)
|
||||
{
|
||||
for (var y = start.Y; y <= visible.End.Y + 20f; y += 20f)
|
||||
_content.DrawCircle(new Vector2(x, y), 0.8f, color);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -516,6 +719,17 @@ namespace Mods.ModManager
|
||||
/// </summary>
|
||||
private void OnCanvasGuiInput(InputEvent @event)
|
||||
{
|
||||
if (@event is InputEventMouseButton wheel && wheel.Pressed && wheel.CtrlPressed)
|
||||
{
|
||||
if (wheel.ButtonIndex == MouseButton.WheelUp)
|
||||
ZoomIn();
|
||||
else if (wheel.ButtonIndex == MouseButton.WheelDown)
|
||||
ZoomOut();
|
||||
else
|
||||
return;
|
||||
AcceptEvent();
|
||||
return;
|
||||
}
|
||||
if (@event is InputEventMouseButton button && button.ButtonIndex == MouseButton.Left)
|
||||
{
|
||||
if (button.Pressed)
|
||||
|
||||
@@ -151,7 +151,7 @@ namespace Mods.ModManager
|
||||
row.AddChild(UiKit.Spacer());
|
||||
|
||||
var newGame = UiKit.Button(TranslationServer.Translate("mod_manager/gallery_new_game"), ButtonKind.Primary);
|
||||
newGame.TooltipText = newGame.Text;
|
||||
ModHelpPanel.Attach(newGame, "collection_new");
|
||||
newGame.Pressed += () => NewGameRequested?.Invoke();
|
||||
row.AddChild(newGame);
|
||||
_host.AddChild(row);
|
||||
@@ -402,7 +402,7 @@ namespace Mods.ModManager
|
||||
button.Disabled = true;
|
||||
}
|
||||
|
||||
button.TooltipText = button.Text;
|
||||
ModHelpPanel.Attach(button, "collection_apply");
|
||||
if (action != null)
|
||||
button.Pressed += action;
|
||||
return button;
|
||||
@@ -411,7 +411,7 @@ namespace Mods.ModManager
|
||||
private Button BuildEditButton(ModCollectionGameSummary summary)
|
||||
{
|
||||
var button = UiKit.Button(TranslationServer.Translate("mod_manager/gallery_edit_scheme"));
|
||||
button.TooltipText = button.Text;
|
||||
ModHelpPanel.Attach(button, "collection_edit");
|
||||
button.Disabled = _installingId != null;
|
||||
button.Pressed += () => EditSchemeRequested?.Invoke(summary.Collection.Id);
|
||||
return button;
|
||||
@@ -420,7 +420,7 @@ namespace Mods.ModManager
|
||||
private Button BuildDeleteButton(ModCollectionGameSummary summary)
|
||||
{
|
||||
var button = UiKit.Button(TranslationServer.Translate("mod_manager/collection_delete"), ButtonKind.Danger);
|
||||
button.TooltipText = button.Text;
|
||||
ModHelpPanel.Attach(button, "collection_delete");
|
||||
button.Disabled = _installingId != null;
|
||||
button.Pressed += () => CollectionDeletion.Confirm(this, summary.Collection, () =>
|
||||
{
|
||||
|
||||
+13
-9
@@ -76,12 +76,18 @@ namespace Mods.ModManager
|
||||
_source.MouseFilter = Control.MouseFilterEnum.Pass;
|
||||
AddThemeStyleboxOverride("panel", ModManagerTheme.CreateCardStyle(data.IsConflict));
|
||||
|
||||
ModHelpPanel.Attach(this, "mod_card");
|
||||
ModHelpPanel.Attach(_versions, "mod_version");
|
||||
ModHelpPanel.Attach(_action, "mod_action");
|
||||
ModHelpPanel.Attach(_enabled, "mod_enabled");
|
||||
|
||||
_versions.ItemSelected += OnVersionSelected;
|
||||
_action.Pressed += OnActionPressed;
|
||||
_enabled.Toggled += OnEnabledPressed;
|
||||
GuiInput += OnGuiInput;
|
||||
|
||||
BuildCollectionsLink();
|
||||
ModHelpPanel.Attach(_collectionsLink, "mod_collections");
|
||||
PopulateVersions();
|
||||
RefreshState();
|
||||
LoadPreview();
|
||||
@@ -138,7 +144,7 @@ namespace Mods.ModManager
|
||||
{
|
||||
_versions.AddItem("v" + (_data.LocalVersion ?? "—"));
|
||||
_versions.Disabled = true;
|
||||
_versions.TooltipText = _versions.GetItemText(0);
|
||||
_versions.TooltipText = TranslationServer.Translate("mod_manager/hint_mod_version_body");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -164,9 +170,9 @@ namespace Mods.ModManager
|
||||
}
|
||||
|
||||
_versions.Selected = selectedIndex;
|
||||
// "v1.2.0 · latest · installed" does not fit the narrow combo: it ends in an
|
||||
// ellipsis (clip_text in the scene), the full label is in the tooltip.
|
||||
_versions.TooltipText = _versions.GetItemText(selectedIndex);
|
||||
// The combo clips its long version label, so its tooltip explains
|
||||
// the version markers instead of repeating the truncated caption.
|
||||
_versions.TooltipText = TranslationServer.Translate("mod_manager/hint_mod_version_body");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -200,7 +206,6 @@ namespace Mods.ModManager
|
||||
var text = ModCollectionLinks.BadgeText(_data.CollectionsCount);
|
||||
_collectionsLink.Visible = text.Length > 0;
|
||||
_collectionsLink.Text = text;
|
||||
_collectionsLink.TooltipText = text;
|
||||
}
|
||||
|
||||
private void UpdateActionButton()
|
||||
@@ -214,7 +219,6 @@ namespace Mods.ModManager
|
||||
|
||||
_action.Visible = true;
|
||||
_action.Text = ModActionNames.Translate(kind.Value);
|
||||
_action.TooltipText = _action.Text;
|
||||
|
||||
if (ModActionNames.IsDanger(kind.Value))
|
||||
CthangoverTheme.ApplyDanger(_action);
|
||||
@@ -232,8 +236,8 @@ namespace Mods.ModManager
|
||||
_enabled.ButtonPressed = !_data.IsDisabled;
|
||||
_updatingEnabled = false;
|
||||
_enabled.TooltipText = TranslationServer.Translate(_data.IsDisabled
|
||||
? "mod_manager/action_enable"
|
||||
: "mod_manager/action_disable");
|
||||
? "mod_manager/enable_tooltip"
|
||||
: "mod_manager/disable_tooltip");
|
||||
}
|
||||
|
||||
private void LoadPreview()
|
||||
@@ -291,7 +295,7 @@ namespace Mods.ModManager
|
||||
return;
|
||||
|
||||
_data.SelectedDownload = _data.Downloads[(int)index];
|
||||
_versions.TooltipText = _versions.GetItemText((int)index);
|
||||
_versions.TooltipText = TranslationServer.Translate("mod_manager/hint_mod_version_body");
|
||||
_data.RecomputeStatus();
|
||||
RefreshState();
|
||||
VersionChanged?.Invoke(_data);
|
||||
|
||||
@@ -86,6 +86,8 @@ namespace Mods.ModManager
|
||||
_conflictsBox = GetNode<VBoxContainer>("Scroll/Content/ConflictsBox");
|
||||
_conflictsText = GetNode<Label>("Scroll/Content/ConflictsBox/Text");
|
||||
|
||||
ModHelpPanel.Attach(_versions, "mod_version");
|
||||
|
||||
_versions.ItemSelected += OnVersionSelected;
|
||||
|
||||
_author.AddThemeColorOverride("font_color", UiTokens.Muted);
|
||||
@@ -177,7 +179,7 @@ namespace Mods.ModManager
|
||||
}
|
||||
|
||||
_versions.Selected = selectedIndex;
|
||||
_versions.TooltipText = _versions.GetItemText(selectedIndex);
|
||||
_versions.TooltipText = TranslationServer.Translate("mod_manager/hint_mod_version_body");
|
||||
}
|
||||
|
||||
private void BuildActions()
|
||||
@@ -191,19 +193,21 @@ namespace Mods.ModManager
|
||||
var primary = ModActions.GetPrimary(_data);
|
||||
if (primary != null)
|
||||
{
|
||||
var button = new Button { Text = ModActionNames.Translate(primary.Value), TooltipText = ModActionNames.Translate(primary.Value) };
|
||||
var button = new Button { Text = ModActionNames.Translate(primary.Value) };
|
||||
if (ModActionNames.IsDanger(primary.Value))
|
||||
CthangoverTheme.ApplyDanger(button);
|
||||
else
|
||||
CthangoverTheme.ApplyPrimary(button);
|
||||
ModHelpPanel.Attach(button, "mod_action");
|
||||
button.Pressed += OnPrimaryPressed;
|
||||
_actionsRow.AddChild(button);
|
||||
}
|
||||
|
||||
if (primary != ModActionKind.Delete && _data.IsDeletable)
|
||||
{
|
||||
var delete = new Button { Text = ModActionNames.Translate(ModActionKind.Delete), TooltipText = ModActionNames.Translate(ModActionKind.Delete) };
|
||||
var delete = new Button { Text = ModActionNames.Translate(ModActionKind.Delete) };
|
||||
CthangoverTheme.ApplyDanger(delete);
|
||||
ModHelpPanel.Attach(delete, "mod_delete");
|
||||
delete.Pressed += OnDeletePressed;
|
||||
_actionsRow.AddChild(delete);
|
||||
}
|
||||
@@ -215,14 +219,12 @@ namespace Mods.ModManager
|
||||
Text = ModActionNames.Translate(_data.IsDisabled
|
||||
? ModActionKind.Enable
|
||||
: ModActionKind.Disable),
|
||||
TooltipText = ModActionNames.Translate(_data.IsDisabled
|
||||
? ModActionKind.Enable
|
||||
: ModActionKind.Disable),
|
||||
};
|
||||
if (_data.IsDisabled)
|
||||
CthangoverTheme.ApplyPrimary(toggle);
|
||||
else
|
||||
CthangoverTheme.ApplyDanger(toggle);
|
||||
ModHelpPanel.Attach(toggle, "mod_enabled");
|
||||
toggle.Pressed += OnTogglePressed;
|
||||
_actionsRow.AddChild(toggle);
|
||||
}
|
||||
@@ -245,12 +247,12 @@ namespace Mods.ModManager
|
||||
var link = new Button
|
||||
{
|
||||
Text = collectionsText,
|
||||
TooltipText = collectionsText,
|
||||
FocusMode = FocusModeEnum.None,
|
||||
};
|
||||
link.AddThemeFontSizeOverride("font_size", UiTokens.FontBodySmall);
|
||||
link.AddThemeColorOverride("font_color", UiTokens.TypeTarget);
|
||||
link.AddThemeColorOverride("font_hover_color", UiTokens.TypeTarget);
|
||||
ModHelpPanel.Attach(link, "mod_collections");
|
||||
link.Pressed += () => CollectionsLinkRequested?.Invoke();
|
||||
_actionsRow.AddChild(link);
|
||||
}
|
||||
|
||||
@@ -56,8 +56,9 @@ namespace Mods.ModManager
|
||||
_statusCaption.Text = TranslationServer.Translate("mod_manager/filter_status");
|
||||
_categoryCaption.Text = TranslationServer.Translate("mod_manager/filter_categories");
|
||||
_groupToggle.Text = TranslationServer.Translate("mod_manager/group_by_category");
|
||||
_groupToggle.TooltipText = _groupToggle.Text;
|
||||
_groupToggle.ButtonPressed = _state.GroupByCategory;
|
||||
ModHelpPanel.Attach(_reset, "filters_reset");
|
||||
ModHelpPanel.Attach(_groupToggle, "group_categories");
|
||||
|
||||
_statusCaption.AddThemeColorOverride("font_color", UiTokens.Muted);
|
||||
_categoryCaption.AddThemeColorOverride("font_color", UiTokens.Muted);
|
||||
@@ -80,14 +81,12 @@ namespace Mods.ModManager
|
||||
{
|
||||
var count = statusCounts.GetValueOrDefault(pair.Key);
|
||||
pair.Value.Text = $"{ModStatusNames.Translate(pair.Key)} ({count})";
|
||||
pair.Value.TooltipText = pair.Value.Text;
|
||||
}
|
||||
|
||||
foreach (var pair in _categoryBoxes)
|
||||
{
|
||||
var count = categoryCounts.GetValueOrDefault(pair.Key);
|
||||
pair.Value.Text = $"{ModCategoryNames.Translate(pair.Key)} ({count})";
|
||||
pair.Value.TooltipText = pair.Value.Text;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+200
-24
@@ -1,54 +1,230 @@
|
||||
namespace Mods.ModManager
|
||||
{
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Cthangover.Mods.Interface.Theming;
|
||||
using Godot;
|
||||
|
||||
/// <summary>
|
||||
/// Full-screen help overlay of the mods hub: one page per hub level — the mods
|
||||
/// grid, a mod's page, the games gallery and the scheme editor. The layout is the
|
||||
/// <c>mod_help_panel.tscn</c> prefab (dim backdrop, centred frame, title, body,
|
||||
/// close button); this class only binds the nodes and fills the texts, so a new
|
||||
/// page is a locale pair <c>mod_manager/help_<level>_title/_body</c> — no code.
|
||||
/// Opened by the hub header's help button, closed by the button, a click on the
|
||||
/// dim or Escape (the hub closes the panel first in its own Escape chain).
|
||||
/// Non-modal, contextual help for the mods hub. Controls carry a locale suffix in
|
||||
/// <see cref="HintKeyMetadata"/>; the layer finds the annotated ancestor under the
|
||||
/// pointer or keyboard focus, outlines it and places a compact card next to it.
|
||||
/// The root ignores mouse input, so the inspected control still performs its normal
|
||||
/// action. A click pins the current card without swallowing that click; Escape or
|
||||
/// the header button leaves help mode through <see cref="ModHubScreen"/>.
|
||||
/// </summary>
|
||||
public partial class ModHelpPanel : Cthangover.Mods.Interface.Base.Lists.ModWidget
|
||||
{
|
||||
public const string HintKeyMetadata = "mod_manager_hint";
|
||||
|
||||
/// <summary>Optional locale-pair suffix for a contextual explanation without changing the target's stable help id.</summary>
|
||||
public const string HintTranslationMetadata = "mod_manager_hint_translation";
|
||||
|
||||
private const float Gap = 10f;
|
||||
private const float ScreenMargin = 12f;
|
||||
private const float FooterClearance = 46f;
|
||||
|
||||
private Panel _highlight;
|
||||
private PanelContainer _card;
|
||||
private Label _title;
|
||||
private RichTextLabel _body;
|
||||
private Label _footer;
|
||||
|
||||
/// <summary>The annotated control currently explained by the card.</summary>
|
||||
public Control CurrentTarget { get; private set; }
|
||||
|
||||
/// <summary>Whether the card stays on its target after the pointer moves away.</summary>
|
||||
public bool IsPinned { get; private set; }
|
||||
|
||||
/// <summary>Associates a control with a mod_manager/hint_* locale pair.</summary>
|
||||
public static void Attach(Control control, string hintKey)
|
||||
{
|
||||
control?.SetMeta(HintKeyMetadata, hintKey);
|
||||
}
|
||||
|
||||
protected override void Construct()
|
||||
{
|
||||
MouseFilter = MouseFilterEnum.Stop;
|
||||
MouseFilter = MouseFilterEnum.Ignore;
|
||||
|
||||
_title = GetNode<Label>("Center/Frame/Margin/Column/Header/Title");
|
||||
_body = GetNode<RichTextLabel>("Center/Frame/Margin/Column/Body");
|
||||
_highlight = GetNode<Panel>("Highlight");
|
||||
_card = GetNode<PanelContainer>("Card");
|
||||
_title = GetNode<Label>("Card/Margin/Column/Title");
|
||||
_body = GetNode<RichTextLabel>("Card/Margin/Column/Body");
|
||||
_footer = GetNode<Label>("Footer/Margin/FooterText");
|
||||
|
||||
var close = GetNode<Button>("Center/Frame/Margin/Column/Header/Close");
|
||||
close.Text = TranslationServer.Translate("mod_manager/help_close");
|
||||
close.TooltipText = TranslationServer.Translate("mod_manager/help_close");
|
||||
close.Pressed += Close;
|
||||
_highlight.AddThemeStyleboxOverride("panel", UiKit.Frame(
|
||||
UiTokens.RadiusSm, UiTokens.Alpha(UiTokens.Accent, 0.025f), UiTokens.AccentLineStrong));
|
||||
_card.AddThemeStyleboxOverride("panel", UiKit.Frame(
|
||||
UiTokens.RadiusSm, UiTokens.TooltipBg, UiTokens.AccentLine));
|
||||
GetNode<PanelContainer>("Footer").AddThemeStyleboxOverride("panel", UiKit.Frame(
|
||||
0, UiTokens.Surface0, UiTokens.Line));
|
||||
_title.AddThemeColorOverride("font_color", UiTokens.InkBright);
|
||||
_body.AddThemeColorOverride("default_color", UiTokens.Body);
|
||||
_footer.AddThemeColorOverride("font_color", UiTokens.Faint);
|
||||
|
||||
var dim = GetNode<ColorRect>("Dim");
|
||||
dim.GuiInput += OnDimGuiInput;
|
||||
_footer.Text = TranslationServer.Translate("mod_manager/help_active");
|
||||
_highlight.Visible = false;
|
||||
_card.Visible = false;
|
||||
}
|
||||
|
||||
/// <summary>Shows the page of one hub level; the level names match the test driver's: mods, mod_page, collections, scheme.</summary>
|
||||
public void Open(string level)
|
||||
/// <summary>Enables inspection without choosing a page or blocking the hub.</summary>
|
||||
public void Open()
|
||||
{
|
||||
_title.Text = TranslationServer.Translate($"mod_manager/help_{level}_title");
|
||||
_body.Text = TranslationServer.Translate($"mod_manager/help_{level}_body");
|
||||
Visible = true;
|
||||
IsPinned = false;
|
||||
CurrentTarget = null;
|
||||
_highlight.Visible = false;
|
||||
_card.Visible = false;
|
||||
UpdateHoveredTarget();
|
||||
}
|
||||
|
||||
/// <summary>Hides the overlay; the hub calls this from its Escape chain and the driver checks it.</summary>
|
||||
/// <summary>Leaves help mode and removes every help-only visual.</summary>
|
||||
public void Close()
|
||||
{
|
||||
Visible = false;
|
||||
IsPinned = false;
|
||||
CurrentTarget = null;
|
||||
if (GodotObject.IsInstanceValid(_highlight))
|
||||
_highlight.Visible = false;
|
||||
if (GodotObject.IsInstanceValid(_card))
|
||||
_card.Visible = false;
|
||||
}
|
||||
|
||||
private void OnDimGuiInput(InputEvent @event)
|
||||
/// <summary>
|
||||
/// Explains and pins one annotated control. Also used by F1 so keyboard users
|
||||
/// get the same stable card as pointer users get after a click.
|
||||
/// </summary>
|
||||
public void ShowFor(Control target)
|
||||
{
|
||||
if (@event is InputEventMouseButton mouse && mouse.Pressed)
|
||||
Close();
|
||||
Control annotated = FindTarget(target);
|
||||
if (annotated == null)
|
||||
return;
|
||||
|
||||
Visible = true;
|
||||
IsPinned = true;
|
||||
SetTarget(annotated);
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
if (!Visible)
|
||||
return;
|
||||
|
||||
if (IsPinned)
|
||||
{
|
||||
if (!GodotObject.IsInstanceValid(CurrentTarget) || !CurrentTarget.IsVisibleInTree())
|
||||
{
|
||||
IsPinned = false;
|
||||
CurrentTarget = null;
|
||||
_highlight.Visible = false;
|
||||
_card.Visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
LayoutAroundTarget();
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateHoveredTarget();
|
||||
}
|
||||
|
||||
public override void _Input(InputEvent @event)
|
||||
{
|
||||
if (!Visible || @event is not InputEventMouseButton mouse
|
||||
|| !mouse.Pressed || mouse.ButtonIndex != MouseButton.Left)
|
||||
return;
|
||||
|
||||
Control target = FindTarget(GetViewport().GuiGetHoveredControl());
|
||||
if (target != null)
|
||||
{
|
||||
IsPinned = true;
|
||||
SetTarget(target);
|
||||
}
|
||||
else
|
||||
{
|
||||
IsPinned = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateHoveredTarget()
|
||||
{
|
||||
Control target = FindTarget(GetViewport().GuiGetHoveredControl());
|
||||
SetTarget(target);
|
||||
}
|
||||
|
||||
private Control FindTarget(Control control)
|
||||
{
|
||||
Node node = control;
|
||||
while (node != null && node != this)
|
||||
{
|
||||
if (node is Control candidate
|
||||
&& candidate.IsVisibleInTree()
|
||||
&& candidate.HasMeta(HintKeyMetadata))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
|
||||
node = node.GetParent();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void SetTarget(Control target)
|
||||
{
|
||||
if (CurrentTarget == target)
|
||||
{
|
||||
if (target != null)
|
||||
LayoutAroundTarget();
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentTarget = target;
|
||||
bool hasTarget = target != null;
|
||||
_highlight.Visible = hasTarget;
|
||||
_card.Visible = hasTarget;
|
||||
if (!hasTarget)
|
||||
return;
|
||||
|
||||
string key = target.GetMeta(HintKeyMetadata).AsString();
|
||||
if (target.HasMeta(HintTranslationMetadata))
|
||||
key = target.GetMeta(HintTranslationMetadata).AsString();
|
||||
_title.Text = TranslationServer.Translate($"mod_manager/hint_{key}_title");
|
||||
_body.Text = TranslationServer.Translate($"mod_manager/hint_{key}_body");
|
||||
_card.ResetSize();
|
||||
LayoutAroundTarget();
|
||||
}
|
||||
|
||||
private void LayoutAroundTarget()
|
||||
{
|
||||
if (CurrentTarget == null || !CurrentTarget.IsVisibleInTree())
|
||||
return;
|
||||
|
||||
_highlight.GlobalPosition = CurrentTarget.GlobalPosition - new Vector2(3, 3);
|
||||
_highlight.Size = CurrentTarget.Size + new Vector2(6, 6);
|
||||
|
||||
Vector2 viewport = GetViewportRect().Size;
|
||||
Vector2 cardSize = _card.GetCombinedMinimumSize();
|
||||
_card.Size = cardSize;
|
||||
|
||||
Rect2 target = new(CurrentTarget.GlobalPosition, CurrentTarget.Size);
|
||||
float x = Mathf.Clamp(target.GetCenter().X - cardSize.X / 2f,
|
||||
ScreenMargin, Mathf.Max(ScreenMargin, viewport.X - cardSize.X - ScreenMargin));
|
||||
float y = target.End.Y + Gap;
|
||||
float bottomLimit = viewport.Y - FooterClearance;
|
||||
|
||||
if (y + cardSize.Y > bottomLimit)
|
||||
y = target.Position.Y - cardSize.Y - Gap;
|
||||
if (y < ScreenMargin)
|
||||
{
|
||||
y = Mathf.Clamp(target.GetCenter().Y - cardSize.Y / 2f,
|
||||
ScreenMargin, Mathf.Max(ScreenMargin, bottomLimit - cardSize.Y));
|
||||
x = target.End.X + Gap;
|
||||
if (x + cardSize.X > viewport.X - ScreenMargin)
|
||||
x = target.Position.X - cardSize.X - Gap;
|
||||
}
|
||||
|
||||
x = Mathf.Clamp(x, ScreenMargin, Mathf.Max(ScreenMargin, viewport.X - cardSize.X - ScreenMargin));
|
||||
_card.GlobalPosition = new Vector2(x, Mathf.Max(ScreenMargin, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user