From 87517becfd93449b6c853d08654271d54d4c00f5 Mon Sep 17 00:00:00 2001 From: Jerric <58122205+Ivinedra@users.noreply.github.com> Date: Sat, 7 Feb 2026 08:55:39 -0500 Subject: [PATCH 01/10] World Marker Categories v 1 Add categorized, collapsible grouping for world marker UI and settings. Introduces WorldMarkerCategoryDefinitions (new) to define category order and assignments, updates MarkerControlWidget and SettingsWindowMarkerSettingsModule to build grouped UI from the registry, and adds collapsible group behavior and label prefixes to MenuPopup. Also updates templates/styles (markers_module.xml, _popup_menu.xml) and i18n entries for category labels and some marker names to support the new grouping and UI. These changes reorganize marker lists to improve discoverability and allow collapsing less-used groups. --- Umbra/i18n/de.json | 12 +- Umbra/i18n/en.json | 12 +- Umbra/i18n/fr.json | 12 +- Umbra/i18n/ja.json | 12 +- Umbra/i18n/zh.json | 12 +- .../System/WorldMarkerCategoryDefinitions.cs | 69 +++++++++++ .../MarkerControl/MarkerControlWidget.cs | 41 ++++--- .../System/Popup/MenuPopup.Components.cs | 56 ++++++++- .../SettingsWindowMarkerSettingsModule.cs | 108 ++++++++++++++---- Umbra/udt/umbra/widgets/_popup_menu.xml | 9 ++ .../settings/modules/markers_module.xml | 16 ++- 11 files changed, 296 insertions(+), 63 deletions(-) create mode 100644 Umbra/src/Markers/System/WorldMarkerCategoryDefinitions.cs diff --git a/Umbra/i18n/de.json b/Umbra/i18n/de.json index af66871b..2afbbb4c 100644 --- a/Umbra/i18n/de.json +++ b/Umbra/i18n/de.json @@ -53,17 +53,17 @@ "Markers.PartyMembers.Config.ShowName.Description": "Ob die Namen der Gruppenmitglieder auf den Weltmarkierungen angezeigt werden sollen.", "Markers.PartyMembers.Config.IconType.Name": "Job-Symboltyp", "Markers.PartyMembers.Config.IconType.Description": "Der Symboltyp, der auf dem Weltmarker verwendet wird. Nur anwendbar, wenn 'Gruppenmitgliedersymbole anzeigen' aktiviert ist.", - "Markers.EurekaCoffers.Name": "Eureka Glückshasen-Schatztruhen", + "Markers.EurekaCoffers.Name": "Hasen-Schatztruhen", "Markers.EurekaCoffers.Description": "Zeigt Weltmarkierungen für die Glückshasen-Schatztruhen in den Eureka-Zonen an, nachdem du die Wünschelmöhre verwendet hast.", "Markers.EurekaCoffers.EnableMapMarkers.Name": "Kartenmarkierungen für erkannte Kisten aktivieren", "Markers.EurekaCoffers.EnableMapMarkers.Description": "Fügt Kartenmarkierungen für erkannte Eureka Glückshasen-Schatztruhen-Standorte hinzu. Beachte, dass diese Funktion nicht verfügbar ist, wenn das Eureka Linker-Plugin ebenfalls installiert und aktiviert ist, da es ähnliche Funktionen bietet.", - "Markers.OccultCoffers.Name": "Kreszentia Topf-Schatzkisten", + "Markers.OccultCoffers.Name": "Topf-Schatzkisten", "Markers.OccultCoffers.Description": "Zeigt Weltmarkierungen für die Topf-Schatzkisten in den Kreszentia-Zonen an, nachdem du das Wunderelixier verwendet hast.", "Markers.OccultCoffers.EnableMapMarkers.Name": "Kartenmarkierungen für entdeckte Kisten aktivieren", "Markers.OccultCoffers.EnableMapMarkers.Description": "Fügt Kartenmarkierungen für entdeckte Kreszentia Topf-Schatzkisten-Standorte hinzu. Beachte, dass diese Funktion nicht verfügbar ist, wenn das Eureka Linker Plugin ebenfalls installiert und aktiviert ist, da es eine ähnliche Funktionalität bietet.", - "Markers.Occult.SurveyPoints.Name": "Kreszentia-Vermessungspunkte", + "Markers.Occult.SurveyPoints.Name": "Vermessungspunkte", "Markers.Occult.SurveyPoints.Description": "Zeigt Weltmarkierungen für die Kreszentia-Vermessungspunkte im aktuellen Gebiet an. Beachte, dass Markierungen dieser Art nur angezeigt werden, wenn sich der Spieler innerhalb von 100 Yalmen um den Punkt befindet.", - "Markers.OccultCarrot.Name": "Kreszentia-Karotten", + "Markers.OccultCarrot.Name": "Karotten", "Markers.OccultCarrot.Description": "Zeigt Weltmarkierungen für die angeknabberten Karotten an, die in Kreszentia gefunden werden können.", "Markers.OccultCarrot.ItemName": "Karotte", "Markers.Fate.Name": "FATE-Markierungen", @@ -116,6 +116,10 @@ "Markers.Vista.Description": "Zeigt Weltmarkierungen für die Sehenswürdigkeiten (Eorzea Incognita) in der aktuellen Zone an, die noch nicht abgeschlossen sind.", "Markers.Waymark.Name": "Wegmarkierungsmarken", "Markers.Waymark.Description": "Zeigt Weltmarkierungen für die platzierten Wegmarken in der aktuellen Zone an.", + "Markers.Category.General": "Allgemein", + "Markers.Category.OpenWorld": "Offene Welt", + "Markers.Category.Eureka": "Eureka", + "Markers.Category.OccultCrescent": "Kreszentia", "Settings.Window.Title": "Umbra-Einstellungen", "Settings.Window.RestartUmbra": "Umbra neu starten", "Settings.Window.RunOOBE": "Installationsprogramm erneut ausführen", diff --git a/Umbra/i18n/en.json b/Umbra/i18n/en.json index e6113dad..c7cb4350 100644 --- a/Umbra/i18n/en.json +++ b/Umbra/i18n/en.json @@ -53,17 +53,17 @@ "Markers.PartyMembers.Config.ShowName.Description": "Whether to show the party member names on the world markers.", "Markers.PartyMembers.Config.IconType.Name": "Job icon type", "Markers.PartyMembers.Config.IconType.Description": "The type of icon that is used on the world marker. Only applicable if 'Show party member icons' is enabled.", - "Markers.EurekaCoffers.Name": "Eureka Bunny Coffers", + "Markers.EurekaCoffers.Name": "Bunny Coffers", "Markers.EurekaCoffers.Description": "Shows world markers for the Bunny Coffers in the Eureka zones after you've used the Lucky Carrot item.", "Markers.EurekaCoffers.EnableMapMarkers.Name": "Enable map markers for detected coffers", "Markers.EurekaCoffers.EnableMapMarkers.Description": "Adds map markers for detected Eureka Bunny Coffer locations. Note that this feature is unavailable if the Eureka Linker plugin is also installed and enabled, since it also provides similar functionality.", - "Markers.OccultCoffers.Name": "Occult Crescent Pot Coffers", + "Markers.OccultCoffers.Name": "Pot Coffers", "Markers.OccultCoffers.Description": "Shows world markers for the Pot Coffers in the Occult Crescent zones after you've used the Magical Elixir item.", "Markers.OccultCoffers.EnableMapMarkers.Name": "Enable map markers for detected coffers", "Markers.OccultCoffers.EnableMapMarkers.Description": "Adds map markers for detected Occult Crescent Pot Coffer locations. Note that this feature is unavailable if the Eureka Linker plugin is also installed and enabled, since it also provides similar functionality.", - "Markers.Occult.SurveyPoints.Name": "Occult Survey Points", + "Markers.Occult.SurveyPoints.Name": "Survey Points", "Markers.Occult.SurveyPoints.Description": "Shows world markers for the occult survey points in the current zone. Note that markers of this type are only drawn when the player is within 100 yalms of the survey point.", - "Markers.OccultCarrot.Name": "Occult Crescent Carrots", + "Markers.OccultCarrot.Name": "Carrots", "Markers.OccultCarrot.Description": "Shows world markers for the half-eaten Carrots you can find in the Occult Crescent", "Markers.OccultCarrot.ItemName": "Carrot", "Markers.Fate.Name": "FATE Markers", @@ -116,6 +116,10 @@ "Markers.Vista.Description": "Shows world markers for the sightseeing log vistas in the current zone that are not yet completed.", "Markers.Waymark.Name": "Waymark Markers", "Markers.Waymark.Description": "Shows world markers for the placed waymarks in the current zone.", + "Markers.Category.General": "General", + "Markers.Category.OpenWorld": "Open World", + "Markers.Category.Eureka": "Eureka", + "Markers.Category.OccultCrescent": "Occult Crescent", "Settings.Window.Title": "Umbra Settings", "Settings.Window.RestartUmbra": "Restart Umbra", "Settings.Window.RunOOBE": "Run the installer again", diff --git a/Umbra/i18n/fr.json b/Umbra/i18n/fr.json index eb0ce3b3..8afc3345 100644 --- a/Umbra/i18n/fr.json +++ b/Umbra/i18n/fr.json @@ -53,17 +53,17 @@ "Markers.PartyMembers.Config.ShowName.Description": "Afficher les noms des membres du groupe sur les marqueurs du monde.", "Markers.PartyMembers.Config.IconType.Name": "Type d'icône de travail", "Markers.PartyMembers.Config.IconType.Description": "Le type d'icône utilisé sur le marqueur mondial. Applicable uniquement si 'Afficher les icônes des membres du groupe' est activé.", - "Markers.EurekaCoffers.Name": "Coffres de lapin d'Eureka", + "Markers.EurekaCoffers.Name": "Coffres de lapin", "Markers.EurekaCoffers.Description": "Affiche les marqueurs flottants pour les Coffres de lapin dans les zones d'Eureka après avoir utilisé l'objet Carotte porte-bonheur.", "Markers.EurekaCoffers.EnableMapMarkers.Name": "Activer les marqueurs de carte pour les coffres détectés", "Markers.EurekaCoffers.EnableMapMarkers.Description": "Ajoute des marqueurs de carte pour les emplacements des Coffres de lapin d'Eureka détectés. Notez que cette fonctionnalité n'est pas disponible si le plugin Eureka Linker est également installé et activé, car il offre également une fonctionnalité similaire.", - "Markers.OccultCoffers.Name": "Coffres au trésor en pot de l'île de Lunule", + "Markers.OccultCoffers.Name": "Coffres au trésor en pot", "Markers.OccultCoffers.Description": "Affiche les marqueurs mondiaux pour les coffres au trésor en pot dans les zones de l'île de Lunule après avoir utilisé l'objet Élixir miraculeux.", "Markers.OccultCoffers.EnableMapMarkers.Name": "Activer les marqueurs de carte pour les coffres détectés", "Markers.OccultCoffers.EnableMapMarkers.Description": "Ajoute des marqueurs de carte pour les emplacements détectés des coffres au trésor en pot de l'île de Lunule. Notez que cette fonctionnalité n'est pas disponible si le plugin Eureka Linker est également installé et activé, car il fournit également une fonctionnalité similaire.", - "Markers.Occult.SurveyPoints.Name": "Points d'observation de l'île de Lunule", + "Markers.Occult.SurveyPoints.Name": "Points d'observation", "Markers.Occult.SurveyPoints.Description": "Affiche des marqueurs pour les points d'observation de l'île de Lunule dans la zone actuelle. Notez que ces marqueurs ne s'affichent que lorsque le joueur se trouve à moins de 100 yalms du point.", - "Markers.OccultCarrot.Name": "Carottes de l'île de Lunule", + "Markers.OccultCarrot.Name": "Carottes", "Markers.OccultCarrot.Description": "Affiche des marqueurs pour les carottes entamées que vous pouvez trouver sur l'île de Lunule.", "Markers.OccultCarrot.ItemName": "Carrote", "Markers.Fate.Name": "Marqueurs d'ALÉAs", @@ -116,6 +116,10 @@ "Markers.Vista.Description": "Affiche des marqueurs dans le monde pour les lieux notoires du carnet d'exploration de la zone actuelle qui n'ont pas encore été accomplis.", "Markers.Waymark.Name": "Marqueurs de repère", "Markers.Waymark.Description": "Affiche les marqueurs mondiaux pour les repères placés dans la zone actuelle.", + "Markers.Category.General": "Général", + "Markers.Category.OpenWorld": "Monde ouvert", + "Markers.Category.Eureka": "Eureka", + "Markers.Category.OccultCrescent": "De l'île de Lunule", "Settings.Window.Title": "Paramètres d'Umbra", "Settings.Window.RestartUmbra": "Redémarrer Umbra", "Settings.Window.RunOOBE": "Relancer l'installation", diff --git a/Umbra/i18n/ja.json b/Umbra/i18n/ja.json index 66de15ff..ff8e48b4 100644 --- a/Umbra/i18n/ja.json +++ b/Umbra/i18n/ja.json @@ -53,17 +53,17 @@ "Markers.PartyMembers.Config.ShowName.Description": "ワールドマーカーにパーティーメンバーの名前を表示するかどうか。", "Markers.PartyMembers.Config.IconType.Name": "ジョブアイコンタイプ", "Markers.PartyMembers.Config.IconType.Description": "ワールドマーカーに使用されるアイコンの種類。「パーティメンバーアイコンを表示」が有効な場合にのみ適用されます。", - "Markers.EurekaCoffers.Name": "エウレカ・バニー宝箱", + "Markers.EurekaCoffers.Name": "バニー宝箱", "Markers.EurekaCoffers.Description": "ラッキーキャロットアイテムを使用した後、エウレカゾーンのバニー宝箱のワールドマーカーを表示します。", "Markers.EurekaCoffers.EnableMapMarkers.Name": "検出された宝箱の地図マーカーを有効にする", "Markers.EurekaCoffers.EnableMapMarkers.Description": "検出されたエウレカ・バニー宝箱の場所に地図マーカーを追加します。エウレカリンクプラグインがインストールされ、有効になっている場合、この機能は利用できませんのでご注意ください。同様の機能も提供します。", - "Markers.OccultCoffers.Name": "クレセントアイルの壺の宝箱", + "Markers.OccultCoffers.Name": "壺の宝箱", "Markers.OccultCoffers.Description": "ミラクルエリクサーを使用した後、クレセントアイルのゾーンにある壺の宝箱のワールドマーカーを表示します。", "Markers.OccultCoffers.EnableMapMarkers.Name": "検出された宝箱のマップマーカーを有効にする", "Markers.OccultCoffers.EnableMapMarkers.Description": "検出されたクレセントアイルの壺の宝箱の場所にマップマーカーを追加します。Eureka Linkerプラグインもインストールされて有効になっている場合、同様の機能を提供するため、この機能は利用できません。", - "Markers.Occult.SurveyPoints.Name": "クレセントアイルの調査地点", + "Markers.Occult.SurveyPoints.Name": "調査地点", "Markers.Occult.SurveyPoints.Description": "現在のエリアにあるクレセントアイルの調査地点のワールドマーカーを表示します。このタイプのマーカーは、プレイヤーが調査地点から100ヤルム以内にいる場合にのみ描画されることに注意してください。", - "Markers.OccultCarrot.Name": "クレセントアイルのニンジン", + "Markers.OccultCarrot.Name": "ニンジン", "Markers.OccultCarrot.Description": "クレセントアイルにある食べかけのニンジンのワールドマーカーを表示します。", "Markers.OccultCarrot.ItemName": "ニンジン", "Markers.Fate.Name": "FATEマーカー", @@ -116,6 +116,10 @@ "Markers.Vista.Description": "現在のゾーンでまだ完了していない観光ログのビスタのワールドマーカーを表示します。", "Markers.Waymark.Name": "ウェイマークマーカー", "Markers.Waymark.Description": "現在のゾーンに配置されたウェイマークの世界マーカーを表示します。", + "Markers.Category.General": "一般", + "Markers.Category.OpenWorld": "オープンワールド", + "Markers.Category.Eureka": "エウレカ", + "Markers.Category.OccultCrescent": "クレセントアイルの", "Settings.Window.Title": "Umbra設定", "Settings.Window.RestartUmbra": "Umbraを再起動", "Settings.Window.RunOOBE": "インストーラーをもう一度実行する", diff --git a/Umbra/i18n/zh.json b/Umbra/i18n/zh.json index 1602e51f..f5e87683 100644 --- a/Umbra/i18n/zh.json +++ b/Umbra/i18n/zh.json @@ -53,17 +53,17 @@ "Markers.PartyMembers.Config.ShowName.Description": "是否在世界标记上显示队员姓名。", "Markers.PartyMembers.Config.IconType.Name": "职位图标类型", "Markers.PartyMembers.Config.IconType.Description": "世界标记上使用的图标类型。仅当启用“显示队员图标”时适用。", - "Markers.EurekaCoffers.Name": "优雷卡幸福兔箱子", + "Markers.EurekaCoffers.Name": "幸福兔箱子", "Markers.EurekaCoffers.Description": "使用胡萝卜后在地图上显示兔子箱子的位置", "Markers.EurekaCoffers.EnableMapMarkers.Name": "显示检测到的优雷卡幸福兔箱子", "Markers.EurekaCoffers.EnableMapMarkers.Description": "标记检测到的优雷卡幸福兔箱子位置。若安装并启用了功能相近的Eureka Linker插件,该功能自动禁用。", - "Markers.OccultCoffers.Name": "诡月陶罐宝箱", + "Markers.OccultCoffers.Name": "陶罐宝箱", "Markers.OccultCoffers.Description": "使用魔法药剂物品后,显示诡月区域中陶罐宝箱的世界标记。", "Markers.OccultCoffers.EnableMapMarkers.Name": "为探测到的宝箱启用地图标记", "Markers.OccultCoffers.EnableMapMarkers.Description": "为探测到的诡月陶罐宝箱位置添加地图标记。请注意,如果Eureka Linker插件也已安装并启用,则此功能不可用,因为它也提供类似的功能。", - "Markers.Occult.SurveyPoints.Name": "诡月调查点", + "Markers.Occult.SurveyPoints.Name": "调查点", "Markers.Occult.SurveyPoints.Description": "显示当前区域中诡月调查点的世界标记。请注意,此类标记仅在玩家位于调查点 100 码范围内时才会显示。", - "Markers.OccultCarrot.Name": "诡月胡萝卜", + "Markers.OccultCarrot.Name": "胡萝卜", "Markers.OccultCarrot.Description": "显示在诡月区域中可以找到的半截胡萝卜的世界标记。", "Markers.OccultCarrot.ItemName": "胡萝卜", "Markers.Fate.Name": "FATE标记", @@ -116,6 +116,10 @@ "Markers.Vista.Description": "显示当前区域中尚未完成的探索笔记景点的世界标记。", "Markers.Waymark.Name": "场景标记", "Markers.Waymark.Description": "显示当前区域中已放置的场景标记。", + "Markers.Category.General": "常规", + "Markers.Category.OpenWorld": "开放世界", + "Markers.Category.Eureka": "优雷卡", + "Markers.Category.OccultCrescent": "诡月", "Settings.Window.Title": "插件设置", "Settings.Window.RestartUmbra": "重启插件", "Settings.Window.RunOOBE": "重新运行设置向导", diff --git a/Umbra/src/Markers/System/WorldMarkerCategoryDefinitions.cs b/Umbra/src/Markers/System/WorldMarkerCategoryDefinitions.cs new file mode 100644 index 00000000..7cf6d36d --- /dev/null +++ b/Umbra/src/Markers/System/WorldMarkerCategoryDefinitions.cs @@ -0,0 +1,69 @@ +using Umbra.Markers; + +namespace Umbra.Markers.System; + +internal static class WorldMarkerCategoryDefinitions +{ + internal const string CategoryGeneral = "General"; + internal const string CategoryOpenWorld = "OpenWorld"; + internal const string CategoryEureka = "Eureka"; + internal const string CategoryOccultCrescent = "OccultCrescent"; + + internal static readonly IReadOnlyList CategoryOrder = [ + CategoryGeneral, + CategoryOpenWorld, + CategoryEureka, + CategoryOccultCrescent + ]; + + private static readonly IReadOnlyDictionary> CategoryAssignments = + new Dictionary> { + [CategoryGeneral] = [ + "MapLinkMarkers", + "FlagMarker", + "PartyMembers", + "QuestMarkers", + "RelicMarkers", + "TreasureCoffers", + "WaymarkWorldMarker" + ], + [CategoryOpenWorld] = [ + "AetherCurrents", + "FateMarkers", + "GatheringNodeMarkers", + "HuntMarkers", + "TripleTriadMarkers", + "Vista" + ], + [CategoryEureka] = [ + "EurekaCoffers" + ], + [CategoryOccultCrescent] = [ + "OccultCarrots", + "OccultCoffers", + "OccultSurveyPointMarkers" + ] + }; + + internal static IEnumerable<(string CategoryId, IReadOnlyList Factories)> GetCategorizedFactories( + WorldMarkerFactoryRegistry registry + ) + { + foreach (string categoryId in CategoryOrder) { + if (!CategoryAssignments.TryGetValue(categoryId, out var ids)) continue; + + List factories = []; + + foreach (string id in ids) { + factories.Add(registry.GetFactory(id)); + } + + yield return (categoryId, factories); + } + } + + internal static string GetCategoryLabel(string categoryId) + { + return I18N.Translate($"Markers.Category.{categoryId}"); + } +} diff --git a/Umbra/src/Toolbar/Widgets/Library/MarkerControl/MarkerControlWidget.cs b/Umbra/src/Toolbar/Widgets/Library/MarkerControl/MarkerControlWidget.cs index 558c5ba3..41bed4d4 100644 --- a/Umbra/src/Toolbar/Widgets/Library/MarkerControl/MarkerControlWidget.cs +++ b/Umbra/src/Toolbar/Widgets/Library/MarkerControl/MarkerControlWidget.cs @@ -1,4 +1,5 @@ -using Umbra.Markers.System; +using Umbra.Markers; +using Umbra.Markers.System; namespace Umbra.Widgets; @@ -26,28 +27,42 @@ internal class MarkerControlWidget( private WorldMarkerFactoryRegistry Registry { get; } = Framework.Service(); - private readonly Dictionary _buttons = []; + private readonly Dictionary _buttons = []; + private readonly List _groups = []; protected override void OnLoad() { - foreach (string id in Registry.GetFactoryIds()) { - var factory = Registry.GetFactory(id); - var button = new MenuPopup.Button(id) { - Icon = FontAwesomeIcon.Check, - Label = factory.Name, - Selected = true, - ClosePopupOnClick = false, - OnClick = () => factory.SetConfigValue("Enabled", !factory.GetConfigValue("Enabled")), + foreach ((string categoryId, IReadOnlyList factories) in + WorldMarkerCategoryDefinitions.GetCategorizedFactories(Registry)) { + bool isGeneral = categoryId == WorldMarkerCategoryDefinitions.CategoryGeneral; + var group = new MenuPopup.Group(WorldMarkerCategoryDefinitions.GetCategoryLabel(categoryId)) { + IsCollapsible = true, + IsCollapsed = !isGeneral }; - Popup.Add(button); - _buttons.Add(id, button); + foreach (WorldMarkerFactory factory in factories) { + var button = new MenuPopup.Button(factory.Id) { + Icon = FontAwesomeIcon.Check, + Label = factory.Name, + Selected = true, + ClosePopupOnClick = false, + OnClick = () => factory.SetConfigValue("Enabled", !factory.GetConfigValue("Enabled")), + }; + + group.Add(button); + _buttons.Add(factory.Id, button); + } + + _groups.Add(group); + Popup.Add(group); } } protected override void OnUnload() { - _buttons.Clear(); + _buttons.Clear(); + _groups.Clear(); + Popup.Clear(true); } protected override void OnDraw() diff --git a/Umbra/src/Toolbar/Widgets/System/Popup/MenuPopup.Components.cs b/Umbra/src/Toolbar/Widgets/System/Popup/MenuPopup.Components.cs index 0458e91c..e5e3fe3c 100644 --- a/Umbra/src/Toolbar/Widgets/System/Popup/MenuPopup.Components.cs +++ b/Umbra/src/Toolbar/Widgets/System/Popup/MenuPopup.Components.cs @@ -207,10 +207,38 @@ public class Group : IMenuItem, IDisposable public event Action? OnButtonClicked; private readonly Dictionary _items = []; + private bool _isCollapsed; + private bool _isCollapsible; + private string _baseLabel = string.Empty; public string? Label { - get => LabelNode.NodeValue?.ToString() ?? string.Empty; - set => LabelNode.NodeValue = value; + get => _baseLabel; + set + { + _baseLabel = value ?? string.Empty; + UpdateHeaderLabel(); + } + } + + public bool IsCollapsed { + get => _isCollapsed; + set + { + _isCollapsed = value; + ContentNode.Style.IsVisible = !_isCollapsed; + Node.ToggleClass("collapsed", _isCollapsed); + UpdateHeaderLabel(); + } + } + + public bool IsCollapsible { + get => _isCollapsible; + set + { + _isCollapsible = value; + Node.ToggleClass("collapsible", value); + UpdateHeaderLabel(); + } } public int SortIndex { @@ -242,12 +270,32 @@ public Group(string label) Label = label; Node.BeforeDraw += _ => { - Node.Style.IsVisible = ContentNode.ChildNodes.Any(c => c.IsVisible); - HeaderNode.Style.IsVisible = !string.IsNullOrEmpty(Label); + bool hasVisibleContent = ContentNode.ChildNodes.Any(c => c.IsVisible); + bool hasHeader = !string.IsNullOrEmpty(Label); + + HeaderNode.Style.IsVisible = hasHeader; + ContentNode.Style.IsVisible = !IsCollapsed; + Node.Style.IsVisible = hasHeader || (!IsCollapsed && hasVisibleContent); + }; + + HeaderNode.OnClick += _ => { + if (!IsCollapsible) return; + IsCollapsed = !IsCollapsed; }; Node.OnDispose += _ => Dispose(); } + + private void UpdateHeaderLabel() + { + if (!IsCollapsible) { + LabelNode.NodeValue = _baseLabel; + return; + } + + string prefix = IsCollapsed ? "▶ " : "▼ "; + LabelNode.NodeValue = $"{prefix}{_baseLabel}"; + } public void Add(IMenuItem item) { diff --git a/Umbra/src/Windows/Library/Settings/Modules/SettingsWindowMarkerSettingsModule.cs b/Umbra/src/Windows/Library/Settings/Modules/SettingsWindowMarkerSettingsModule.cs index cc1366a2..1143ab07 100644 --- a/Umbra/src/Windows/Library/Settings/Modules/SettingsWindowMarkerSettingsModule.cs +++ b/Umbra/src/Windows/Library/Settings/Modules/SettingsWindowMarkerSettingsModule.cs @@ -1,4 +1,3 @@ -using Microsoft.Win32; using Umbra.Markers; using Umbra.Markers.System; @@ -14,32 +13,41 @@ public class SettingsWindowMarkerSettingsModule : SettingsWindowModule private WorldMarkerFactoryRegistry Registry { get; } = Framework.Service(); private readonly Dictionary _buttons = []; + private readonly Dictionary _categoryGroups = []; + private readonly Dictionary _markerCategories = []; protected override void OnOpen() { Node targetNode = RootNode.QuerySelector("#sidebar-buttons")!; - List factories = GetSortedFactories(); - - foreach (var factory in factories) { - Node button = new() { - ClassList = ["tab-button"], - NodeValue = factory.Name, - }; - - button.OnClick += OnTabButtonClicked; - _buttons.Add(factory.Id, button); - - targetNode.AppendChild(button); + foreach ((string categoryId, IReadOnlyList factories) in + WorldMarkerCategoryDefinitions.GetCategorizedFactories(Registry)) { + if (factories.Count == 0) continue; + + var group = CreateCategoryGroup(targetNode, categoryId); + + foreach (WorldMarkerFactory factory in factories) { + Node button = new() { + ClassList = ["tab-button"], + NodeValue = factory.Name, + }; + + button.OnClick += OnTabButtonClicked; + _buttons.Add(factory.Id, button); + _markerCategories.Add(factory.Id, categoryId); + + group.BodyNode.AppendChild(button); + } } - var firstButton = targetNode.QuerySelectorAll(".tab-button").FirstOrDefault(); - if (firstButton != null) OnTabButtonClicked(firstButton); + RootNode.QuerySelector("#main")!.Clear(); } protected override void OnClose() { _buttons.Clear(); + _categoryGroups.Clear(); + _markerCategories.Clear(); } private void OnTabButtonClicked(Node node) @@ -54,6 +62,11 @@ private void OnTabButtonClicked(Node node) if (factoryId == null) return; WorldMarkerFactory factory = Registry.GetFactory(factoryId); + + if (_markerCategories.TryGetValue(factoryId, out string? categoryId) + && _categoryGroups.TryGetValue(categoryId, out MarkerCategoryGroup? group)) { + group.IsCollapsed = false; + } Node mainNode = RootNode.QuerySelector("#main")!; Node markerEditor = Document.CreateNodeFromTemplate("marker-config", new() { @@ -188,17 +201,66 @@ private void RenderControlNode(Node targetNode, WorldMarkerFactory factory, IMar } } - private List GetSortedFactories() + private MarkerCategoryGroup CreateCategoryGroup(Node targetNode, string categoryId) { - List factories = []; - - foreach (string id in Registry.GetFactoryIds()) { - var factory = Registry.GetFactory(id); - factories.Add(factory); + Node groupNode = Document.CreateNodeFromTemplate("category-group", new() { + { "label", WorldMarkerCategoryDefinitions.GetCategoryLabel(categoryId) } + }); + + Node headerNode = groupNode.QuerySelector(".header")!; + Node bodyNode = groupNode.QuerySelector(".body")!; + + bool isGeneral = categoryId == WorldMarkerCategoryDefinitions.CategoryGeneral; + var group = new MarkerCategoryGroup(categoryId, groupNode, headerNode, bodyNode) { + IsCollapsed = !isGeneral + }; + + headerNode.OnClick += _ => group.IsCollapsed = !group.IsCollapsed; + targetNode.AppendChild(groupNode); + + _categoryGroups.Add(categoryId, group); + + return group; + } + + private sealed class MarkerCategoryGroup + { + public string CategoryId { get; } + public Node GroupNode { get; } + public Node HeaderNode { get; } + public Node BodyNode { get; } + + private bool _isCollapsed; + private string _baseLabel; + + public bool IsCollapsed { + get => _isCollapsed; + set + { + _isCollapsed = value; + BodyNode.Style.IsVisible = !_isCollapsed; + GroupNode.ToggleClass("collapsed", _isCollapsed); + UpdateHeaderLabel(); + } } - factories.Sort((a, b) => String.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase)); + public MarkerCategoryGroup(string categoryId, Node groupNode, Node headerNode, Node bodyNode) + { + CategoryId = categoryId; + GroupNode = groupNode; + HeaderNode = headerNode; + BodyNode = bodyNode; + _baseLabel = headerNode.QuerySelector(".text")?.NodeValue?.ToString() ?? string.Empty; + UpdateHeaderLabel(); + } - return factories; + private void UpdateHeaderLabel() + { + string prefix = _isCollapsed ? "▶ " : "▼ "; + Node? labelNode = HeaderNode.QuerySelector(".text"); + if (labelNode != null) { + labelNode.NodeValue = $"{prefix}{_baseLabel}"; + } + } } } diff --git a/Umbra/udt/umbra/widgets/_popup_menu.xml b/Umbra/udt/umbra/widgets/_popup_menu.xml index 5ebe4a61..a93f9414 100644 --- a/Umbra/udt/umbra/widgets/_popup_menu.xml +++ b/Umbra/udt/umbra/widgets/_popup_menu.xml @@ -33,6 +33,15 @@ } } + &.collapsible > .header { + padding: 2 4; + border-radius: 2; + + &:hover { + background-color: "Widget.PopupMenuBackgroundHover"; + } + } + & > .content { flow: vertical; auto-size: grow fit; diff --git a/Umbra/udt/umbra/windows/settings/modules/markers_module.xml b/Umbra/udt/umbra/windows/settings/modules/markers_module.xml index 6e9e0d03..8dde11dd 100644 --- a/Umbra/udt/umbra/windows/settings/modules/markers_module.xml +++ b/Umbra/udt/umbra/windows/settings/modules/markers_module.xml @@ -3,13 +3,23 @@ - - - + + +