diff --git a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs index c63bf31..e1e58a6 100644 --- a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs +++ b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs @@ -374,6 +374,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa { _needsCollectionRebuild = true; _forceFullReapply = true; + _forceApplyMods = true; } if (!releaseFromPenumbra || toRelease == Guid.Empty || !_ipcManager.Penumbra.APIAvailable) diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 0f5efac..255e99e 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -79,8 +79,69 @@ public class SettingsUi : WindowMediatorSubscriberBase private bool _lightfinderIconInputInitialized = false; private int _lightfinderIconPresetIndex = -1; private bool _selectGeneralTabOnNextDraw = false; - private bool _openLightfinderSectionOnNextDraw = false; private static readonly LightlessConfig DefaultConfig = new(); + private MainSettingsTab _selectedMainTab = MainSettingsTab.General; + private TransferSettingsTab _selectedTransferTab = TransferSettingsTab.Transfers; + private ServerSettingsTab _selectedServerTab = ServerSettingsTab.CharacterManagement; + private static readonly UiSharedService.TabOption[] MainTabOptions = new[] + { + new UiSharedService.TabOption("General", MainSettingsTab.General), + new UiSharedService.TabOption("Performance", MainSettingsTab.Performance), + new UiSharedService.TabOption("Storage", MainSettingsTab.Storage), + new UiSharedService.TabOption("Transfers", MainSettingsTab.Transfers), + new UiSharedService.TabOption("Service Settings", MainSettingsTab.ServiceSettings), + new UiSharedService.TabOption("Notifications", MainSettingsTab.Notifications), + new UiSharedService.TabOption("Debug", MainSettingsTab.Debug), + }; + private readonly UiSharedService.TabOption[] _transferTabOptions = new UiSharedService.TabOption[2]; + private readonly List> _serverTabOptions = new(4); + private readonly string[] _generalTreeNavOrder = new[] + { + "Import & Export", + "Popup & Auto Fill", + "Behavior", + "Lightfinder", + "Pair List", + "Profiles", + "Colors", + "Server Info Bar", + "Nameplate", + }; + private static readonly HashSet _generalNavSeparatorAfter = new(StringComparer.Ordinal) + { + "Popup & Auto Fill", + "Profiles", + }; + private string? _generalScrollTarget = null; + private string? _generalOpenTreeTarget = null; + private readonly Dictionary _generalTreeHighlights = new(StringComparer.Ordinal); + private const float GeneralTreeHighlightDuration = 1.5f; + private readonly SeluneBrush _generalSeluneBrush = new(); + + private enum MainSettingsTab + { + General, + Performance, + Storage, + Transfers, + ServiceSettings, + Notifications, + Debug, + } + + private enum TransferSettingsTab + { + Transfers, + BlockedTransfers, + } + + private enum ServerSettingsTab + { + CharacterManagement, + SecretKeyManagement, + ServiceConfiguration, + PermissionSettings, + } private static readonly (string Label, SeIconChar Icon)[] LightfinderIconPresets = new[] { @@ -139,8 +200,8 @@ public class SettingsUi : WindowMediatorSubscriberBase SizeConstraints = new WindowSizeConstraints() { - MinimumSize = new Vector2(850f, 400f), - MaximumSize = new Vector2(850f, 2000f), + MinimumSize = new Vector2(900f, 400f), + MaximumSize = new Vector2(900f, 2000f), }; TitleBarButtons = new() @@ -167,7 +228,7 @@ public class SettingsUi : WindowMediatorSubscriberBase { IsOpen = true; _selectGeneralTabOnNextDraw = true; - _openLightfinderSectionOnNextDraw = true; + FocusGeneralTree("Lightfinder"); }); Mediator.Subscribe(this, (_) => IsOpen = false); Mediator.Subscribe(this, (_) => UiSharedService_GposeStart()); @@ -973,89 +1034,96 @@ public class SettingsUi : WindowMediatorSubscriberBase _uiShared.UnderlinedBigText("Current Transfers", UIColors.Get("LightlessBlue")); ImGuiHelpers.ScaledDummy(5); - if (ImGui.BeginTabBar("TransfersTabBar")) + _transferTabOptions[0] = new UiSharedService.TabOption( + "Transfers", + TransferSettingsTab.Transfers, + _apiController.ServerState is ServerState.Connected); + _transferTabOptions[1] = new UiSharedService.TabOption( + "Blocked Transfers", + TransferSettingsTab.BlockedTransfers); + + UiSharedService.Tab("TransferSettingsTabs", _transferTabOptions, ref _selectedTransferTab); + ImGuiHelpers.ScaledDummy(5); + + switch (_selectedTransferTab) { - if (ApiController.ServerState is ServerState.Connected && ImGui.BeginTabItem("Transfers")) - { - var uploadsSnapshot = _fileTransferManager.GetCurrentUploadsSnapshot(); - var activeUploads = uploadsSnapshot.Count(c => !c.IsTransferred); - var uploadSlotLimit = Math.Clamp(_configService.Current.ParallelUploads, 1, 8); - ImGui.TextUnformatted($"Uploads (slots {activeUploads}/{uploadSlotLimit})"); - if (ImGui.BeginTable("UploadsTable", 3)) + case TransferSettingsTab.Transfers when _apiController.ServerState is ServerState.Connected: { - ImGui.TableSetupColumn("File"); - ImGui.TableSetupColumn("Uploaded"); - ImGui.TableSetupColumn("Size"); - ImGui.TableHeadersRow(); - foreach (var transfer in uploadsSnapshot) + var uploadsSnapshot = _fileTransferManager.GetCurrentUploadsSnapshot(); + var activeUploads = uploadsSnapshot.Count(c => !c.IsTransferred); + var uploadSlotLimit = Math.Clamp(_configService.Current.ParallelUploads, 1, 8); + ImGui.TextUnformatted($"Uploads (slots {activeUploads}/{uploadSlotLimit})"); + if (ImGui.BeginTable("UploadsTable", 3)) { - var color = UiSharedService.UploadColor((transfer.Transferred, transfer.Total)); - using var col = ImRaii.PushColor(ImGuiCol.Text, color); - ImGui.TableNextColumn(); - if (transfer is UploadFileTransfer uploadTransfer) + ImGui.TableSetupColumn("File"); + ImGui.TableSetupColumn("Uploaded"); + ImGui.TableSetupColumn("Size"); + ImGui.TableHeadersRow(); + foreach (var transfer in uploadsSnapshot) { - ImGui.TextUnformatted(uploadTransfer.LocalFile); - } - else - { - ImGui.TextUnformatted(transfer.Hash); + var color = UiSharedService.UploadColor((transfer.Transferred, transfer.Total)); + using var col = ImRaii.PushColor(ImGuiCol.Text, color); + ImGui.TableNextColumn(); + if (transfer is UploadFileTransfer uploadTransfer) + { + ImGui.TextUnformatted(uploadTransfer.LocalFile); + } + else + { + ImGui.TextUnformatted(transfer.Hash); + } + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(UiSharedService.ByteToString(transfer.Transferred)); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(UiSharedService.ByteToString(transfer.Total)); } - ImGui.TableNextColumn(); - ImGui.TextUnformatted(UiSharedService.ByteToString(transfer.Transferred)); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(UiSharedService.ByteToString(transfer.Total)); + ImGui.EndTable(); } - ImGui.EndTable(); - } - - ImGui.Separator(); - ImGui.TextUnformatted("Downloads"); - if (ImGui.BeginTable("DownloadsTable", 4)) - { - ImGui.TableSetupColumn("User"); - ImGui.TableSetupColumn("Server"); - ImGui.TableSetupColumn("Files"); - ImGui.TableSetupColumn("Download"); - ImGui.TableHeadersRow(); - - foreach (var transfer in _currentDownloads.ToArray()) + ImGui.Separator(); + ImGui.TextUnformatted("Downloads"); + if (ImGui.BeginTable("DownloadsTable", 4)) { - var userName = transfer.Key.Name; - foreach (var entry in transfer.Value) + ImGui.TableSetupColumn("User"); + ImGui.TableSetupColumn("Server"); + ImGui.TableSetupColumn("Files"); + ImGui.TableSetupColumn("Download"); + ImGui.TableHeadersRow(); + + foreach (var transfer in _currentDownloads.ToArray()) { - var color = UiSharedService.UploadColor((entry.Value.TransferredBytes, - entry.Value.TotalBytes)); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(userName); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(entry.Key); - var col = ImRaii.PushColor(ImGuiCol.Text, color); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(entry.Value.TransferredFiles + "/" + entry.Value.TotalFiles); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(UiSharedService.ByteToString(entry.Value.TransferredBytes) + "/" + - UiSharedService.ByteToString(entry.Value.TotalBytes)); - ImGui.TableNextColumn(); - col.Dispose(); - ImGui.TableNextRow(); + var userName = transfer.Key.Name; + foreach (var entry in transfer.Value) + { + var color = UiSharedService.UploadColor((entry.Value.TransferredBytes, + entry.Value.TotalBytes)); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(userName); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(entry.Key); + var col = ImRaii.PushColor(ImGuiCol.Text, color); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(entry.Value.TransferredFiles + "/" + entry.Value.TotalFiles); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( + UiSharedService.ByteToString(entry.Value.TransferredBytes) + "/" + + UiSharedService.ByteToString(entry.Value.TotalBytes)); + ImGui.TableNextColumn(); + col.Dispose(); + ImGui.TableNextRow(); + } } + + ImGui.EndTable(); } - ImGui.EndTable(); + break; } - - ImGui.EndTabItem(); - } - - if (ImGui.BeginTabItem("Blocked Transfers")) - { + case TransferSettingsTab.BlockedTransfers: DrawBlockedTransfers(); - ImGui.EndTabItem(); - } - - ImGui.EndTabBar(); + break; } } @@ -1507,72 +1575,100 @@ public class SettingsUi : WindowMediatorSubscriberBase _lastTab = "General"; + using var generalSelune = Selune.Begin(_generalSeluneBrush, ImGui.GetWindowDrawList(), ImGui.GetWindowPos(), ImGui.GetWindowSize()); + + var navAvailableWidth = ImGui.GetContentRegionAvail().X; + var minNavWidth = 80f * ImGuiHelpers.GlobalScale; + var maxNavWidth = 150f * ImGuiHelpers.GlobalScale; + var navWidth = Math.Max(minNavWidth, Math.Min(maxNavWidth, navAvailableWidth * 0.24f)); + var navHeight = MathF.Max(ImGui.GetContentRegionAvail().Y, 400f * ImGuiHelpers.GlobalScale); + var style = ImGui.GetStyle(); + + ImGui.BeginGroup(); + ImGui.BeginChild("GeneralNavigation", new Vector2(navWidth, navHeight), true); + DrawGeneralNavigation(); + ImGui.EndChild(); + ImGui.EndGroup(); + + ImGui.SameLine(0, style.ItemSpacing.X * 1.75f); + + ImGui.BeginGroup(); + ImGui.BeginChild("GeneralSettingsContent", new Vector2(0, navHeight), false); + _uiShared.UnderlinedBigText("General Settings", UIColors.Get("LightlessBlue")); ImGui.Dummy(new Vector2(10)); _uiShared.BigText("Notes"); - if (_uiShared.MediumTreeNode("Import & Export", UIColors.Get("LightlessPurple"))) + using (var importExportTree = BeginGeneralTree("Import & Export", UIColors.Get("LightlessPurple"))) { - if (_uiShared.IconTextButton(FontAwesomeIcon.StickyNote, "Export all your user notes to clipboard")) + if (importExportTree.Visible) { - var snapshot = _pairUiService.GetSnapshot(); - ImGui.SetClipboardText(UiSharedService.GetNotes(snapshot.DirectPairs - .UnionBy(snapshot.GroupPairs.SelectMany(p => p.Value), p => p.UserData, - UserDataComparer.Instance).ToList())); - } + if (_uiShared.IconTextButton(FontAwesomeIcon.StickyNote, "Export all your user notes to clipboard")) + { + var snapshot = _pairUiService.GetSnapshot(); + ImGui.SetClipboardText(UiSharedService.GetNotes(snapshot.DirectPairs + .UnionBy(snapshot.GroupPairs.SelectMany(p => p.Value), p => p.UserData, + UserDataComparer.Instance).ToList())); + } - if (_uiShared.IconTextButton(FontAwesomeIcon.FileImport, "Import notes from clipboard")) - { - _notesSuccessfullyApplied = null; - var notes = ImGui.GetClipboardText(); - _notesSuccessfullyApplied = _uiShared.ApplyNotesFromClipboard(notes, _overwriteExistingLabels); - } + if (_uiShared.IconTextButton(FontAwesomeIcon.FileImport, "Import notes from clipboard")) + { + _notesSuccessfullyApplied = null; + var notes = ImGui.GetClipboardText(); + _notesSuccessfullyApplied = _uiShared.ApplyNotesFromClipboard(notes, _overwriteExistingLabels); + } - ImGui.SameLine(); - ImGui.Checkbox("Overwrite existing notes", ref _overwriteExistingLabels); - _uiShared.DrawHelpText( - "If this option is selected all already existing notes for UIDs will be overwritten by the imported notes."); - if (_notesSuccessfullyApplied.HasValue && _notesSuccessfullyApplied.Value) - { - UiSharedService.ColorTextWrapped("User Notes successfully imported", UIColors.Get("LightlessBlue")); - } - else if (_notesSuccessfullyApplied.HasValue && !_notesSuccessfullyApplied.Value) - { - UiSharedService.ColorTextWrapped( - "Attempt to import notes from clipboard failed. Check formatting and try again", - ImGuiColors.DalamudRed); - } + ImGui.SameLine(); + ImGui.Checkbox("Overwrite existing notes", ref _overwriteExistingLabels); + _uiShared.DrawHelpText( + "If this option is selected all already existing notes for UIDs will be overwritten by the imported notes."); + if (_notesSuccessfullyApplied.HasValue && _notesSuccessfullyApplied.Value) + { + UiSharedService.ColorTextWrapped("User Notes successfully imported", UIColors.Get("LightlessBlue")); + } + else if (_notesSuccessfullyApplied.HasValue && !_notesSuccessfullyApplied.Value) + { + UiSharedService.ColorTextWrapped( + "Attempt to import notes from clipboard failed. Check formatting and try again", + ImGuiColors.DalamudRed); + } - UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); - ImGui.TreePop(); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + ImGui.TreePop(); + importExportTree.MarkContentEnd(); + } } ImGui.Separator(); var openPopupOnAddition = _configService.Current.OpenPopupOnAdd; - if (_uiShared.MediumTreeNode("Popup & Auto Fill", UIColors.Get("LightlessPurple"))) + using (var popupTree = BeginGeneralTree("Popup & Auto Fill", UIColors.Get("LightlessPurple"))) { - if (ImGui.Checkbox("Open Notes Popup on user addition", ref openPopupOnAddition)) + if (popupTree.Visible) { - _configService.Current.OpenPopupOnAdd = openPopupOnAddition; - _configService.Save(); + if (ImGui.Checkbox("Open Notes Popup on user addition", ref openPopupOnAddition)) + { + _configService.Current.OpenPopupOnAdd = openPopupOnAddition; + _configService.Save(); + } + + _uiShared.DrawHelpText( + "This will open a popup that allows you to set the notes for a user after successfully adding them to your individual pairs."); + + var autoPopulateNotes = _configService.Current.AutoPopulateEmptyNotesFromCharaName; + if (ImGui.Checkbox("Automatically populate notes using player names", ref autoPopulateNotes)) + { + _configService.Current.AutoPopulateEmptyNotesFromCharaName = autoPopulateNotes; + _configService.Save(); + } + + _uiShared.DrawHelpText( + "This will automatically populate user notes using the first encountered player name if the note was not set prior"); + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + ImGui.TreePop(); + popupTree.MarkContentEnd(); } - - _uiShared.DrawHelpText( - "This will open a popup that allows you to set the notes for a user after successfully adding them to your individual pairs."); - - var autoPopulateNotes = _configService.Current.AutoPopulateEmptyNotesFromCharaName; - if (ImGui.Checkbox("Automatically populate notes using player names", ref autoPopulateNotes)) - { - _configService.Current.AutoPopulateEmptyNotesFromCharaName = autoPopulateNotes; - _configService.Save(); - } - - _uiShared.DrawHelpText( - "This will automatically populate user notes using the first encountered player name if the note was not set prior"); - - UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); - ImGui.TreePop(); } ImGui.Separator(); @@ -1603,66 +1699,59 @@ public class SettingsUi : WindowMediatorSubscriberBase var syncshellOfflineSeparate = _configService.Current.ShowSyncshellOfflineUsersSeparately; - if (_uiShared.MediumTreeNode("Behavior", UIColors.Get("LightlessPurple"))) + using (var behaviorTree = BeginGeneralTree("Behavior", UIColors.Get("LightlessPurple"))) { - if (ImGui.Checkbox("Enable Game Right Click Menu Entries", ref enableRightClickMenu)) + if (behaviorTree.Visible) { - _configService.Current.EnableRightClickMenus = enableRightClickMenu; - _configService.Save(); - } - - _uiShared.DrawHelpText("This will add all Lightless related right click menu entries in the game UI."); - - if (ImGui.Checkbox("Display status and visible pair count in Server Info Bar", ref enableDtrEntry)) - { - _configService.Current.EnableDtrEntry = enableDtrEntry; - _configService.Save(); - } - - _uiShared.DrawHelpText( - "This will add Lightless connection status and visible pair count in the Server Info Bar.\nYou can further configure this through your Dalamud Settings."); - - using (ImRaii.Disabled(!enableDtrEntry)) - { - using var indent = ImRaii.PushIndent(); - if (ImGui.Checkbox("Show visible character's UID in tooltip", ref showUidInDtrTooltip)) + if (ImGui.Checkbox("Enable Game Right Click Menu Entries", ref enableRightClickMenu)) { - _configService.Current.ShowUidInDtrTooltip = showUidInDtrTooltip; + _configService.Current.EnableRightClickMenus = enableRightClickMenu; _configService.Save(); } - if (ImGui.Checkbox("Prefer notes over player names in tooltip", ref preferNoteInDtrTooltip)) + _uiShared.DrawHelpText("This will add all Lightless related right click menu entries in the game UI."); + + if (ImGui.Checkbox("Display status and visible pair count in Server Info Bar", ref enableDtrEntry)) { - _configService.Current.PreferNoteInDtrTooltip = preferNoteInDtrTooltip; + _configService.Current.EnableDtrEntry = enableDtrEntry; _configService.Save(); } - } + _uiShared.DrawHelpText( + "This will add Lightless connection status and visible pair count in the Server Info Bar.\nYou can further configure this through your Dalamud Settings."); - UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); - ImGui.TreePop(); + using (ImRaii.Disabled(!enableDtrEntry)) + { + using var indent = ImRaii.PushIndent(); + if (ImGui.Checkbox("Show visible character's UID in tooltip", ref showUidInDtrTooltip)) + { + _configService.Current.ShowUidInDtrTooltip = showUidInDtrTooltip; + _configService.Save(); + } + + if (ImGui.Checkbox("Prefer notes over player names in tooltip", ref preferNoteInDtrTooltip)) + { + _configService.Current.PreferNoteInDtrTooltip = preferNoteInDtrTooltip; + _configService.Save(); + } + + } + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + ImGui.TreePop(); + behaviorTree.MarkContentEnd(); + } } ImGui.Separator(); - var forceOpenLightfinder = _openLightfinderSectionOnNextDraw; - if (_openLightfinderSectionOnNextDraw) + using (var lightfinderTree = BeginGeneralTree("Lightfinder", UIColors.Get("LightlessPurple"))) { - ImGui.SetNextItemOpen(true, ImGuiCond.Always); - } - - if (_uiShared.MediumTreeNode("Lightfinder", UIColors.Get("LightlessPurple"))) - { - if (forceOpenLightfinder) + if (lightfinderTree.Visible) { - ImGui.SetScrollHereY(); - } - - _openLightfinderSectionOnNextDraw = false; - - bool autoEnable = _configService.Current.LightfinderAutoEnableOnConnect; - var autoAlign = _configService.Current.LightfinderAutoAlign; - var offsetX = (int)_configService.Current.LightfinderLabelOffsetX; + bool autoEnable = _configService.Current.LightfinderAutoEnableOnConnect; + var autoAlign = _configService.Current.LightfinderAutoAlign; + var offsetX = (int)_configService.Current.LightfinderLabelOffsetX; var offsetY = (int)_configService.Current.LightfinderLabelOffsetY; var labelScale = _configService.Current.LightfinderLabelScale; bool showLightfinderInDtr = _configService.Current.ShowLightfinderInDtr; @@ -2102,356 +2191,542 @@ public class SettingsUi : WindowMediatorSubscriberBase UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); + lightfinderTree.MarkContentEnd(); + } } ImGui.Separator(); - if (_uiShared.MediumTreeNode("Colors", UIColors.Get("LightlessPurple"))) + using (var pairListTree = BeginGeneralTree("Pair List", UIColors.Get("LightlessPurple"))) { - ImGui.TextUnformatted("UI Theme Colors"); - - var colorNames = new[] + if (pairListTree.Visible) { - ("LightlessPurple", "Primary Purple", "Section titles and dividers"), - ("LightlessPurpleActive", "Primary Purple (Active)", "Active tabs and hover highlights"), - ("LightlessPurpleDefault", "Primary Purple (Inactive)", "Inactive tabs and default dividers"), - ("LightlessBlue", "Secondary Blue", "Secondary title colors, visable pairs"), - ("LightlessGreen", "Success Green", "Join buttons and success messages"), - ("LightlessYellow", "Warning Yellow", "Warning colors"), - ("LightlessOrange", "Performance Orange", "Performance notifications and warnings"), - ("PairBlue", "Syncshell Blue", "Syncshell headers, toggle highlights, and moderator actions"), - ("DimRed", "Error Red", "Error and offline colors") - }; - if (ImGui.BeginTable("##ColorTable", 3, - ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit)) - { - ImGui.TableSetupColumn("Color", ImGuiTableColumnFlags.WidthFixed); - ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("Reset", ImGuiTableColumnFlags.WidthFixed, 40); - ImGui.TableHeadersRow(); - - foreach (var (colorKey, displayName, description) in colorNames) + if (ImGui.Checkbox("Show separate Visible group", ref showVisibleSeparate)) { - ImGui.TableNextRow(); + _configService.Current.ShowVisibleUsersSeparately = showVisibleSeparate; + _configService.Save(); + Mediator.Publish(new RefreshUiMessage()); + } - // color column - ImGui.TableSetColumnIndex(0); - var currentColor = UIColors.Get(colorKey); - var colorToEdit = currentColor; - if (ImGui.ColorEdit4($"##color_{colorKey}", ref colorToEdit, - ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf)) + _uiShared.DrawHelpText( + "This will show all currently visible users in a special 'Visible' group in the main UI."); + + using (ImRaii.Disabled(!showVisibleSeparate)) + { + using var indent = ImRaii.PushIndent(); + if (ImGui.Checkbox("Show Syncshell Users in Visible Group", ref groupInVisible)) { - UIColors.Set(colorKey, colorToEdit); + _configService.Current.ShowSyncshellUsersInVisible = groupInVisible; + _configService.Save(); + Mediator.Publish(new RefreshUiMessage()); } + } - ImGui.SameLine(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(displayName); + if (ImGui.Checkbox("Show separate Offline group", ref showOfflineSeparate)) + { + _configService.Current.ShowOfflineUsersSeparately = showOfflineSeparate; + _configService.Save(); + Mediator.Publish(new RefreshUiMessage()); + } - // description column - ImGui.TableSetColumnIndex(1); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(description); + _uiShared.DrawHelpText( + "This will show all currently offline users in a special 'Offline' group in the main UI."); - // actions column - ImGui.TableSetColumnIndex(2); - using var resetId = ImRaii.PushId($"Reset_{colorKey}"); - var availableWidth = ImGui.GetContentRegionAvail().X; - var isCustom = UIColors.IsCustom(colorKey); - - using (ImRaii.Disabled(!isCustom)) + using (ImRaii.Disabled(!showOfflineSeparate)) + { + using var indent = ImRaii.PushIndent(); + if (ImGui.Checkbox("Show separate Offline group for Syncshell users", ref syncshellOfflineSeparate)) { - using (ImRaii.PushFont(UiBuilder.IconFont)) + _configService.Current.ShowSyncshellOfflineUsersSeparately = syncshellOfflineSeparate; + _configService.Save(); + Mediator.Publish(new RefreshUiMessage()); + } + } + + if (ImGui.Checkbox("Group up all syncshells in one folder", ref groupUpSyncshells)) + { + _configService.Current.GroupUpSyncshells = groupUpSyncshells; + _configService.Save(); + Mediator.Publish(new RefreshUiMessage()); + } + + _uiShared.DrawHelpText( + "This will group up all Syncshells in a special 'All Syncshells' folder in the main UI."); + + if (ImGui.Checkbox("Show grouped syncshells in main screen/all syncshells", ref groupedSyncshells)) + { + _configService.Current.ShowGroupedSyncshellsInAll = groupedSyncshells; + _configService.Save(); + Mediator.Publish(new RefreshUiMessage()); + } + + _uiShared.DrawHelpText("This will show grouped syncshells in main screen or group 'All Syncshells'."); + + if (ImGui.Checkbox("Show player name for visible players", ref showNameInsteadOfNotes)) + { + _configService.Current.ShowCharacterNameInsteadOfNotesForVisible = showNameInsteadOfNotes; + _configService.Save(); + Mediator.Publish(new RefreshUiMessage()); + } + + _uiShared.DrawHelpText( + "This will show the character name instead of custom set note when a character is visible"); + + ImGui.Indent(); + if (!_configService.Current.ShowCharacterNameInsteadOfNotesForVisible) ImGui.BeginDisabled(); + if (ImGui.Checkbox("Prefer notes over player names for visible players", ref preferNotesInsteadOfName)) + { + _configService.Current.PreferNotesOverNamesForVisible = preferNotesInsteadOfName; + _configService.Save(); + Mediator.Publish(new RefreshUiMessage()); + } + + _uiShared.DrawHelpText("If you set a note for a player it will be shown instead of the player name"); + if (!_configService.Current.ShowCharacterNameInsteadOfNotesForVisible) ImGui.EndDisabled(); + ImGui.Unindent(); + + if (ImGui.Checkbox("Set visible pairs as focus targets when clicking the eye", ref useFocusTarget)) + { + _configService.Current.UseFocusTarget = useFocusTarget; + _configService.Save(); + } + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + ImGui.TreePop(); + pairListTree.MarkContentEnd(); + } + } + + ImGui.Separator(); + + using (var profilesTree = BeginGeneralTree("Profiles", UIColors.Get("LightlessPurple"))) + { + if (profilesTree.Visible) + { + if (ImGui.Checkbox("Show Lightless Profiles on Hover", ref showProfiles)) + { + Mediator.Publish(new ClearProfileUserDataMessage()); + _configService.Current.ProfilesShow = showProfiles; + _configService.Save(); + } + + _uiShared.DrawHelpText("This will show the configured user profile after a set delay"); + ImGui.Indent(); + if (!showProfiles) ImGui.BeginDisabled(); + if (ImGui.Checkbox("Popout profiles on the right", ref profileOnRight)) + { + _configService.Current.ProfilePopoutRight = profileOnRight; + _configService.Save(); + Mediator.Publish(new CompactUiChange(Vector2.Zero, Vector2.Zero)); + } + + _uiShared.DrawHelpText("Will show profiles on the right side of the main UI"); + if (ImGui.SliderFloat("Hover Delay", ref profileDelay, 1, 10)) + { + _configService.Current.ProfileDelay = profileDelay; + _configService.Save(); + } + + _uiShared.DrawHelpText("Delay until the profile should be displayed"); + if (!showProfiles) ImGui.EndDisabled(); + ImGui.Unindent(); + if (ImGui.Checkbox("Show profiles marked as NSFW", ref showNsfwProfiles)) + { + Mediator.Publish(new ClearProfileUserDataMessage()); + _configService.Current.ProfilesAllowNsfw = showNsfwProfiles; + _configService.Save(); + } + + _uiShared.DrawHelpText("Will show profiles that have the NSFW tag enabled"); + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + ImGui.TreePop(); + profilesTree.MarkContentEnd(); + } + } + + ImGui.Separator(); + + ImGui.Dummy(new Vector2(10)); + _uiShared.BigText("UI Theme"); + + using (var colorsTree = BeginGeneralTree("Colors", UIColors.Get("LightlessPurple"))) + { + if (colorsTree.Visible) + { + ImGui.TextUnformatted("UI Theme Colors"); + + var colorNames = new[] + { + ("LightlessPurple", "Primary Purple", "Section titles and dividers"), + ("LightlessPurpleActive", "Primary Purple (Active)", "Active tabs and hover highlights"), + ("LightlessPurpleDefault", "Primary Purple (Inactive)", "Inactive tabs and default dividers"), + ("LightlessBlue", "Secondary Blue", "Secondary title colors, visable pairs"), + ("LightlessGreen", "Success Green", "Join buttons and success messages"), + ("LightlessYellow", "Warning Yellow", "Warning colors"), + ("LightlessOrange", "Performance Orange", "Performance notifications and warnings"), + ("PairBlue", "Syncshell Blue", "Syncshell headers, toggle highlights, and moderator actions"), + ("DimRed", "Error Red", "Error and offline colors") + }; + if (ImGui.BeginTable("##ColorTable", 3, + ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit)) + { + ImGui.TableSetupColumn("Color", ImGuiTableColumnFlags.WidthFixed); + ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Reset", ImGuiTableColumnFlags.WidthFixed, 40); + ImGui.TableHeadersRow(); + + foreach (var (colorKey, displayName, description) in colorNames) + { + ImGui.TableNextRow(); + + ImGui.TableSetColumnIndex(0); + var currentColor = UIColors.Get(colorKey); + var colorToEdit = currentColor; + if (ImGui.ColorEdit4($"##color_{colorKey}", ref colorToEdit, + ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf)) { - if (ImGui.Button(FontAwesomeIcon.Undo.ToIconString(), new Vector2(availableWidth, 0))) + UIColors.Set(colorKey, colorToEdit); + } + + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(displayName); + + ImGui.TableSetColumnIndex(1); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(description); + + ImGui.TableSetColumnIndex(2); + using var resetId = ImRaii.PushId($"Reset_{colorKey}"); + var availableWidth = ImGui.GetContentRegionAvail().X; + var isCustom = UIColors.IsCustom(colorKey); + + using (ImRaii.Disabled(!isCustom)) + { + using (ImRaii.PushFont(UiBuilder.IconFont)) { - UIColors.Reset(colorKey); + if (ImGui.Button(FontAwesomeIcon.Undo.ToIconString(), new Vector2(availableWidth, 0))) + { + UIColors.Reset(colorKey); + } } } + + UiSharedService.AttachToolTip(isCustom + ? "Reset this color to default" + : "Color is already at default value"); } - UiSharedService.AttachToolTip(isCustom - ? "Reset this color to default" - : "Color is already at default value"); + ImGui.EndTable(); } - ImGui.EndTable(); - } - - ImGui.Spacing(); - if (_uiShared.IconTextButton(FontAwesomeIcon.Undo, "Reset All Theme Colors")) - { - UIColors.ResetAll(); - } - - _uiShared.DrawHelpText("This will reset all theme colors to their default values"); - - ImGui.Spacing(); - - UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); - - ImGui.TextUnformatted("Server Info Bar Colors"); - - if (ImGui.Checkbox("Color-code the Server Info Bar entry according to status", ref useColorsInDtr)) - { - _configService.Current.UseColorsInDtr = useColorsInDtr; - _configService.Save(); - } - - _uiShared.DrawHelpText( - "This will color the Server Info Bar entry based on connection status and visible pairs."); - - ImGui.BeginDisabled(!useColorsInDtr); - const ImGuiTableFlags serverInfoTableFlags = ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit; - if (ImGui.BeginTable("##ServerInfoBarColorTable", 3, serverInfoTableFlags)) - { - ImGui.TableSetupColumn("Status", ImGuiTableColumnFlags.WidthFixed, 220f); - ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("Reset", ImGuiTableColumnFlags.WidthFixed, 40f); - ImGui.TableHeadersRow(); - - DrawDtrColorRow( - "server-default", - "Default", - "Displayed when connected without any special status.", - ref dtrColorsDefault, - DefaultConfig.DtrColorsDefault, - value => _configService.Current.DtrColorsDefault = value); - - DrawDtrColorRow( - "server-not-connected", - "Not Connected", - "Shown while disconnected from the Lightless server.", - ref dtrColorsNotConnected, - DefaultConfig.DtrColorsNotConnected, - value => _configService.Current.DtrColorsNotConnected = value); - - DrawDtrColorRow( - "server-pairs", - "Pairs in Range", - "Used when nearby paired players are detected.", - ref dtrColorsPairsInRange, - DefaultConfig.DtrColorsPairsInRange, - value => _configService.Current.DtrColorsPairsInRange = value); - - ImGui.EndTable(); - } - ImGui.EndDisabled(); - - ImGui.Spacing(); - - UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); - - ImGui.TextUnformatted("Nameplate Colors"); - - var nameColorsEnabled = _configService.Current.IsNameplateColorsEnabled; - var nameColors = _configService.Current.NameplateColors; - var isFriendOverride = _configService.Current.overrideFriendColor; - var isPartyOverride = _configService.Current.overridePartyColor; - - if (ImGui.Checkbox("Override name color of visible paired players", ref nameColorsEnabled)) - { - _configService.Current.IsNameplateColorsEnabled = nameColorsEnabled; - _configService.Save(); - _nameplateService.RequestRedraw(); - } - - _uiShared.DrawHelpText("This will override the nameplate colors for visible paired players in-game."); - - using (ImRaii.Disabled(!nameColorsEnabled)) - { - using var indent = ImRaii.PushIndent(); - if (InputDtrColors("Name color", ref nameColors)) + ImGui.Spacing(); + if (_uiShared.IconTextButton(FontAwesomeIcon.Undo, "Reset All Theme Colors")) { - _configService.Current.NameplateColors = nameColors; - _configService.Save(); - _nameplateService.RequestRedraw(); + UIColors.ResetAll(); } - if (ImGui.Checkbox("Override friend color", ref isFriendOverride)) + _uiShared.DrawHelpText("This will reset all theme colors to their default values"); + + ImGui.Spacing(); + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); + + ImGui.TextUnformatted("UI Theme"); + + if (ImGui.Checkbox("Use the redesign of the UI for Lightless client", ref useLightlessRedesign)) { - _configService.Current.overrideFriendColor = isFriendOverride; + _configService.Current.UseLightlessRedesign = useLightlessRedesign; _configService.Save(); - _nameplateService.RequestRedraw(); } - if (ImGui.Checkbox("Override party color", ref isPartyOverride)) + var usePairColoredUIDs = _configService.Current.useColoredUIDs; + + if (ImGui.Checkbox("Toggle the colored UID's in pair list", ref usePairColoredUIDs)) { - _configService.Current.overridePartyColor = isPartyOverride; + _configService.Current.useColoredUIDs = usePairColoredUIDs; _configService.Save(); - _nameplateService.RequestRedraw(); } + + _uiShared.DrawHelpText("This changes the vanity colored UID's in pair list."); + + DrawThemeOverridesSection(); + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + ImGui.TreePop(); + colorsTree.MarkContentEnd(); } - - ImGui.Spacing(); - - UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); - - ImGui.TextUnformatted("UI Theme"); - - if (ImGui.Checkbox("Use the redesign of the UI for Lightless client", ref useLightlessRedesign)) - { - _configService.Current.UseLightlessRedesign = useLightlessRedesign; - _configService.Save(); - } - - var usePairColoredUIDs = _configService.Current.useColoredUIDs; - - if (ImGui.Checkbox("Toggle the colored UID's in pair list", ref usePairColoredUIDs)) - { - _configService.Current.useColoredUIDs = usePairColoredUIDs; - _configService.Save(); - } - - _uiShared.DrawHelpText("This changes the vanity colored UID's in pair list."); - - DrawThemeOverridesSection(); - - UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); - ImGui.TreePop(); } ImGui.Separator(); - if (_uiShared.MediumTreeNode("Pair List", UIColors.Get("LightlessPurple"))) + using (var serverInfoTree = BeginGeneralTree("Server Info Bar", UIColors.Get("LightlessPurple"))) { - if (ImGui.Checkbox("Show separate Visible group", ref showVisibleSeparate)) + if (serverInfoTree.Visible) { - _configService.Current.ShowVisibleUsersSeparately = showVisibleSeparate; - _configService.Save(); - Mediator.Publish(new RefreshUiMessage()); - } + ImGui.TextUnformatted("Server Info Bar Colors"); - _uiShared.DrawHelpText( - "This will show all currently visible users in a special 'Visible' group in the main UI."); - - using (ImRaii.Disabled(!showVisibleSeparate)) - { - using var indent = ImRaii.PushIndent(); - if (ImGui.Checkbox("Show Syncshell Users in Visible Group", ref groupInVisible)) + if (ImGui.Checkbox("Color-code the Server Info Bar entry according to status", ref useColorsInDtr)) { - _configService.Current.ShowSyncshellUsersInVisible = groupInVisible; + _configService.Current.UseColorsInDtr = useColorsInDtr; _configService.Save(); - Mediator.Publish(new RefreshUiMessage()); } - } - if (ImGui.Checkbox("Show separate Offline group", ref showOfflineSeparate)) - { - _configService.Current.ShowOfflineUsersSeparately = showOfflineSeparate; - _configService.Save(); - Mediator.Publish(new RefreshUiMessage()); - } + _uiShared.DrawHelpText( + "This will color the Server Info Bar entry based on connection status and visible pairs."); - _uiShared.DrawHelpText( - "This will show all currently offline users in a special 'Offline' group in the main UI."); - - using (ImRaii.Disabled(!showOfflineSeparate)) - { - using var indent = ImRaii.PushIndent(); - if (ImGui.Checkbox("Show separate Offline group for Syncshell users", ref syncshellOfflineSeparate)) + ImGui.BeginDisabled(!useColorsInDtr); + const ImGuiTableFlags serverInfoTableFlags = ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit; + if (ImGui.BeginTable("##ServerInfoBarColorTable", 3, serverInfoTableFlags)) { - _configService.Current.ShowSyncshellOfflineUsersSeparately = syncshellOfflineSeparate; - _configService.Save(); - Mediator.Publish(new RefreshUiMessage()); + ImGui.TableSetupColumn("Status", ImGuiTableColumnFlags.WidthFixed, 220f); + ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Reset", ImGuiTableColumnFlags.WidthFixed, 40f); + ImGui.TableHeadersRow(); + + DrawDtrColorRow( + "server-default", + "Default", + "Displayed when connected without any special status.", + ref dtrColorsDefault, + DefaultConfig.DtrColorsDefault, + value => _configService.Current.DtrColorsDefault = value); + + DrawDtrColorRow( + "server-not-connected", + "Not Connected", + "Shown while disconnected from the Lightless server.", + ref dtrColorsNotConnected, + DefaultConfig.DtrColorsNotConnected, + value => _configService.Current.DtrColorsNotConnected = value); + + DrawDtrColorRow( + "server-pairs", + "Pairs in Range", + "Used when nearby paired players are detected.", + ref dtrColorsPairsInRange, + DefaultConfig.DtrColorsPairsInRange, + value => _configService.Current.DtrColorsPairsInRange = value); + + ImGui.EndTable(); } + ImGui.EndDisabled(); + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + ImGui.TreePop(); + serverInfoTree.MarkContentEnd(); } - - if (ImGui.Checkbox("Group up all syncshells in one folder", ref groupUpSyncshells)) - { - _configService.Current.GroupUpSyncshells = groupUpSyncshells; - _configService.Save(); - Mediator.Publish(new RefreshUiMessage()); - } - - _uiShared.DrawHelpText( - "This will group up all Syncshells in a special 'All Syncshells' folder in the main UI."); - - if (ImGui.Checkbox("Show grouped syncshells in main screen/all syncshells", ref groupedSyncshells)) - { - _configService.Current.ShowGroupedSyncshellsInAll = groupedSyncshells; - _configService.Save(); - Mediator.Publish(new RefreshUiMessage()); - } - - _uiShared.DrawHelpText("This will show grouped syncshells in main screen or group 'All Syncshells'."); - - if (ImGui.Checkbox("Show player name for visible players", ref showNameInsteadOfNotes)) - { - _configService.Current.ShowCharacterNameInsteadOfNotesForVisible = showNameInsteadOfNotes; - _configService.Save(); - Mediator.Publish(new RefreshUiMessage()); - } - - _uiShared.DrawHelpText( - "This will show the character name instead of custom set note when a character is visible"); - - ImGui.Indent(); - if (!_configService.Current.ShowCharacterNameInsteadOfNotesForVisible) ImGui.BeginDisabled(); - if (ImGui.Checkbox("Prefer notes over player names for visible players", ref preferNotesInsteadOfName)) - { - _configService.Current.PreferNotesOverNamesForVisible = preferNotesInsteadOfName; - _configService.Save(); - Mediator.Publish(new RefreshUiMessage()); - } - - _uiShared.DrawHelpText("If you set a note for a player it will be shown instead of the player name"); - if (!_configService.Current.ShowCharacterNameInsteadOfNotesForVisible) ImGui.EndDisabled(); - ImGui.Unindent(); - - if (ImGui.Checkbox("Set visible pairs as focus targets when clicking the eye", ref useFocusTarget)) - { - _configService.Current.UseFocusTarget = useFocusTarget; - _configService.Save(); - } - - UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); - ImGui.TreePop(); } ImGui.Separator(); - if (_uiShared.MediumTreeNode("Profiles", UIColors.Get("LightlessPurple"))) + + using (var nameplateTree = BeginGeneralTree("Nameplate", UIColors.Get("LightlessPurple"))) { - if (ImGui.Checkbox("Show Lightless Profiles on Hover", ref showProfiles)) + if (nameplateTree.Visible) { - Mediator.Publish(new ClearProfileUserDataMessage()); - _configService.Current.ProfilesShow = showProfiles; - _configService.Save(); + ImGui.TextUnformatted("Nameplate Colors"); + + var nameColorsEnabled = _configService.Current.IsNameplateColorsEnabled; + var nameColors = _configService.Current.NameplateColors; + var isFriendOverride = _configService.Current.overrideFriendColor; + var isPartyOverride = _configService.Current.overridePartyColor; + + if (ImGui.Checkbox("Override name color of visible paired players", ref nameColorsEnabled)) + { + _configService.Current.IsNameplateColorsEnabled = nameColorsEnabled; + _configService.Save(); + _nameplateService.RequestRedraw(); + } + + _uiShared.DrawHelpText("This will override the nameplate colors for visible paired players in-game."); + + using (ImRaii.Disabled(!nameColorsEnabled)) + { + using var indent = ImRaii.PushIndent(); + if (InputDtrColors("Name color", ref nameColors)) + { + _configService.Current.NameplateColors = nameColors; + _configService.Save(); + _nameplateService.RequestRedraw(); + } + + if (ImGui.Checkbox("Override friend color", ref isFriendOverride)) + { + _configService.Current.overrideFriendColor = isFriendOverride; + _configService.Save(); + _nameplateService.RequestRedraw(); + } + + if (ImGui.Checkbox("Override party color", ref isPartyOverride)) + { + _configService.Current.overridePartyColor = isPartyOverride; + _configService.Save(); + _nameplateService.RequestRedraw(); + } + } + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + ImGui.TreePop(); + nameplateTree.MarkContentEnd(); } - - _uiShared.DrawHelpText("This will show the configured user profile after a set delay"); - ImGui.Indent(); - if (!showProfiles) ImGui.BeginDisabled(); - if (ImGui.Checkbox("Popout profiles on the right", ref profileOnRight)) - { - _configService.Current.ProfilePopoutRight = profileOnRight; - _configService.Save(); - Mediator.Publish(new CompactUiChange(Vector2.Zero, Vector2.Zero)); - } - - _uiShared.DrawHelpText("Will show profiles on the right side of the main UI"); - if (ImGui.SliderFloat("Hover Delay", ref profileDelay, 1, 10)) - { - _configService.Current.ProfileDelay = profileDelay; - _configService.Save(); - } - - _uiShared.DrawHelpText("Delay until the profile should be displayed"); - if (!showProfiles) ImGui.EndDisabled(); - ImGui.Unindent(); - if (ImGui.Checkbox("Show profiles marked as NSFW", ref showNsfwProfiles)) - { - Mediator.Publish(new ClearProfileUserDataMessage()); - _configService.Current.ProfilesAllowNsfw = showNsfwProfiles; - _configService.Save(); - } - - _uiShared.DrawHelpText("Will show profiles that have the NSFW tag enabled"); - - UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); - ImGui.TreePop(); } + ImGui.Separator(); + + ImGui.EndChild(); + ImGui.EndGroup(); + + generalSelune.DrawHighlightOnly(ImGui.GetIO().DeltaTime); + } + + private void DrawGeneralNavigation() + { + var buttonWidth = Math.Max(1f, ImGui.GetContentRegionAvail().X); + var buttonHeight = Math.Max(ImGui.GetFrameHeight(), 36f * ImGuiHelpers.GlobalScale); + for (var i = 0; i < _generalTreeNavOrder.Length; i++) + { + var label = _generalTreeNavOrder[i]; + using var id = ImRaii.PushId(label); + var isTarget = string.Equals(_generalOpenTreeTarget, label, StringComparison.Ordinal) || + string.Equals(_generalScrollTarget, label, StringComparison.Ordinal); + using var activeColor = isTarget + ? ImRaii.PushColor(ImGuiCol.Button, ImGui.GetStyle().Colors[(int)ImGuiCol.TabActive]) + : null; + using var activeHover = isTarget + ? ImRaii.PushColor(ImGuiCol.ButtonHovered, ImGui.GetStyle().Colors[(int)ImGuiCol.TabHovered]) + : null; + using var activeActive = isTarget + ? ImRaii.PushColor(ImGuiCol.ButtonActive, ImGui.GetStyle().Colors[(int)ImGuiCol.TabActive]) + : null; + + if (ImGui.Button(label, new Vector2(buttonWidth, buttonHeight))) + { + FocusGeneralTree(label); + } + + if (_generalNavSeparatorAfter.Contains(label) && i < _generalTreeNavOrder.Length - 1) + { + ImGui.Spacing(); + ImGui.Separator(); + ImGui.Spacing(); + } + } + } + + private GeneralTreeScope BeginGeneralTree(string label, Vector4 color) + { + var shouldForceOpen = string.Equals(_generalOpenTreeTarget, label, StringComparison.Ordinal); + if (shouldForceOpen) + { + ImGui.SetNextItemOpen(true, ImGuiCond.Always); + } + + var open = _uiShared.MediumTreeNode(label, color); + if (shouldForceOpen) + { + _generalOpenTreeTarget = null; + } + + var headerMin = ImGui.GetItemRectMin(); + var headerMax = ImGui.GetItemRectMax(); + var windowPos = ImGui.GetWindowPos(); + var contentRegionMin = windowPos + ImGui.GetWindowContentRegionMin(); + var contentRegionMax = windowPos + ImGui.GetWindowContentRegionMax(); + + if (open && string.Equals(_generalScrollTarget, label, StringComparison.Ordinal)) + { + ImGui.SetScrollHereY(0f); + _generalScrollTarget = null; + } + + return new GeneralTreeScope(open, color, GetGeneralTreeHighlightAlpha(label), headerMin, headerMax, contentRegionMin, contentRegionMax); + } + + private void FocusGeneralTree(string label) + { + _generalOpenTreeTarget = label; + _generalScrollTarget = label; + _generalTreeHighlights[label] = ImGui.GetTime(); + } + + private float GetGeneralTreeHighlightAlpha(string label) + { + if (!_generalTreeHighlights.TryGetValue(label, out var startTime)) + return 0f; + + var elapsed = (float)(ImGui.GetTime() - startTime); + if (elapsed >= GeneralTreeHighlightDuration) + { + _generalTreeHighlights.Remove(label); + return 0f; + } + + return 1f - (elapsed / GeneralTreeHighlightDuration); + } + + private struct GeneralTreeScope : IDisposable + { + private readonly bool _visible; + private readonly Vector4 _color; + private readonly float _highlightAlpha; + private readonly Vector2 _headerMin; + private readonly Vector2 _headerMax; + private readonly Vector2 _contentRegionMin; + private readonly Vector2 _contentRegionMax; + private Vector2 _contentEnd; + private bool _hasContentEnd; + + public bool Visible => _visible; + + public GeneralTreeScope(bool visible, Vector4 color, float highlightAlpha, Vector2 headerMin, Vector2 headerMax, Vector2 contentRegionMin, Vector2 contentRegionMax) + { + _visible = visible; + _color = color; + _highlightAlpha = highlightAlpha; + _headerMin = headerMin; + _headerMax = headerMax; + _contentRegionMin = contentRegionMin; + _contentRegionMax = contentRegionMax; + _contentEnd = Vector2.Zero; + _hasContentEnd = false; + } + + public void MarkContentEnd() + { + if (!_visible) + return; + + _contentEnd = ImGui.GetCursorScreenPos(); + _hasContentEnd = true; + } + + public void Dispose() + { + if (_highlightAlpha <= 0f) + return; + + var style = ImGui.GetStyle(); + var rectMin = new Vector2(_contentRegionMin.X, _headerMin.Y) - new Vector2(0f, 2f); + var rectMax = new Vector2(_contentRegionMax.X, _headerMax.Y) + new Vector2(0f, 2f); + + if (_visible) + { + var contentEnd = _hasContentEnd ? _contentEnd : ImGui.GetCursorScreenPos(); + rectMax.Y = Math.Max(rectMax.Y, contentEnd.Y + style.ItemSpacing.Y + 2f); + } + + Selune.RegisterHighlight( + rectMin, + rectMax, + SeluneHighlightMode.Both, + borderOnly: false, + exactSize: true, + clipToElement: true, + clipPadding: new Vector2(0f, 4f), + highlightColorOverride: new Vector4(_color.X, _color.Y, _color.Z, 0.4f), + highlightAlphaOverride: _highlightAlpha); + } } private void DrawPerformance() @@ -2946,530 +3221,555 @@ public class SettingsUi : WindowMediatorSubscriberBase bool useOauth = selectedServer.UseOAuth2; - if (ImGui.BeginTabBar("serverTabBar")) + _serverTabOptions.Clear(); + _serverTabOptions.Add(new UiSharedService.TabOption( + "Character Management", + ServerSettingsTab.CharacterManagement)); + + if (!useOauth) { - if (ImGui.BeginTabItem("Character Management")) - { - if (selectedServer.SecretKeys.Any() || useOauth) - { - UiSharedService.ColorTextWrapped( - "Characters listed here will automatically connect to the selected Lightless service with the settings as provided below." + - " Make sure to enter the character names correctly or use the 'Add current character' button at the bottom.", - UIColors.Get("LightlessYellow")); - int i = 0; - _uiShared.DrawUpdateOAuthUIDsButton(selectedServer); + _serverTabOptions.Add(new UiSharedService.TabOption( + "Secret Key Management", + ServerSettingsTab.SecretKeyManagement)); + } - if (selectedServer.UseOAuth2 && !string.IsNullOrEmpty(selectedServer.OAuthToken)) - { - bool hasSetSecretKeysButNoUid = - selectedServer.Authentications.Exists(u => - u.SecretKeyIdx != -1 && string.IsNullOrEmpty(u.UID)); - if (hasSetSecretKeysButNoUid) - { - ImGui.Dummy(new(5f, 5f)); - UiSharedService.TextWrapped( - "Some entries have been detected that have previously been assigned secret keys but not UIDs. " + - "Press this button below to attempt to convert those entries."); - using (ImRaii.Disabled(_secretKeysConversionTask != null && - !_secretKeysConversionTask.IsCompleted)) - { - if (_uiShared.IconTextButton(FontAwesomeIcon.ArrowsLeftRight, - "Try to Convert Secret Keys to UIDs")) - { - _secretKeysConversionTask = - ConvertSecretKeysToUIDs(selectedServer, _secretKeysConversionCts.Token); - } - } + _serverTabOptions.Add(new UiSharedService.TabOption( + "Service Configuration", + ServerSettingsTab.ServiceConfiguration)); + _serverTabOptions.Add(new UiSharedService.TabOption( + "Permission Settings", + ServerSettingsTab.PermissionSettings)); - if (_secretKeysConversionTask != null && !_secretKeysConversionTask.IsCompleted) - { - UiSharedService.ColorTextWrapped("Converting Secret Keys to UIDs", - UIColors.Get("LightlessYellow")); - } + UiSharedService.Tab("ServerSettingsTabs", _serverTabOptions, ref _selectedServerTab); + ImGuiHelpers.ScaledDummy(5); - if (_secretKeysConversionTask != null && _secretKeysConversionTask.IsCompletedSuccessfully) - { - Vector4? textColor = null; - if (_secretKeysConversionTask.Result.PartialSuccess) - { - textColor = UIColors.Get("LightlessYellow"); - } - - if (!_secretKeysConversionTask.Result.Success) - { - textColor = ImGuiColors.DalamudRed; - } - - string text = $"Conversion has completed: {_secretKeysConversionTask.Result.Result}"; - if (textColor == null) - { - UiSharedService.TextWrapped(text); - } - else - { - UiSharedService.ColorTextWrapped(text, textColor!.Value); - } - - if (!_secretKeysConversionTask.Result.Success || - _secretKeysConversionTask.Result.PartialSuccess) - { - UiSharedService.TextWrapped( - "In case of conversion failures, please set the UIDs for the failed conversions manually."); - } - } - } - } - - ImGui.Separator(); - string youName = _dalamudUtilService.GetPlayerName(); - uint youWorld = _dalamudUtilService.GetHomeWorldId(); - ulong youCid = _dalamudUtilService.GetCID(); - if (!selectedServer.Authentications.Exists(a => - string.Equals(a.CharacterName, youName, StringComparison.Ordinal) && a.WorldId == youWorld)) - { - _uiShared.BigText("Your Character is not Configured", ImGuiColors.DalamudRed); - UiSharedService.ColorTextWrapped( - "You have currently no character configured that corresponds to your current name and world.", - ImGuiColors.DalamudRed); - var authWithCid = selectedServer.Authentications.Find(f => f.LastSeenCID == youCid); - if (authWithCid != null) - { - ImGuiHelpers.ScaledDummy(5); - UiSharedService.ColorText( - "A potential rename/world change from this character was detected:", - UIColors.Get("LightlessYellow")); - using (ImRaii.PushIndent(10f)) - UiSharedService.ColorText( - "Entry: " + authWithCid.CharacterName + " - " + - _dalamudUtilService.WorldData.Value[(ushort)authWithCid.WorldId], - UIColors.Get("LightlessBlue")); - UiSharedService.ColorText( - "Press the button below to adjust that entry to your current character:", - UIColors.Get("LightlessYellow")); - using (ImRaii.PushIndent(10f)) - UiSharedService.ColorText( - "Current: " + youName + " - " + - _dalamudUtilService.WorldData.Value[(ushort)youWorld], - UIColors.Get("LightlessBlue")); - ImGuiHelpers.ScaledDummy(5); - if (_uiShared.IconTextButton(FontAwesomeIcon.ArrowRight, - "Update Entry to Current Character")) - { - authWithCid.CharacterName = youName; - authWithCid.WorldId = youWorld; - _serverConfigurationManager.Save(); - } - } - - ImGuiHelpers.ScaledDummy(5); - ImGui.Separator(); - ImGuiHelpers.ScaledDummy(5); - } - - foreach (var item in selectedServer.Authentications.ToList()) - { - using var charaId = ImRaii.PushId("selectedChara" + i); - - var worldIdx = (ushort)item.WorldId; - var data = _uiShared.WorldData.OrderBy(u => u.Value, StringComparer.Ordinal) - .ToDictionary(k => k.Key, k => k.Value); - if (!data.TryGetValue(worldIdx, out string? worldPreview)) - { - worldPreview = data.First().Value; - } - - Dictionary keys = []; - - if (!useOauth) - { - var secretKeyIdx = item.SecretKeyIdx; - keys = selectedServer.SecretKeys; - if (!keys.TryGetValue(secretKeyIdx, out var secretKey)) - { - secretKey = new(); - } - } - - bool thisIsYou = false; - if (string.Equals(youName, item.CharacterName, StringComparison.OrdinalIgnoreCase) - && youWorld == worldIdx) - { - thisIsYou = true; - } - - bool misManaged = false; - if (selectedServer.UseOAuth2 && !string.IsNullOrEmpty(selectedServer.OAuthToken) && - string.IsNullOrEmpty(item.UID)) - { - misManaged = true; - } - - if (!selectedServer.UseOAuth2 && item.SecretKeyIdx == -1) - { - misManaged = true; - } - - Vector4 color = UIColors.Get("LightlessBlue"); - string text = thisIsYou ? "Your Current Character" : string.Empty; - if (misManaged) - { - text += " [MISMANAGED (" + (selectedServer.UseOAuth2 ? "No UID Set" : "No Secret Key Set") + - ")]"; - color = ImGuiColors.DalamudRed; - } - - if (selectedServer.Authentications.Where(e => e != item).Any(e => - string.Equals(e.CharacterName, item.CharacterName, StringComparison.Ordinal) - && e.WorldId == item.WorldId)) - { - text += " [DUPLICATE]"; - color = ImGuiColors.DalamudRed; - } - - if (!string.IsNullOrEmpty(text)) - { - text = text.Trim(); - _uiShared.BigText(text, color); - } - - var charaName = item.CharacterName; - if (ImGui.InputText("Character Name", ref charaName, 64)) - { - item.CharacterName = charaName; - _serverConfigurationManager.Save(); - } - - _uiShared.DrawCombo("World##" + item.CharacterName + i, data, (w) => w.Value, - (w) => - { - if (item.WorldId != w.Key) - { - item.WorldId = w.Key; - _serverConfigurationManager.Save(); - } - }, - EqualityComparer>.Default.Equals( - data.FirstOrDefault(f => f.Key == worldIdx), default) - ? data.First() - : data.First(f => f.Key == worldIdx)); - - if (!useOauth) - { - _uiShared.DrawCombo("Secret Key###" + item.CharacterName + i, keys, - (w) => w.Value.FriendlyName, - (w) => - { - if (w.Key != item.SecretKeyIdx) - { - item.SecretKeyIdx = w.Key; - _serverConfigurationManager.Save(); - } - }, - EqualityComparer>.Default.Equals( - keys.FirstOrDefault(f => f.Key == item.SecretKeyIdx), default) - ? keys.First() - : keys.First(f => f.Key == item.SecretKeyIdx)); - } - else - { - _uiShared.DrawUIDComboForAuthentication(i, item, selectedServer.ServerUri, _logger); - } - - bool isAutoLogin = item.AutoLogin; - if (ImGui.Checkbox("Automatically login to Lightless", ref isAutoLogin)) - { - item.AutoLogin = isAutoLogin; - _serverConfigurationManager.Save(); - } - - _uiShared.DrawHelpText( - "When enabled and logging into this character in XIV, Lightless will automatically connect to the current service."); - if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete Character") && - UiSharedService.CtrlPressed()) - _serverConfigurationManager.RemoveCharacterFromServer(idx, item); - UiSharedService.AttachToolTip("Hold CTRL to delete this entry."); - - i++; - if (item != selectedServer.Authentications.ToList()[^1]) - { - ImGuiHelpers.ScaledDummy(5); - ImGui.Separator(); - ImGuiHelpers.ScaledDummy(5); - } - } - - if (selectedServer.Authentications.Any()) - ImGui.Separator(); - - if (!selectedServer.Authentications.Exists(c => - string.Equals(c.CharacterName, youName, StringComparison.Ordinal) - && c.WorldId == youWorld)) - { - if (_uiShared.IconTextButton(FontAwesomeIcon.User, "Add current character")) - { - _serverConfigurationManager.AddCurrentCharacterToServer(idx); - } - - ImGui.SameLine(); - } - - if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Add new character")) - { - _serverConfigurationManager.AddEmptyCharacterToServer(idx); - } - } - else - { - UiSharedService.ColorTextWrapped("You need to add a Secret Key first before adding Characters.", - UIColors.Get("LightlessYellow")); - } - - ImGui.EndTabItem(); - } - - if (!useOauth && ImGui.BeginTabItem("Secret Key Management")) - { - foreach (var item in selectedServer.SecretKeys.ToList()) - { - using var id = ImRaii.PushId("key" + item.Key); - var friendlyName = item.Value.FriendlyName; - if (ImGui.InputText("Secret Key Display Name", ref friendlyName, 255)) - { - item.Value.FriendlyName = friendlyName; - _serverConfigurationManager.Save(); - } - - var key = item.Value.Key; - if (ImGui.InputText("Secret Key", ref key, 64)) - { - item.Value.Key = key; - _serverConfigurationManager.Save(); - } - - if (!selectedServer.Authentications.Exists(p => p.SecretKeyIdx == item.Key)) - { - if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete Secret Key") && - UiSharedService.CtrlPressed()) - { - selectedServer.SecretKeys.Remove(item.Key); - _serverConfigurationManager.Save(); - } - - UiSharedService.AttachToolTip("Hold CTRL to delete this secret key entry"); - } - else - { - UiSharedService.ColorTextWrapped("This key is in use and cannot be deleted", - UIColors.Get("LightlessYellow")); - } - - if (item.Key != selectedServer.SecretKeys.Keys.LastOrDefault()) - ImGui.Separator(); - } - - ImGui.Separator(); - if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Add new Secret Key")) - { - selectedServer.SecretKeys.Add( - selectedServer.SecretKeys.Any() ? selectedServer.SecretKeys.Max(p => p.Key) + 1 : 0, - new SecretKey() { FriendlyName = "New Secret Key", }); - _serverConfigurationManager.Save(); - } - - ImGui.EndTabItem(); - } - - if (ImGui.BeginTabItem("Service Configuration")) - { - var serverName = selectedServer.ServerName; - var serverUri = selectedServer.ServerUri; - var isMain = string.Equals(serverName, ApiController.MainServer, StringComparison.OrdinalIgnoreCase); - var flags = isMain ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None; - - if (ImGui.InputText("Service URI", ref serverUri, 255, flags)) - { - selectedServer.ServerUri = serverUri; - } - - if (isMain) - { - _uiShared.DrawHelpText("You cannot edit the URI of the main service."); - } - - if (ImGui.InputText("Service Name", ref serverName, 255, flags)) - { - selectedServer.ServerName = serverName; - _serverConfigurationManager.Save(); - } - - if (isMain) - { - _uiShared.DrawHelpText("You cannot edit the name of the main service."); - } - - ImGui.SetNextItemWidth(200); - var serverTransport = _serverConfigurationManager.GetTransport(); - _uiShared.DrawCombo("Server Transport Type", - Enum.GetValues().Where(t => t != HttpTransportType.None), - (v) => v.ToString(), - onSelected: (t) => _serverConfigurationManager.SetTransportType(t), - serverTransport); - _uiShared.DrawHelpText( - "You normally do not need to change this, if you don't know what this is or what it's for, keep it to WebSockets." + - Environment.NewLine - + "If you run into connection issues with e.g. VPNs, try ServerSentEvents first before trying out LongPolling." + - UiSharedService.TooltipSeparator - + "Note: if the server does not support a specific Transport Type it will fall through to the next automatically: WebSockets > ServerSentEvents > LongPolling"); - - ImGuiHelpers.ScaledDummy(5); - - if (ImGui.Checkbox("Use Discord OAuth2 Authentication", ref useOauth)) - { - selectedServer.UseOAuth2 = useOauth; - _serverConfigurationManager.Save(); - } - - _uiShared.DrawHelpText( - "Use Discord OAuth2 Authentication to identify with this server instead of secret keys"); - if (useOauth) - { - _uiShared.DrawOAuth(selectedServer); - if (string.IsNullOrEmpty(_serverConfigurationManager.GetDiscordUserFromToken(selectedServer))) - { - ImGuiHelpers.ScaledDummy(10f); - UiSharedService.ColorTextWrapped( - "You have enabled OAuth2 but it is not linked. Press the buttons Check, then Authenticate to link properly.", - ImGuiColors.DalamudRed); - } - - if (!string.IsNullOrEmpty(_serverConfigurationManager.GetDiscordUserFromToken(selectedServer)) - && selectedServer.Authentications.TrueForAll(u => string.IsNullOrEmpty(u.UID))) - { - ImGuiHelpers.ScaledDummy(10f); - UiSharedService.ColorTextWrapped( - "You have enabled OAuth2 but no characters configured. Set the correct UIDs for your characters in \"Character Management\".", - ImGuiColors.DalamudRed); - } - } - - if (!isMain && selectedServer != _serverConfigurationManager.CurrentServer) - { - ImGui.Separator(); - if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete Service") && - UiSharedService.CtrlPressed()) - { - _serverConfigurationManager.DeleteServer(selectedServer); - } - - _uiShared.DrawHelpText("Hold CTRL to delete this service"); - } - - ImGui.EndTabItem(); - } - - if (ImGui.BeginTabItem("Permission Settings")) - { - _uiShared.BigText("Default Permission Settings"); - if (selectedServer == _serverConfigurationManager.CurrentServer && _apiController.IsConnected) - { - UiSharedService.TextWrapped( - "Note: The default permissions settings here are not applied retroactively to existing pairs or joined Syncshells."); - UiSharedService.TextWrapped( - "Note: The default permissions settings here are sent and stored on the connected service."); - ImGuiHelpers.ScaledDummy(5f); - var perms = _apiController.DefaultPermissions!; - bool individualIsSticky = perms.IndividualIsSticky; - bool disableIndividualSounds = perms.DisableIndividualSounds; - bool disableIndividualAnimations = perms.DisableIndividualAnimations; - bool disableIndividualVFX = perms.DisableIndividualVFX; - if (ImGui.Checkbox("Individually set permissions become preferred permissions", - ref individualIsSticky)) - { - perms.IndividualIsSticky = individualIsSticky; - _ = _apiController.UserUpdateDefaultPermissions(perms); - } - - _uiShared.DrawHelpText( - "The preferred attribute means that the permissions to that user will never change through any of your permission changes to Syncshells " + - "(i.e. if you have paused one specific user in a Syncshell and they become preferred permissions, then pause and unpause the same Syncshell, the user will remain paused - " + - "if a user does not have preferred permissions, it will follow the permissions of the Syncshell and be unpaused)." + - Environment.NewLine + Environment.NewLine + - "This setting means:" + Environment.NewLine + - " - All new individual pairs get their permissions defaulted to preferred permissions." + - Environment.NewLine + - " - All individually set permissions for any pair will also automatically become preferred permissions. This includes pairs in Syncshells." + - Environment.NewLine + Environment.NewLine + - "It is possible to remove or set the preferred permission state for any pair at any time." + - Environment.NewLine + Environment.NewLine + - "If unsure, leave this setting off."); - ImGuiHelpers.ScaledDummy(3f); - - if (ImGui.Checkbox("Disable individual pair sounds", ref disableIndividualSounds)) - { - perms.DisableIndividualSounds = disableIndividualSounds; - _ = _apiController.UserUpdateDefaultPermissions(perms); - } - - _uiShared.DrawHelpText("This setting will disable sound sync for all new individual pairs."); - if (ImGui.Checkbox("Disable individual pair animations", ref disableIndividualAnimations)) - { - perms.DisableIndividualAnimations = disableIndividualAnimations; - _ = _apiController.UserUpdateDefaultPermissions(perms); - } - - _uiShared.DrawHelpText("This setting will disable animation sync for all new individual pairs."); - if (ImGui.Checkbox("Disable individual pair VFX", ref disableIndividualVFX)) - { - perms.DisableIndividualVFX = disableIndividualVFX; - _ = _apiController.UserUpdateDefaultPermissions(perms); - } - - _uiShared.DrawHelpText("This setting will disable VFX sync for all new individual pairs."); - ImGuiHelpers.ScaledDummy(5f); - bool disableGroundSounds = perms.DisableGroupSounds; - bool disableGroupAnimations = perms.DisableGroupAnimations; - bool disableGroupVFX = perms.DisableGroupVFX; - if (ImGui.Checkbox("Disable Syncshell pair sounds", ref disableGroundSounds)) - { - perms.DisableGroupSounds = disableGroundSounds; - _ = _apiController.UserUpdateDefaultPermissions(perms); - } - - _uiShared.DrawHelpText( - "This setting will disable sound sync for all non-sticky pairs in newly joined syncshells."); - if (ImGui.Checkbox("Disable Syncshell pair animations", ref disableGroupAnimations)) - { - perms.DisableGroupAnimations = disableGroupAnimations; - _ = _apiController.UserUpdateDefaultPermissions(perms); - } - - _uiShared.DrawHelpText( - "This setting will disable animation sync for all non-sticky pairs in newly joined syncshells."); - if (ImGui.Checkbox("Disable Syncshell pair VFX", ref disableGroupVFX)) - { - perms.DisableGroupVFX = disableGroupVFX; - _ = _apiController.UserUpdateDefaultPermissions(perms); - } - - _uiShared.DrawHelpText( - "This setting will disable VFX sync for all non-sticky pairs in newly joined syncshells."); - } - else - { - UiSharedService.ColorTextWrapped("Default Permission Settings unavailable for this service. " + - "You need to connect to this service to change the default permissions since they are stored on the service.", - UIColors.Get("LightlessYellow")); - } - - ImGui.EndTabItem(); - } - - ImGui.EndTabBar(); + switch (_selectedServerTab) + { + case ServerSettingsTab.CharacterManagement: + DrawServerCharacterManagement(selectedServer, idx, useOauth); + break; + case ServerSettingsTab.SecretKeyManagement when !useOauth: + DrawServerSecretKeyManagement(selectedServer); + break; + case ServerSettingsTab.ServiceConfiguration: + DrawServerServiceConfiguration(selectedServer, ref useOauth); + break; + case ServerSettingsTab.PermissionSettings: + DrawServerPermissionSettings(selectedServer); + break; } ImGui.Dummy(new Vector2(10)); } + private void DrawServerCharacterManagement(ServerStorage selectedServer, int idx, bool useOauth) + { + if (selectedServer.SecretKeys.Any() || useOauth) + { + UiSharedService.ColorTextWrapped( + "Characters listed here will automatically connect to the selected Lightless service with the settings as provided below." + + " Make sure to enter the character names correctly or use the 'Add current character' button at the bottom.", + UIColors.Get("LightlessYellow")); + int i = 0; + _uiShared.DrawUpdateOAuthUIDsButton(selectedServer); + + if (selectedServer.UseOAuth2 && !string.IsNullOrEmpty(selectedServer.OAuthToken)) + { + bool hasSetSecretKeysButNoUid = + selectedServer.Authentications.Exists(u => + u.SecretKeyIdx != -1 && string.IsNullOrEmpty(u.UID)); + if (hasSetSecretKeysButNoUid) + { + ImGui.Dummy(new(5f, 5f)); + UiSharedService.TextWrapped( + "Some entries have been detected that have previously been assigned secret keys but not UIDs. " + + "Press this button below to attempt to convert those entries."); + using (ImRaii.Disabled(_secretKeysConversionTask != null && + !_secretKeysConversionTask.IsCompleted)) + { + if (_uiShared.IconTextButton(FontAwesomeIcon.ArrowsLeftRight, + "Try to Convert Secret Keys to UIDs")) + { + _secretKeysConversionTask = + ConvertSecretKeysToUIDs(selectedServer, _secretKeysConversionCts.Token); + } + } + + if (_secretKeysConversionTask != null && !_secretKeysConversionTask.IsCompleted) + { + UiSharedService.ColorTextWrapped("Converting Secret Keys to UIDs", + UIColors.Get("LightlessYellow")); + } + + if (_secretKeysConversionTask != null && _secretKeysConversionTask.IsCompletedSuccessfully) + { + Vector4? textColor = null; + if (_secretKeysConversionTask.Result.PartialSuccess) + { + textColor = UIColors.Get("LightlessYellow"); + } + + if (!_secretKeysConversionTask.Result.Success) + { + textColor = ImGuiColors.DalamudRed; + } + + string text = $"Conversion has completed: {_secretKeysConversionTask.Result.Result}"; + if (textColor == null) + { + UiSharedService.TextWrapped(text); + } + else + { + UiSharedService.ColorTextWrapped(text, textColor!.Value); + } + + if (!_secretKeysConversionTask.Result.Success || + _secretKeysConversionTask.Result.PartialSuccess) + { + UiSharedService.TextWrapped( + "In case of conversion failures, please set the UIDs for the failed conversions manually."); + } + } + } + } + + ImGui.Separator(); + string youName = _dalamudUtilService.GetPlayerName(); + uint youWorld = _dalamudUtilService.GetHomeWorldId(); + ulong youCid = _dalamudUtilService.GetCID(); + if (!selectedServer.Authentications.Exists(a => + string.Equals(a.CharacterName, youName, StringComparison.Ordinal) && a.WorldId == youWorld)) + { + _uiShared.BigText("Your Character is not Configured", ImGuiColors.DalamudRed); + UiSharedService.ColorTextWrapped( + "You have currently no character configured that corresponds to your current name and world.", + ImGuiColors.DalamudRed); + var authWithCid = selectedServer.Authentications.Find(f => f.LastSeenCID == youCid); + if (authWithCid != null) + { + ImGuiHelpers.ScaledDummy(5); + UiSharedService.ColorTextWrapped( + "A potential rename/world change from this character was detected:", + UIColors.Get("LightlessYellow")); + using (ImRaii.PushIndent(10f)) + UiSharedService.ColorText( + "Entry: " + authWithCid.CharacterName + " - " + + _dalamudUtilService.WorldData.Value[(ushort)authWithCid.WorldId], + UIColors.Get("LightlessBlue")); + UiSharedService.ColorText( + "Press the button below to adjust that entry to your current character:", + UIColors.Get("LightlessYellow")); + using (ImRaii.PushIndent(10f)) + UiSharedService.ColorText( + "Current: " + youName + " - " + + _dalamudUtilService.WorldData.Value[(ushort)youWorld], + UIColors.Get("LightlessBlue")); + ImGuiHelpers.ScaledDummy(5); + if (_uiShared.IconTextButton(FontAwesomeIcon.ArrowRight, + "Update Entry to Current Character")) + { + authWithCid.CharacterName = youName; + authWithCid.WorldId = youWorld; + _serverConfigurationManager.Save(); + } + } + + ImGuiHelpers.ScaledDummy(5); + ImGui.Separator(); + ImGuiHelpers.ScaledDummy(5); + } + + foreach (var item in selectedServer.Authentications.ToList()) + { + using var charaId = ImRaii.PushId("selectedChara" + i); + + var worldIdx = (ushort)item.WorldId; + var data = _uiShared.WorldData.OrderBy(u => u.Value, StringComparer.Ordinal) + .ToDictionary(k => k.Key, k => k.Value); + if (!data.TryGetValue(worldIdx, out string? worldPreview)) + { + worldPreview = data.First().Value; + } + + Dictionary keys = []; + + if (!useOauth) + { + var secretKeyIdx = item.SecretKeyIdx; + keys = selectedServer.SecretKeys; + if (!keys.TryGetValue(secretKeyIdx, out var secretKey)) + { + secretKey = new(); + } + } + + bool thisIsYou = false; + if (string.Equals(youName, item.CharacterName, StringComparison.OrdinalIgnoreCase) + && youWorld == worldIdx) + { + thisIsYou = true; + } + + bool misManaged = false; + if (selectedServer.UseOAuth2 && !string.IsNullOrEmpty(selectedServer.OAuthToken) && + string.IsNullOrEmpty(item.UID)) + { + misManaged = true; + } + + if (!selectedServer.UseOAuth2 && item.SecretKeyIdx == -1) + { + misManaged = true; + } + + Vector4 color = UIColors.Get("LightlessBlue"); + string text = thisIsYou ? "Your Current Character" : string.Empty; + if (misManaged) + { + text += " [MISMANAGED (" + (selectedServer.UseOAuth2 ? "No UID Set" : "No Secret Key Set") + + ")]"; + color = ImGuiColors.DalamudRed; + } + + if (selectedServer.Authentications.Where(e => e != item).Any(e => + string.Equals(e.CharacterName, item.CharacterName, StringComparison.Ordinal) + && e.WorldId == item.WorldId)) + { + text += " [DUPLICATE]"; + color = ImGuiColors.DalamudRed; + } + + if (!string.IsNullOrEmpty(text)) + { + text = text.Trim(); + _uiShared.BigText(text, color); + } + + var charaName = item.CharacterName; + if (ImGui.InputText("Character Name", ref charaName, 64)) + { + item.CharacterName = charaName; + _serverConfigurationManager.Save(); + } + + _uiShared.DrawCombo("World##" + item.CharacterName + i, data, (w) => w.Value, + (w) => + { + if (item.WorldId != w.Key) + { + item.WorldId = w.Key; + _serverConfigurationManager.Save(); + } + }, + EqualityComparer>.Default.Equals( + data.FirstOrDefault(f => f.Key == worldIdx), default) + ? data.First() + : data.First(f => f.Key == worldIdx)); + + if (!useOauth) + { + _uiShared.DrawCombo("Secret Key###" + item.CharacterName + i, keys, + (w) => w.Value.FriendlyName, + (w) => + { + if (w.Key != item.SecretKeyIdx) + { + item.SecretKeyIdx = w.Key; + _serverConfigurationManager.Save(); + } + }, + EqualityComparer>.Default.Equals( + keys.FirstOrDefault(f => f.Key == item.SecretKeyIdx), default) + ? keys.First() + : keys.First(f => f.Key == item.SecretKeyIdx)); + } + else + { + _uiShared.DrawUIDComboForAuthentication(i, item, selectedServer.ServerUri, _logger); + } + + bool isAutoLogin = item.AutoLogin; + if (ImGui.Checkbox("Automatically login to Lightless", ref isAutoLogin)) + { + item.AutoLogin = isAutoLogin; + _serverConfigurationManager.Save(); + } + + _uiShared.DrawHelpText( + "When enabled and logging into this character in XIV, Lightless will automatically connect to the current service."); + if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete Character") && + UiSharedService.CtrlPressed()) + _serverConfigurationManager.RemoveCharacterFromServer(idx, item); + UiSharedService.AttachToolTip("Hold CTRL to delete this entry."); + + i++; + if (item != selectedServer.Authentications.ToList()[^1]) + { + ImGuiHelpers.ScaledDummy(5); + ImGui.Separator(); + ImGuiHelpers.ScaledDummy(5); + } + } + + if (selectedServer.Authentications.Any()) + ImGui.Separator(); + + if (!selectedServer.Authentications.Exists(c => + string.Equals(c.CharacterName, youName, StringComparison.Ordinal) + && c.WorldId == youWorld)) + { + if (_uiShared.IconTextButton(FontAwesomeIcon.User, "Add current character")) + { + _serverConfigurationManager.AddCurrentCharacterToServer(idx); + } + + ImGui.SameLine(); + } + + if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Add new character")) + { + _serverConfigurationManager.AddEmptyCharacterToServer(idx); + } + } + else + { + UiSharedService.ColorTextWrapped("You need to add a Secret Key first before adding Characters.", + UIColors.Get("LightlessYellow")); + } + } + + private void DrawServerSecretKeyManagement(ServerStorage selectedServer) + { + foreach (var item in selectedServer.SecretKeys.ToList()) + { + using var id = ImRaii.PushId("key" + item.Key); + var friendlyName = item.Value.FriendlyName; + if (ImGui.InputText("Secret Key Display Name", ref friendlyName, 255)) + { + item.Value.FriendlyName = friendlyName; + _serverConfigurationManager.Save(); + } + + var key = item.Value.Key; + if (ImGui.InputText("Secret Key", ref key, 64)) + { + item.Value.Key = key; + _serverConfigurationManager.Save(); + } + + if (!selectedServer.Authentications.Exists(p => p.SecretKeyIdx == item.Key)) + { + if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete Secret Key") && + UiSharedService.CtrlPressed()) + { + selectedServer.SecretKeys.Remove(item.Key); + _serverConfigurationManager.Save(); + } + + UiSharedService.AttachToolTip("Hold CTRL to delete this secret key entry"); + } + else + { + UiSharedService.ColorTextWrapped("This key is in use and cannot be deleted", + UIColors.Get("LightlessYellow")); + } + + if (item.Key != selectedServer.SecretKeys.Keys.LastOrDefault()) + ImGui.Separator(); + } + + ImGui.Separator(); + if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Add new Secret Key")) + { + selectedServer.SecretKeys.Add( + selectedServer.SecretKeys.Any() ? selectedServer.SecretKeys.Max(p => p.Key) + 1 : 0, + new SecretKey() { FriendlyName = "New Secret Key", }); + _serverConfigurationManager.Save(); + } + } + + private void DrawServerServiceConfiguration(ServerStorage selectedServer, ref bool useOauth) + { + var serverName = selectedServer.ServerName; + var serverUri = selectedServer.ServerUri; + var isMain = string.Equals(serverName, ApiController.MainServer, StringComparison.OrdinalIgnoreCase); + var flags = isMain ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None; + + if (ImGui.InputText("Service URI", ref serverUri, 255, flags)) + { + selectedServer.ServerUri = serverUri; + } + + if (isMain) + { + _uiShared.DrawHelpText("You cannot edit the URI of the main service."); + } + + if (ImGui.InputText("Service Name", ref serverName, 255, flags)) + { + selectedServer.ServerName = serverName; + _serverConfigurationManager.Save(); + } + + if (isMain) + { + _uiShared.DrawHelpText("You cannot edit the name of the main service."); + } + + ImGui.SetNextItemWidth(200); + var serverTransport = _serverConfigurationManager.GetTransport(); + _uiShared.DrawCombo("Server Transport Type", + Enum.GetValues().Where(t => t != HttpTransportType.None), + (v) => v.ToString(), + onSelected: (t) => _serverConfigurationManager.SetTransportType(t), + serverTransport); + _uiShared.DrawHelpText( + "You normally do not need to change this, if you don't know what this is or what it's for, keep it to WebSockets." + + Environment.NewLine + + "If you run into connection issues with e.g. VPNs, try ServerSentEvents first before trying out LongPolling." + + UiSharedService.TooltipSeparator + + "Note: if the server does not support a specific Transport Type it will fall through to the next automatically: WebSockets > ServerSentEvents > LongPolling"); + + ImGuiHelpers.ScaledDummy(5); + + if (ImGui.Checkbox("Use Discord OAuth2 Authentication", ref useOauth)) + { + selectedServer.UseOAuth2 = useOauth; + _serverConfigurationManager.Save(); + } + + _uiShared.DrawHelpText( + "Use Discord OAuth2 Authentication to identify with this server instead of secret keys"); + if (useOauth) + { + _uiShared.DrawOAuth(selectedServer); + if (string.IsNullOrEmpty(_serverConfigurationManager.GetDiscordUserFromToken(selectedServer))) + { + ImGuiHelpers.ScaledDummy(10f); + UiSharedService.ColorTextWrapped( + "You have enabled OAuth2 but it is not linked. Press the buttons Check, then Authenticate to link properly.", + ImGuiColors.DalamudRed); + } + + if (!string.IsNullOrEmpty(_serverConfigurationManager.GetDiscordUserFromToken(selectedServer)) + && selectedServer.Authentications.TrueForAll(u => string.IsNullOrEmpty(u.UID))) + { + ImGuiHelpers.ScaledDummy(10f); + UiSharedService.ColorTextWrapped( + "You have enabled OAuth2 but no characters configured. Set the correct UIDs for your characters in \"Character Management\".", + ImGuiColors.DalamudRed); + } + } + + if (!isMain && selectedServer != _serverConfigurationManager.CurrentServer) + { + ImGui.Separator(); + if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete Service") && + UiSharedService.CtrlPressed()) + { + _serverConfigurationManager.DeleteServer(selectedServer); + } + + _uiShared.DrawHelpText("Hold CTRL to delete this service"); + } + } + + private void DrawServerPermissionSettings(ServerStorage selectedServer) + { + _uiShared.BigText("Default Permission Settings"); + if (selectedServer == _serverConfigurationManager.CurrentServer && _apiController.IsConnected) + { + UiSharedService.TextWrapped( + "Note: The default permissions settings here are not applied retroactively to existing pairs or joined Syncshells."); + UiSharedService.TextWrapped( + "Note: The default permissions settings here are sent and stored on the connected service."); + ImGuiHelpers.ScaledDummy(5f); + var perms = _apiController.DefaultPermissions!; + bool individualIsSticky = perms.IndividualIsSticky; + bool disableIndividualSounds = perms.DisableIndividualSounds; + bool disableIndividualAnimations = perms.DisableIndividualAnimations; + bool disableIndividualVFX = perms.DisableIndividualVFX; + if (ImGui.Checkbox("Individually set permissions become preferred permissions", + ref individualIsSticky)) + { + perms.IndividualIsSticky = individualIsSticky; + _ = _apiController.UserUpdateDefaultPermissions(perms); + } + + _uiShared.DrawHelpText( + "The preferred attribute means that the permissions to that user will never change through any of your permission changes to Syncshells " + + "(i.e. if you have paused one specific user in a Syncshell and they become preferred permissions, then pause and unpause the same Syncshell, the user will remain paused - " + + "if a user does not have preferred permissions, it will follow the permissions of the Syncshell and be unpaused)." + + Environment.NewLine + Environment.NewLine + + "This setting means:" + Environment.NewLine + + " - All new individual pairs get their permissions defaulted to preferred permissions." + + Environment.NewLine + + " - All individually set permissions for any pair will also automatically become preferred permissions. This includes pairs in Syncshells." + + Environment.NewLine + Environment.NewLine + + "It is possible to remove or set the preferred permission state for any pair at any time." + + Environment.NewLine + Environment.NewLine + + "If unsure, leave this setting off."); + ImGuiHelpers.ScaledDummy(3f); + + if (ImGui.Checkbox("Disable individual pair sounds", ref disableIndividualSounds)) + { + perms.DisableIndividualSounds = disableIndividualSounds; + _ = _apiController.UserUpdateDefaultPermissions(perms); + } + + _uiShared.DrawHelpText("This setting will disable sound sync for all new individual pairs."); + if (ImGui.Checkbox("Disable individual pair animations", ref disableIndividualAnimations)) + { + perms.DisableIndividualAnimations = disableIndividualAnimations; + _ = _apiController.UserUpdateDefaultPermissions(perms); + } + + _uiShared.DrawHelpText("This setting will disable animation sync for all new individual pairs."); + if (ImGui.Checkbox("Disable individual pair VFX", ref disableIndividualVFX)) + { + perms.DisableIndividualVFX = disableIndividualVFX; + _ = _apiController.UserUpdateDefaultPermissions(perms); + } + + _uiShared.DrawHelpText("This setting will disable VFX sync for all new individual pairs."); + ImGuiHelpers.ScaledDummy(5f); + bool disableGroundSounds = perms.DisableGroupSounds; + bool disableGroupAnimations = perms.DisableGroupAnimations; + bool disableGroupVFX = perms.DisableGroupVFX; + if (ImGui.Checkbox("Disable Syncshell pair sounds", ref disableGroundSounds)) + { + perms.DisableGroupSounds = disableGroundSounds; + _ = _apiController.UserUpdateDefaultPermissions(perms); + } + + _uiShared.DrawHelpText( + "This setting will disable sound sync for all non-sticky pairs in newly joined syncshells."); + if (ImGui.Checkbox("Disable Syncshell pair animations", ref disableGroupAnimations)) + { + perms.DisableGroupAnimations = disableGroupAnimations; + _ = _apiController.UserUpdateDefaultPermissions(perms); + } + + _uiShared.DrawHelpText( + "This setting will disable animation sync for all non-sticky pairs in newly joined syncshells."); + if (ImGui.Checkbox("Disable Syncshell pair VFX", ref disableGroupVFX)) + { + perms.DisableGroupVFX = disableGroupVFX; + _ = _apiController.UserUpdateDefaultPermissions(perms); + } + + _uiShared.DrawHelpText( + "This setting will disable VFX sync for all non-sticky pairs in newly joined syncshells."); + } + else + { + UiSharedService.ColorTextWrapped("Default Permission Settings unavailable for this service. " + + "You need to connect to this service to change the default permissions since they are stored on the service.", + UIColors.Get("LightlessYellow")); + } + } + private int _lastSelectedServerIndex = -1; private Task<(bool Success, bool PartialSuccess, string Result)>? _secretKeysConversionTask = null; private CancellationTokenSource _secretKeysConversionCts = new(); @@ -3613,58 +3913,39 @@ public class SettingsUi : WindowMediatorSubscriberBase } ImGui.Separator(); - if (ImGui.BeginTabBar("mainTabBar")) + + if (_selectGeneralTabOnNextDraw) { - var generalTabFlags = ImGuiTabItemFlags.None; - if (_selectGeneralTabOnNextDraw) - { - generalTabFlags |= ImGuiTabItemFlags.SetSelected; - } + _selectedMainTab = MainSettingsTab.General; + _selectGeneralTabOnNextDraw = false; + } - if (ImGui.BeginTabItem("General", generalTabFlags)) - { - _selectGeneralTabOnNextDraw = false; + UiSharedService.Tab("MainSettingsTabs", MainTabOptions, ref _selectedMainTab); + ImGuiHelpers.ScaledDummy(5); + + switch (_selectedMainTab) + { + case MainSettingsTab.General: DrawGeneral(); - ImGui.EndTabItem(); - } - - if (ImGui.BeginTabItem("Performance")) - { + break; + case MainSettingsTab.Performance: DrawPerformance(); - ImGui.EndTabItem(); - } - - if (ImGui.BeginTabItem("Storage")) - { + break; + case MainSettingsTab.Storage: DrawFileStorageSettings(); - ImGui.EndTabItem(); - } - - if (ImGui.BeginTabItem("Transfers")) - { + break; + case MainSettingsTab.Transfers: DrawCurrentTransfers(); - ImGui.EndTabItem(); - } - - if (ImGui.BeginTabItem("Service Settings")) - { + break; + case MainSettingsTab.ServiceSettings: DrawServerConfiguration(); - ImGui.EndTabItem(); - } - - if (ImGui.BeginTabItem("Notifications")) - { + break; + case MainSettingsTab.Notifications: DrawNotificationSettings(); - ImGui.EndTabItem(); - } - - if (ImGui.BeginTabItem("Debug")) - { + break; + case MainSettingsTab.Debug: DrawDebug(); - ImGui.EndTabItem(); - } - - ImGui.EndTabBar(); + break; } } @@ -4534,4 +4815,4 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.EndTable(); } } -} \ No newline at end of file +} diff --git a/LightlessSync/UI/UISharedService.cs b/LightlessSync/UI/UISharedService.cs index 537d1bf..2b5431a 100644 --- a/LightlessSync/UI/UISharedService.cs +++ b/LightlessSync/UI/UISharedService.cs @@ -28,8 +28,10 @@ using LightlessSync.WebAPI; using LightlessSync.WebAPI.SignalR; using Microsoft.Extensions.Logging; using System; +using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Numerics; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; @@ -191,6 +193,99 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase return addSuffix ? $"{dblSByte:0.00} {suffix[i]}" : $"{dblSByte:0.00}"; } + public readonly struct TabOption + { + public string Label { get; } + public T Value { get; } + public bool Enabled { get; } + + public TabOption(string label, T value, bool enabled = true) + { + Label = label; + Value = value; + Enabled = enabled; + } + } + + public static bool Tab(string id, IReadOnlyList> options, ref T selectedValue) where T : struct + { + if (options.Count == 0) + return false; + + var pushIdValue = string.IsNullOrEmpty(id) + ? $"UiSharedTab_{RuntimeHelpers.GetHashCode(options):X}" + : id; + using var tabId = ImRaii.PushId(pushIdValue); + + var selectedIndex = -1; + for (var i = 0; i < options.Count; i++) + { + if (!EqualityComparer.Default.Equals(options[i].Value, selectedValue)) + continue; + + selectedIndex = i; + break; + } + + if (selectedIndex == -1 || !options[selectedIndex].Enabled) + selectedIndex = GetFirstEnabledTabIndex(options); + + if (selectedIndex == -1) + return false; + + var changed = DrawTabsInternal(options, ref selectedIndex); + selectedValue = options[Math.Clamp(selectedIndex, 0, options.Count - 1)].Value; + return changed; + } + + private static int GetFirstEnabledTabIndex(IReadOnlyList> options) + { + for (var i = 0; i < options.Count; i++) + { + if (options[i].Enabled) + return i; + } + + return -1; + } + + private static bool DrawTabsInternal(IReadOnlyList> options, ref int selectedIndex) + { + selectedIndex = Math.Clamp(selectedIndex, 0, Math.Max(0, options.Count - 1)); + + var style = ImGui.GetStyle(); + var availableWidth = ImGui.GetContentRegionAvail().X; + var spacingX = style.ItemSpacing.X; + var buttonWidth = options.Count > 0 ? Math.Max(1f, (availableWidth - spacingX * (options.Count - 1)) / options.Count) : availableWidth; + var buttonHeight = Math.Max(ImGui.GetFrameHeight() + style.FramePadding.Y, 28f * ImGuiHelpers.GlobalScale); + var changed = false; + + for (var i = 0; i < options.Count; i++) + { + if (i > 0) + ImGui.SameLine(); + + var tab = options[i]; + var isSelected = i == selectedIndex; + + using (ImRaii.Disabled(!tab.Enabled)) + { + using var tabIndexId = ImRaii.PushId(i); + using var selectedButton = isSelected ? ImRaii.PushColor(ImGuiCol.Button, style.Colors[(int)ImGuiCol.TabActive]) : null; + using var selectedHover = isSelected ? ImRaii.PushColor(ImGuiCol.ButtonHovered, style.Colors[(int)ImGuiCol.TabHovered]) : null; + using var selectedActive = isSelected ? ImRaii.PushColor(ImGuiCol.ButtonActive, style.Colors[(int)ImGuiCol.TabActive]) : null; + + if (ImGui.Button(tab.Label, new Vector2(buttonWidth, buttonHeight))) + { + selectedIndex = i; + changed = true; + } + } + } + + return changed; + } + public static void CenterNextWindow(float width, float height, ImGuiCond cond = ImGuiCond.None) { var center = ImGui.GetMainViewport().GetCenter(); diff --git a/LightlessSync/UI/ZoneChatUi.cs b/LightlessSync/UI/ZoneChatUi.cs index aeddbbb..697f100 100644 --- a/LightlessSync/UI/ZoneChatUi.cs +++ b/LightlessSync/UI/ZoneChatUi.cs @@ -1153,7 +1153,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase } if (ImGui.IsItemHovered()) { - ImGui.SetTooltip("Adjusts the scale of chat message text.\nRight-click to reset."); + ImGui.SetTooltip("Adjust scale of chat message text.\nRight-click to reset to default."); } var windowOpacity = Math.Clamp(chatConfig.ChatWindowOpacity, MinWindowOpacity, MaxWindowOpacity); @@ -1173,7 +1173,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase if (ImGui.IsItemHovered()) { - ImGui.SetTooltip("Adjust transparency of the chat window.\nRight-click to reset to default."); + ImGui.SetTooltip("Adjust chat window transparency.\nRight-click to reset to default."); } ImGui.EndPopup();