From ab369d008e30363f66b172bb692fa62d9cc5f8f4 Mon Sep 17 00:00:00 2001 From: azyges Date: Sun, 21 Dec 2025 01:17:00 +0900 Subject: [PATCH 1/4] can drag chat tabs around as much as u want syncshell tabs can use notes instead by rightclicking and prefering it added some visibility settings (hide in combat, etc) and cleaned up some of the ui --- .../Configurations/ChatConfig.cs | 8 + LightlessSync/Plugin.cs | 2 + .../Services/Chat/ZoneChatService.cs | 116 +++++- LightlessSync/Services/DalamudUtilService.cs | 27 ++ LightlessSync/UI/DataAnalysisUi.cs | 4 +- LightlessSync/UI/ZoneChatUi.cs | 353 +++++++++++++++++- LightlessSync/WebAPI/SignalR/ApiController.cs | 5 +- 7 files changed, 493 insertions(+), 22 deletions(-) diff --git a/LightlessSync/LightlessConfiguration/Configurations/ChatConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/ChatConfig.cs index 9065b81..dcdfc78 100644 --- a/LightlessSync/LightlessConfiguration/Configurations/ChatConfig.cs +++ b/LightlessSync/LightlessConfiguration/Configurations/ChatConfig.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace LightlessSync.LightlessConfiguration.Configurations; @@ -13,4 +14,11 @@ public sealed class ChatConfig : ILightlessConfiguration public bool IsWindowPinned { get; set; } = false; public bool AutoOpenChatOnPluginLoad { get; set; } = false; public float ChatFontScale { get; set; } = 1.0f; + public bool HideInCombat { get; set; } = false; + public bool HideInDuty { get; set; } = false; + public bool ShowWhenUiHidden { get; set; } = true; + public bool ShowInCutscenes { get; set; } = true; + public bool ShowInGpose { get; set; } = true; + public List ChannelOrder { get; set; } = new(); + public Dictionary PreferNotesForChannels { get; set; } = new(StringComparer.Ordinal); } diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index d8e5ee7..58374e3 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -1,5 +1,6 @@ using Dalamud.Game; using Dalamud.Game.ClientState.Objects; +using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Windowing; using Dalamud.Plugin; @@ -105,6 +106,7 @@ public sealed class Plugin : IDalamudPlugin services.AddSingleton(new Dalamud.Localization("LightlessSync.Localization.", string.Empty, useEmbedded: true)); services.AddSingleton(gameGui); services.AddSingleton(addonLifecycle); + services.AddSingleton(pluginInterface.UiBuilder); // Core singletons services.AddSingleton(); diff --git a/LightlessSync/Services/Chat/ZoneChatService.cs b/LightlessSync/Services/Chat/ZoneChatService.cs index 55009ab..6eebf4f 100644 --- a/LightlessSync/Services/Chat/ZoneChatService.cs +++ b/LightlessSync/Services/Chat/ZoneChatService.cs @@ -23,6 +23,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS private readonly DalamudUtilService _dalamudUtilService; private readonly ActorObjectService _actorObjectService; private readonly PairUiService _pairUiService; + private readonly ChatConfigService _chatConfigService; private readonly Lock _sync = new(); @@ -57,6 +58,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS _dalamudUtilService = dalamudUtilService; _actorObjectService = actorObjectService; _pairUiService = pairUiService; + _chatConfigService = chatConfigService; _isLoggedIn = _dalamudUtilService.IsLoggedIn; _isConnected = _apiController.IsConnected; @@ -136,6 +138,42 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS } } + public void MoveChannel(string draggedKey, string targetKey) + { + if (string.IsNullOrWhiteSpace(draggedKey) || string.IsNullOrWhiteSpace(targetKey)) + { + return; + } + + bool updated = false; + using (_sync.EnterScope()) + { + if (!_channels.ContainsKey(draggedKey) || !_channels.ContainsKey(targetKey)) + { + return; + } + + var fromIndex = _channelOrder.IndexOf(draggedKey); + var toIndex = _channelOrder.IndexOf(targetKey); + if (fromIndex < 0 || toIndex < 0 || fromIndex == toIndex) + { + return; + } + + _channelOrder.RemoveAt(fromIndex); + var insertIndex = Math.Clamp(toIndex, 0, _channelOrder.Count); + _channelOrder.Insert(insertIndex, draggedKey); + _chatConfigService.Current.ChannelOrder = new List(_channelOrder); + _chatConfigService.Save(); + updated = true; + } + + if (updated) + { + PublishChannelListChanged(); + } + } + public Task SetChatEnabledAsync(bool enabled) => enabled ? EnableChatAsync() : DisableChatAsync(); @@ -512,7 +550,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS if (!_isLoggedIn || !_apiController.IsConnected) { - await LeaveCurrentZoneAsync(force, 0).ConfigureAwait(false); + await LeaveCurrentZoneAsync(force, 0, 0).ConfigureAwait(false); return; } @@ -520,6 +558,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS { var location = await _dalamudUtilService.GetMapDataAsync().ConfigureAwait(false); var territoryId = (ushort)location.TerritoryId; + var worldId = (ushort)location.ServerId; string? zoneKey; ZoneChannelDefinition? definition = null; @@ -536,14 +575,14 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS if (definition is null) { - await LeaveCurrentZoneAsync(force, territoryId).ConfigureAwait(false); + await LeaveCurrentZoneAsync(force, territoryId, worldId).ConfigureAwait(false); return; } var descriptor = await BuildZoneDescriptorAsync(definition.Value).ConfigureAwait(false); if (descriptor is null) { - await LeaveCurrentZoneAsync(force, territoryId).ConfigureAwait(false); + await LeaveCurrentZoneAsync(force, territoryId, worldId).ConfigureAwait(false); return; } @@ -586,7 +625,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS } } - private async Task LeaveCurrentZoneAsync(bool force, ushort territoryId) + private async Task LeaveCurrentZoneAsync(bool force, ushort territoryId, ushort worldId) { ChatChannelDescriptor? descriptor = null; @@ -602,7 +641,27 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS state.StatusText = !_chatEnabled ? "Chat services disabled" : (_isConnected ? ZoneUnavailableMessage : "Disconnected from chat server"); - state.DisplayName = "Zone Chat"; + if (territoryId != 0 + && _dalamudUtilService.TerritoryData.Value.TryGetValue(territoryId, out var territoryName) + && !string.IsNullOrWhiteSpace(territoryName)) + { + state.DisplayName = territoryName; + } + else + { + state.DisplayName = "Zone Chat"; + } + + if (worldId != 0) + { + state.Descriptor = new ChatChannelDescriptor + { + Type = ChatChannelType.Zone, + WorldId = worldId, + ZoneId = territoryId, + CustomKey = string.Empty + }; + } } if (string.Equals(_activeChannelKey, ZoneChannelKey, StringComparison.Ordinal)) @@ -1092,17 +1151,50 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS { _channelOrder.Clear(); - if (_channels.ContainsKey(ZoneChannelKey)) + var configuredOrder = _chatConfigService.Current.ChannelOrder; + if (configuredOrder.Count > 0) { - _channelOrder.Add(ZoneChannelKey); + var seen = new HashSet(StringComparer.Ordinal); + foreach (var key in configuredOrder) + { + if (_channels.ContainsKey(key) && seen.Add(key)) + { + _channelOrder.Add(key); + } + } + + var remaining = _channels.Values + .Where(state => !seen.Contains(state.Key)) + .ToList(); + + if (remaining.Count > 0) + { + var zoneKeys = remaining + .Where(state => state.Type == ChatChannelType.Zone) + .Select(state => state.Key); + var groupKeys = remaining + .Where(state => state.Type == ChatChannelType.Group) + .OrderBy(state => state.DisplayName, StringComparer.OrdinalIgnoreCase) + .Select(state => state.Key); + + _channelOrder.AddRange(zoneKeys); + _channelOrder.AddRange(groupKeys); + } } + else + { + if (_channels.ContainsKey(ZoneChannelKey)) + { + _channelOrder.Add(ZoneChannelKey); + } - var groups = _channels.Values - .Where(state => state.Type == ChatChannelType.Group) - .OrderBy(state => state.DisplayName, StringComparer.OrdinalIgnoreCase) - .Select(state => state.Key); + var groups = _channels.Values + .Where(state => state.Type == ChatChannelType.Group) + .OrderBy(state => state.DisplayName, StringComparer.OrdinalIgnoreCase) + .Select(state => state.Key); - _channelOrder.AddRange(groups); + _channelOrder.AddRange(groups); + } if (_activeChannelKey is null && _channelOrder.Count > 0) { diff --git a/LightlessSync/Services/DalamudUtilService.cs b/LightlessSync/Services/DalamudUtilService.cs index 06d480b..c8668eb 100644 --- a/LightlessSync/Services/DalamudUtilService.cs +++ b/LightlessSync/Services/DalamudUtilService.cs @@ -239,6 +239,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber public bool IsInCombat { get; private set; } = false; public bool IsPerforming { get; private set; } = false; public bool IsInInstance { get; private set; } = false; + public bool IsInDuty => _condition[ConditionFlag.BoundByDuty]; public bool HasModifiedGameFiles => _gameData.HasModifiedGameDataFiles; public uint ClassJobId => _classJobId!.Value; public Lazy> JobData { get; private set; } @@ -248,6 +249,32 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber public bool IsLodEnabled { get; private set; } public LightlessMediator Mediator { get; } + public bool IsInFieldOperation + { + get + { + if (!IsInDuty) + { + return false; + } + + var territoryId = _clientState.TerritoryType; + if (territoryId == 0) + { + return false; + } + + if (!TerritoryData.Value.TryGetValue(territoryId, out var name) || string.IsNullOrWhiteSpace(name)) + { + return false; + } + + return name.Contains("Eureka", StringComparison.OrdinalIgnoreCase) + || name.Contains("Bozja", StringComparison.OrdinalIgnoreCase) + || name.Contains("Zadnor", StringComparison.OrdinalIgnoreCase); + } + } + public IGameObject? CreateGameObject(IntPtr reference) { EnsureIsOnFramework(); diff --git a/LightlessSync/UI/DataAnalysisUi.cs b/LightlessSync/UI/DataAnalysisUi.cs index 94c8add..2958ebc 100644 --- a/LightlessSync/UI/DataAnalysisUi.cs +++ b/LightlessSync/UI/DataAnalysisUi.cs @@ -27,7 +27,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase { private const float MinTextureFilterPaneWidth = 305f; private const float MaxTextureFilterPaneWidth = 405f; - private const float MinTextureDetailPaneWidth = 580f; + private const float MinTextureDetailPaneWidth = 480f; private const float MaxTextureDetailPaneWidth = 720f; private const float SelectedFilePanelLogicalHeight = 90f; private static readonly Vector4 SelectedTextureRowTextColor = new(0f, 0f, 0f, 1f); @@ -111,7 +111,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase _hasUpdate = true; }); WindowBuilder.For(this) - .SetSizeConstraints(new Vector2(1650, 1000), new Vector2(3840, 2160)) + .SetSizeConstraints(new Vector2(1240, 680), new Vector2(3840, 2160)) .Apply(); _conversionProgress.ProgressChanged += ConversionProgress_ProgressChanged; diff --git a/LightlessSync/UI/ZoneChatUi.cs b/LightlessSync/UI/ZoneChatUi.cs index 6944759..668bcb8 100644 --- a/LightlessSync/UI/ZoneChatUi.cs +++ b/LightlessSync/UI/ZoneChatUi.cs @@ -12,6 +12,7 @@ using LightlessSync.LightlessConfiguration.Models; using LightlessSync.Services; using LightlessSync.Services.Chat; using LightlessSync.Services.Mediator; +using LightlessSync.Services.ServerConfiguration; using LightlessSync.UI.Services; using LightlessSync.Utils; using LightlessSync.WebAPI; @@ -23,8 +24,10 @@ namespace LightlessSync.UI; public sealed class ZoneChatUi : WindowMediatorSubscriberBase { private const string ChatDisabledStatus = "Chat services disabled"; + private const string ZoneUnavailableStatus = "Zone chat is only available in major cities."; private const string SettingsPopupId = "zone_chat_settings_popup"; private const string ReportPopupId = "Report Message##zone_chat_report_popup"; + private const string ChannelDragPayloadId = "zone_chat_channel_drag"; private const float DefaultWindowOpacity = .97f; private const float MinWindowOpacity = 0.05f; private const float MaxWindowOpacity = 1f; @@ -32,6 +35,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase private const float MaxChatFontScale = 1.5f; private const int ReportReasonMaxLength = 500; private const int ReportContextMaxLength = 1000; + private const int MaxChannelNoteTabLength = 25; private readonly UiSharedService _uiSharedService; private readonly ZoneChatService _zoneChatService; @@ -39,6 +43,9 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase private readonly LightlessProfileManager _profileManager; private readonly ApiController _apiController; private readonly ChatConfigService _chatConfigService; + private readonly ServerConfigurationManager _serverConfigurationManager; + private readonly DalamudUtilService _dalamudUtilService; + private readonly IUiBuilder _uiBuilder; private readonly Dictionary _draftMessages = new(StringComparer.Ordinal); private readonly ImGuiWindowFlags _unpinnedWindowFlags; private float _currentWindowOpacity = DefaultWindowOpacity; @@ -61,6 +68,10 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase private bool _reportSubmitting; private string? _reportError; private ChatReportResult? _reportSubmissionResult; + private string? _dragChannelKey; + private string? _dragHoverKey; + private bool _HideStateActive; + private bool _HideStateWasOpen; public ZoneChatUi( ILogger logger, @@ -70,6 +81,9 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase PairUiService pairUiService, LightlessProfileManager profileManager, ChatConfigService chatConfigService, + ServerConfigurationManager serverConfigurationManager, + DalamudUtilService dalamudUtilService, + IUiBuilder uiBuilder, ApiController apiController, PerformanceCollectorService performanceCollectorService) : base(logger, mediator, "Lightless Chat", performanceCollectorService) @@ -79,6 +93,9 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase _pairUiService = pairUiService; _profileManager = profileManager; _chatConfigService = chatConfigService; + _serverConfigurationManager = serverConfigurationManager; + _dalamudUtilService = dalamudUtilService; + _uiBuilder = uiBuilder; _apiController = apiController; _isWindowPinned = _chatConfigService.Current.IsWindowPinned; _showRulesOverlay = _chatConfigService.Current.ShowRulesOverlayOnOpen; @@ -88,6 +105,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase } _unpinnedWindowFlags = Flags; RefreshWindowFlags(); + ApplyUiVisibilitySettings(); Size = new Vector2(450, 420) * ImGuiHelpers.GlobalScale; SizeCondition = ImGuiCond.FirstUseEver; WindowBuilder.For(this) @@ -98,6 +116,8 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase Mediator.Subscribe(this, OnChatChannelMessageAdded); Mediator.Subscribe(this, _ => _scrollToBottom = true); + Mediator.Subscribe(this, _ => UpdateHideState()); + Mediator.Subscribe(this, _ => UpdateHideState()); } public override void PreDraw() @@ -108,6 +128,55 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase ImGui.SetNextWindowBgAlpha(_currentWindowOpacity); } + private void UpdateHideState() + { + ApplyUiVisibilitySettings(); + var shouldHide = ShouldHide(); + if (shouldHide) + { + _HideStateWasOpen |= IsOpen; + if (IsOpen) + { + IsOpen = false; + } + _HideStateActive = true; + } + else if (_HideStateActive) + { + if (_HideStateWasOpen) + { + IsOpen = true; + } + _HideStateActive = false; + _HideStateWasOpen = false; + } + } + + private void ApplyUiVisibilitySettings() + { + var config = _chatConfigService.Current; + _uiBuilder.DisableAutomaticUiHide = config.ShowWhenUiHidden; + _uiBuilder.DisableCutsceneUiHide = config.ShowInCutscenes; + _uiBuilder.DisableGposeUiHide = config.ShowInGpose; + } + + private bool ShouldHide() + { + var config = _chatConfigService.Current; + + if (config.HideInCombat && _dalamudUtilService.IsInCombat) + { + return true; + } + + if (config.HideInDuty && _dalamudUtilService.IsInDuty && !_dalamudUtilService.IsInFieldOperation) + { + return true; + } + + return false; + } + protected override void DrawInternal() { var childBgColor = ImGui.GetStyle().Colors[(int)ImGuiCol.ChildBg]; @@ -155,7 +224,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase } } - private static void DrawHeader(ChatChannelSnapshot channel) + private void DrawHeader(ChatChannelSnapshot channel) { var prefix = channel.Type == ChatChannelType.Zone ? "Zone" : "Syncshell"; Vector4 color; @@ -178,11 +247,18 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase if (channel.Type == ChatChannelType.Zone && channel.Descriptor.WorldId != 0) { ImGui.SameLine(); - ImGui.TextUnformatted($"World #{channel.Descriptor.WorldId}"); + var worldId = channel.Descriptor.WorldId; + var worldName = _dalamudUtilService.WorldData.Value.TryGetValue(worldId, out var name) ? name : $"World #{worldId}"; + ImGui.TextUnformatted(worldName); + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip($"World ID: {worldId}"); + } } - var showInlineDisabled = string.Equals(channel.StatusText, ChatDisabledStatus, StringComparison.OrdinalIgnoreCase); - if (showInlineDisabled) + var showInlineStatus = string.Equals(channel.StatusText, ChatDisabledStatus, StringComparison.OrdinalIgnoreCase) + || string.Equals(channel.StatusText, ZoneUnavailableStatus, StringComparison.OrdinalIgnoreCase); + if (showInlineStatus) { ImGui.SameLine(); ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3); @@ -324,6 +400,15 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase _refocusChatInputKey = null; } ImGui.InputText(inputId, ref draft, MaxMessageLength); + if (ImGui.IsItemActive() || ImGui.IsItemFocused()) + { + var drawList = ImGui.GetWindowDrawList(); + var itemMin = ImGui.GetItemRectMin(); + var itemMax = ImGui.GetItemRectMax(); + var highlight = UIColors.Get("LightlessPurple").WithAlpha(0.35f); + var highlightU32 = ImGui.ColorConvertFloat4ToU32(highlight); + drawList.AddRect(itemMin, itemMax, highlightU32, style.FrameRounding, ImDrawFlags.None, Math.Max(1f, ImGuiHelpers.GlobalScale)); + } var enterPressed = ImGui.IsItemFocused() && (ImGui.IsKeyPressed(ImGuiKey.Enter) || ImGui.IsKeyPressed(ImGuiKey.KeypadEnter)); _draftMessages[channel.Key] = draft; @@ -480,7 +565,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase ImGui.Separator(); _uiSharedService.MediumText("Syncshell Chat Rules", UIColors.Get("LightlessYellow")); - _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), new SeStringUtils.RichTextEntry("Syncshell chats are self-moderated (their own set rules) by it's owner and appointed moderators. If they fail to enforce chat rules within their syncshell, the owner (and its moderators) may face punishment.")); + _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), new SeStringUtils.RichTextEntry("Syncshell chats are self-moderated (their own set rules) by it's owner and appointed moderators.")); ImGui.Dummy(new Vector2(5)); @@ -1187,6 +1272,71 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase ImGui.SetTooltip("Toggles the timestamp prefix on messages."); } + ImGui.Separator(); + ImGui.TextUnformatted("Chat Visibility"); + + var autoHideCombat = chatConfig.HideInCombat; + if (ImGui.Checkbox("Hide in combat", ref autoHideCombat)) + { + chatConfig.HideInCombat = autoHideCombat; + _chatConfigService.Save(); + UpdateHideState(); + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Temporarily hides the chat window while in combat."); + } + + var autoHideDuty = chatConfig.HideInDuty; + if (ImGui.Checkbox("Hide in duty (Not in field operations)", ref autoHideDuty)) + { + chatConfig.HideInDuty = autoHideDuty; + _chatConfigService.Save(); + UpdateHideState(); + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Hides the chat window inside duties."); + } + + var showWhenUiHidden = chatConfig.ShowWhenUiHidden; + if (ImGui.Checkbox("Show when game UI is hidden", ref showWhenUiHidden)) + { + chatConfig.ShowWhenUiHidden = showWhenUiHidden; + _chatConfigService.Save(); + UpdateHideState(); + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Allow the chat window to remain visible when the game UI is hidden."); + } + + var showInCutscenes = chatConfig.ShowInCutscenes; + if (ImGui.Checkbox("Show in cutscenes", ref showInCutscenes)) + { + chatConfig.ShowInCutscenes = showInCutscenes; + _chatConfigService.Save(); + UpdateHideState(); + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Allow the chat window to remain visible during cutscenes."); + } + + var showInGpose = chatConfig.ShowInGpose; + if (ImGui.Checkbox("Show in group pose", ref showInGpose)) + { + chatConfig.ShowInGpose = showInGpose; + _chatConfigService.Save(); + UpdateHideState(); + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Allow the chat window to remain visible in /gpose."); + } + + ImGui.Separator(); + var fontScale = Math.Clamp(chatConfig.ChatFontScale, MinChatFontScale, MaxChatFontScale); var fontScaleChanged = ImGui.SliderFloat("Message font scale", ref fontScale, MinChatFontScale, MaxChatFontScale, "%.2fx"); var resetFontScale = ImGui.IsItemClicked(ImGuiMouseButton.Right); @@ -1244,7 +1394,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase }); } - private void DrawChannelButtons(IReadOnlyList channels) + private unsafe void DrawChannelButtons(IReadOnlyList channels) { var style = ImGui.GetStyle(); var baseFramePadding = style.FramePadding; @@ -1305,6 +1455,8 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase { if (child) { + var dragActive = _dragChannelKey is not null && ImGui.IsMouseDragging(ImGuiMouseButton.Left); + var hoveredTargetThisFrame = false; var first = true; foreach (var channel in channels) { @@ -1315,6 +1467,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase var showBadge = !isSelected && channel.UnreadCount > 0; var isZoneChannel = channel.Type == ChatChannelType.Zone; (string Text, Vector2 TextSize, float Width, float Height)? badgeMetrics = null; + var channelLabel = GetChannelTabLabel(channel); var normal = isSelected ? UIColors.Get("LightlessPurpleDefault") : UIColors.Get("ButtonDefault"); var hovered = isSelected @@ -1343,7 +1496,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase badgeMetrics = (badgeText, badgeTextSize, badgeWidth, badgeHeight); } - var clicked = ImGui.Button($"{channel.DisplayName}##chat_channel_{channel.Key}"); + var clicked = ImGui.Button($"{channelLabel}##chat_channel_{channel.Key}"); if (showBadge) { @@ -1359,10 +1512,77 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase _scrollToBottom = true; } + if (ShouldShowChannelTabContextMenu(channel) + && ImGui.BeginPopupContextItem($"chat_channel_ctx##{channel.Key}")) + { + DrawChannelTabContextMenu(channel); + ImGui.EndPopup(); + } + + if (ImGui.BeginDragDropSource(ImGuiDragDropFlags.None)) + { + if (!string.Equals(_dragChannelKey, channel.Key, StringComparison.Ordinal)) + { + _dragHoverKey = null; + } + + _dragChannelKey = channel.Key; + ImGui.SetDragDropPayload(ChannelDragPayloadId, null, 0); + ImGui.TextUnformatted(channelLabel); + ImGui.EndDragDropSource(); + } + + var isDragTarget = false; + + if (ImGui.BeginDragDropTarget()) + { + var acceptFlags = ImGuiDragDropFlags.AcceptBeforeDelivery | ImGuiDragDropFlags.AcceptNoDrawDefaultRect; + var payload = ImGui.AcceptDragDropPayload(ChannelDragPayloadId, acceptFlags); + if (!payload.IsNull && _dragChannelKey is { } draggedKey + && !string.Equals(draggedKey, channel.Key, StringComparison.Ordinal)) + { + isDragTarget = true; + if (!string.Equals(_dragHoverKey, channel.Key, StringComparison.Ordinal)) + { + _dragHoverKey = channel.Key; + _zoneChatService.MoveChannel(draggedKey, channel.Key); + } + } + + ImGui.EndDragDropTarget(); + } + + var isHoveredDuringDrag = dragActive + && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem | ImGuiHoveredFlags.AllowWhenOverlapped); + + if (!isDragTarget && isHoveredDuringDrag + && !string.Equals(_dragChannelKey, channel.Key, StringComparison.Ordinal)) + { + isDragTarget = true; + if (!string.Equals(_dragHoverKey, channel.Key, StringComparison.Ordinal)) + { + _dragHoverKey = channel.Key; + _zoneChatService.MoveChannel(_dragChannelKey!, channel.Key); + } + } + var drawList = ImGui.GetWindowDrawList(); var itemMin = ImGui.GetItemRectMin(); var itemMax = ImGui.GetItemRectMax(); + if (isHoveredDuringDrag) + { + var highlight = UIColors.Get("LightlessPurple").WithAlpha(0.35f); + var highlightU32 = ImGui.ColorConvertFloat4ToU32(highlight); + drawList.AddRectFilled(itemMin, itemMax, highlightU32, style.FrameRounding); + drawList.AddRect(itemMin, itemMax, highlightU32, style.FrameRounding, ImDrawFlags.None, Math.Max(1f, ImGuiHelpers.GlobalScale)); + } + + if (isDragTarget) + { + hoveredTargetThisFrame = true; + } + if (isZoneChannel) { var borderColor = UIColors.Get("LightlessOrange"); @@ -1390,6 +1610,11 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase first = false; } + if (dragActive && !hoveredTargetThisFrame) + { + _dragHoverKey = null; + } + if (_pendingChannelScroll.HasValue) { ImGui.SetScrollX(_pendingChannelScroll.Value); @@ -1430,9 +1655,123 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase _channelScroll = currentScroll; _channelScrollMax = maxScroll; + if (_dragChannelKey is not null && !ImGui.IsMouseDown(ImGuiMouseButton.Left)) + { + _dragChannelKey = null; + _dragHoverKey = null; + } + ImGui.SetCursorPosY(ImGui.GetCursorPosY() - style.ItemSpacing.Y * 0.3f); } + private string GetChannelTabLabel(ChatChannelSnapshot channel) + { + if (channel.Type != ChatChannelType.Group) + { + return channel.DisplayName; + } + + if (!_chatConfigService.Current.PreferNotesForChannels.TryGetValue(channel.Key, out var preferNote) || !preferNote) + { + return channel.DisplayName; + } + + var note = GetChannelNote(channel); + if (string.IsNullOrWhiteSpace(note)) + { + return channel.DisplayName; + } + + return TruncateChannelNoteForTab(note); + } + + private static string TruncateChannelNoteForTab(string note) + { + if (note.Length <= MaxChannelNoteTabLength) + { + return note; + } + + var ellipsis = "..."; + var maxPrefix = Math.Max(0, MaxChannelNoteTabLength - ellipsis.Length); + return note[..maxPrefix] + ellipsis; + } + + private bool ShouldShowChannelTabContextMenu(ChatChannelSnapshot channel) + { + if (channel.Type != ChatChannelType.Group) + { + return false; + } + + if (_chatConfigService.Current.PreferNotesForChannels.TryGetValue(channel.Key, out var preferNote) && preferNote) + { + return true; + } + + var note = GetChannelNote(channel); + return !string.IsNullOrWhiteSpace(note); + } + + private void DrawChannelTabContextMenu(ChatChannelSnapshot channel) + { + var preferNote = _chatConfigService.Current.PreferNotesForChannels.TryGetValue(channel.Key, out var value) && value; + var note = GetChannelNote(channel); + var hasNote = !string.IsNullOrWhiteSpace(note); + if (preferNote || hasNote) + { + var label = preferNote ? "Prefer name instead" : "Prefer note instead"; + if (ImGui.MenuItem(label)) + { + SetPreferNoteForChannel(channel.Key, !preferNote); + } + } + + if (preferNote) + { + ImGui.Separator(); + ImGui.TextDisabled("Name:"); + ImGui.TextWrapped(channel.DisplayName); + } + + if (hasNote) + { + ImGui.Separator(); + ImGui.TextDisabled("Note:"); + ImGui.TextWrapped(note); + } + } + + private string? GetChannelNote(ChatChannelSnapshot channel) + { + if (channel.Type != ChatChannelType.Group) + { + return null; + } + + var gid = channel.Descriptor.CustomKey; + if (string.IsNullOrWhiteSpace(gid)) + { + return null; + } + + return _serverConfigurationManager.GetNoteForGid(gid); + } + + private void SetPreferNoteForChannel(string channelKey, bool preferNote) + { + if (preferNote) + { + _chatConfigService.Current.PreferNotesForChannels[channelKey] = true; + } + else + { + _chatConfigService.Current.PreferNotesForChannels.Remove(channelKey); + } + + _chatConfigService.Save(); + } + private void DrawSystemEntry(ChatMessageEntry entry) { var system = entry.SystemMessage; diff --git a/LightlessSync/WebAPI/SignalR/ApiController.cs b/LightlessSync/WebAPI/SignalR/ApiController.cs index af98de9..011a6d8 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.cs @@ -584,7 +584,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL OnGroupSendInfo((dto) => _ = Client_GroupSendInfo(dto)); OnGroupUpdateProfile((dto) => _ = Client_GroupSendProfile(dto)); OnGroupChangeUserPairPermissions((dto) => _ = Client_GroupChangeUserPairPermissions(dto)); - _lightlessHub.On(nameof(Client_ChatReceive), (Func)Client_ChatReceive); + if (!_initialized) + { + _lightlessHub.On(nameof(Client_ChatReceive), (Func)Client_ChatReceive); + } OnGposeLobbyJoin((dto) => _ = Client_GposeLobbyJoin(dto)); OnGposeLobbyLeave((dto) => _ = Client_GposeLobbyLeave(dto)); From 7c7a98f7708564c002be6099ecab2293635bc8da Mon Sep 17 00:00:00 2001 From: azyges Date: Sun, 21 Dec 2025 01:19:17 +0900 Subject: [PATCH 2/4] This looks better --- LightlessSync/UI/ZoneChatUi.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LightlessSync/UI/ZoneChatUi.cs b/LightlessSync/UI/ZoneChatUi.cs index 668bcb8..d45427c 100644 --- a/LightlessSync/UI/ZoneChatUi.cs +++ b/LightlessSync/UI/ZoneChatUi.cs @@ -1720,7 +1720,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase var hasNote = !string.IsNullOrWhiteSpace(note); if (preferNote || hasNote) { - var label = preferNote ? "Prefer name instead" : "Prefer note instead"; + var label = preferNote ? "Prefer Name Instead" : "Prefer Note Instead"; if (ImGui.MenuItem(label)) { SetPreferNoteForChannel(channel.Key, !preferNote); From b99f68a8912331f0b84b40c37431a5f3835aef47 Mon Sep 17 00:00:00 2001 From: azyges Date: Sun, 21 Dec 2025 02:23:18 +0900 Subject: [PATCH 3/4] collapsible texture details --- LightlessSync/UI/DataAnalysisUi.cs | 211 ++++++++++++++++++++++++----- Penumbra.Api | 2 +- 2 files changed, 177 insertions(+), 36 deletions(-) diff --git a/LightlessSync/UI/DataAnalysisUi.cs b/LightlessSync/UI/DataAnalysisUi.cs index 2958ebc..a4bbf9f 100644 --- a/LightlessSync/UI/DataAnalysisUi.cs +++ b/LightlessSync/UI/DataAnalysisUi.cs @@ -29,6 +29,9 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase private const float MaxTextureFilterPaneWidth = 405f; private const float MinTextureDetailPaneWidth = 480f; private const float MaxTextureDetailPaneWidth = 720f; + private const float TextureFilterSplitterWidth = 8f; + private const float TextureDetailSplitterWidth = 12f; + private const float TextureDetailSplitterCollapsedWidth = 18f; private const float SelectedFilePanelLogicalHeight = 90f; private static readonly Vector4 SelectedTextureRowTextColor = new(0f, 0f, 0f, 1f); @@ -80,6 +83,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase private bool _modalOpen = false; private bool _showModal = false; private bool _textureRowsDirty = true; + private bool _textureDetailCollapsed = false; private bool _conversionFailed; private bool _showAlreadyAddedTransients = false; private bool _acknowledgeReview = false; @@ -1205,35 +1209,52 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase var availableSize = ImGui.GetContentRegionAvail(); var windowPos = ImGui.GetWindowPos(); var spacingX = ImGui.GetStyle().ItemSpacing.X; - var splitterWidth = 6f * scale; + var filterSplitterWidth = TextureFilterSplitterWidth * scale; + var detailSplitterWidth = (_textureDetailCollapsed ? TextureDetailSplitterCollapsedWidth : TextureDetailSplitterWidth) * scale; + var totalSplitterWidth = filterSplitterWidth + detailSplitterWidth; + var totalSpacing = 2 * spacingX; const float minFilterWidth = MinTextureFilterPaneWidth; const float minDetailWidth = MinTextureDetailPaneWidth; const float minCenterWidth = 340f; - var dynamicFilterMax = Math.Max(minFilterWidth, availableSize.X - minDetailWidth - minCenterWidth - 2 * (splitterWidth + spacingX)); + var detailMinForLayout = _textureDetailCollapsed ? 0f : minDetailWidth; + var dynamicFilterMax = Math.Max(minFilterWidth, availableSize.X - detailMinForLayout - minCenterWidth - totalSplitterWidth - totalSpacing); var filterMaxBound = Math.Min(MaxTextureFilterPaneWidth, dynamicFilterMax); var filterWidth = Math.Clamp(_textureFilterPaneWidth, minFilterWidth, filterMaxBound); - var dynamicDetailMax = Math.Max(minDetailWidth, availableSize.X - filterWidth - minCenterWidth - 2 * (splitterWidth + spacingX)); - var detailMaxBound = Math.Min(MaxTextureDetailPaneWidth, dynamicDetailMax); - var detailWidth = Math.Clamp(_textureDetailPaneWidth, minDetailWidth, detailMaxBound); + var dynamicDetailMax = Math.Max(detailMinForLayout, availableSize.X - filterWidth - minCenterWidth - totalSplitterWidth - totalSpacing); + var detailMaxBound = _textureDetailCollapsed ? 0f : Math.Min(MaxTextureDetailPaneWidth, dynamicDetailMax); + var detailWidth = _textureDetailCollapsed ? 0f : Math.Clamp(_textureDetailPaneWidth, minDetailWidth, detailMaxBound); - var centerWidth = availableSize.X - filterWidth - detailWidth - 2 * (splitterWidth + spacingX); + var centerWidth = availableSize.X - filterWidth - detailWidth - totalSplitterWidth - totalSpacing; if (centerWidth < minCenterWidth) { var deficit = minCenterWidth - centerWidth; - detailWidth = Math.Clamp(detailWidth - deficit, minDetailWidth, - Math.Min(MaxTextureDetailPaneWidth, Math.Max(minDetailWidth, availableSize.X - filterWidth - minCenterWidth - 2 * (splitterWidth + spacingX)))); - centerWidth = availableSize.X - filterWidth - detailWidth - 2 * (splitterWidth + spacingX); - if (centerWidth < minCenterWidth) + if (!_textureDetailCollapsed) + { + detailWidth = Math.Clamp(detailWidth - deficit, minDetailWidth, + Math.Min(MaxTextureDetailPaneWidth, Math.Max(minDetailWidth, availableSize.X - filterWidth - minCenterWidth - totalSplitterWidth - totalSpacing))); + centerWidth = availableSize.X - filterWidth - detailWidth - totalSplitterWidth - totalSpacing; + if (centerWidth < minCenterWidth) + { + deficit = minCenterWidth - centerWidth; + filterWidth = Math.Clamp(filterWidth - deficit, minFilterWidth, + Math.Min(MaxTextureFilterPaneWidth, Math.Max(minFilterWidth, availableSize.X - detailWidth - minCenterWidth - totalSplitterWidth - totalSpacing))); + detailWidth = Math.Clamp(detailWidth, minDetailWidth, + Math.Min(MaxTextureDetailPaneWidth, Math.Max(minDetailWidth, availableSize.X - filterWidth - minCenterWidth - totalSplitterWidth - totalSpacing))); + centerWidth = availableSize.X - filterWidth - detailWidth - totalSplitterWidth - totalSpacing; + if (centerWidth < minCenterWidth) + { + centerWidth = minCenterWidth; + } + } + } + else { - deficit = minCenterWidth - centerWidth; filterWidth = Math.Clamp(filterWidth - deficit, minFilterWidth, - Math.Min(MaxTextureFilterPaneWidth, Math.Max(minFilterWidth, availableSize.X - detailWidth - minCenterWidth - 2 * (splitterWidth + spacingX)))); - detailWidth = Math.Clamp(detailWidth, minDetailWidth, - Math.Min(MaxTextureDetailPaneWidth, Math.Max(minDetailWidth, availableSize.X - filterWidth - minCenterWidth - 2 * (splitterWidth + spacingX)))); - centerWidth = availableSize.X - filterWidth - detailWidth - 2 * (splitterWidth + spacingX); + Math.Min(MaxTextureFilterPaneWidth, Math.Max(minFilterWidth, availableSize.X - minCenterWidth - totalSplitterWidth - totalSpacing))); + centerWidth = availableSize.X - filterWidth - detailWidth - totalSplitterWidth - totalSpacing; if (centerWidth < minCenterWidth) { centerWidth = minCenterWidth; @@ -1242,7 +1263,10 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase } _textureFilterPaneWidth = filterWidth; - _textureDetailPaneWidth = detailWidth; + if (!_textureDetailCollapsed) + { + _textureDetailPaneWidth = detailWidth; + } ImGui.BeginGroup(); using (var filters = ImRaii.Child("textureFilters", new Vector2(filterWidth, 0), true)) @@ -1264,8 +1288,8 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase var filterMax = ImGui.GetItemRectMax(); var filterHeight = filterMax.Y - filterMin.Y; var filterTopLocal = filterMin - windowPos; - var maxFilterResize = Math.Min(MaxTextureFilterPaneWidth, Math.Max(minFilterWidth, availableSize.X - minCenterWidth - minDetailWidth - 2 * (splitterWidth + spacingX))); - DrawVerticalResizeHandle("##textureFilterSplitter", filterTopLocal.Y, filterHeight, ref _textureFilterPaneWidth, minFilterWidth, maxFilterResize); + var maxFilterResize = Math.Min(MaxTextureFilterPaneWidth, Math.Max(minFilterWidth, availableSize.X - minCenterWidth - detailMinForLayout - totalSplitterWidth - totalSpacing)); + DrawVerticalResizeHandle("##textureFilterSplitter", filterTopLocal.Y, filterHeight, ref _textureFilterPaneWidth, minFilterWidth, maxFilterResize, out _); TextureRow? selectedRow; ImGui.BeginGroup(); @@ -1279,15 +1303,36 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase var tableMax = ImGui.GetItemRectMax(); var tableHeight = tableMax.Y - tableMin.Y; var tableTopLocal = tableMin - windowPos; - var maxDetailResize = Math.Min(MaxTextureDetailPaneWidth, Math.Max(minDetailWidth, availableSize.X - _textureFilterPaneWidth - minCenterWidth - 2 * (splitterWidth + spacingX))); - DrawVerticalResizeHandle("##textureDetailSplitter", tableTopLocal.Y, tableHeight, ref _textureDetailPaneWidth, minDetailWidth, maxDetailResize, invert: true); - - ImGui.BeginGroup(); - using (var detailChild = ImRaii.Child("textureDetailPane", new Vector2(detailWidth, 0), true)) + var maxDetailResize = Math.Min(MaxTextureDetailPaneWidth, Math.Max(minDetailWidth, availableSize.X - _textureFilterPaneWidth - minCenterWidth - totalSplitterWidth - totalSpacing)); + var detailToggle = DrawVerticalResizeHandle( + "##textureDetailSplitter", + tableTopLocal.Y, + tableHeight, + ref _textureDetailPaneWidth, + minDetailWidth, + maxDetailResize, + out var detailDragging, + invert: true, + showToggle: true, + isCollapsed: _textureDetailCollapsed); + if (detailToggle) { - DrawTextureDetail(selectedRow); + _textureDetailCollapsed = !_textureDetailCollapsed; + } + if (_textureDetailCollapsed && detailDragging) + { + _textureDetailCollapsed = false; + } + + if (!_textureDetailCollapsed) + { + ImGui.BeginGroup(); + using (var detailChild = ImRaii.Child("textureDetailPane", new Vector2(detailWidth, 0), true)) + { + DrawTextureDetail(selectedRow); + } + ImGui.EndGroup(); } - ImGui.EndGroup(); } private void DrawTextureFilters( @@ -1935,26 +1980,118 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase } } - private void DrawVerticalResizeHandle(string id, float topY, float height, ref float leftWidth, float minWidth, float maxWidth, bool invert = false) + private bool DrawVerticalResizeHandle( + string id, + float topY, + float height, + ref float leftWidth, + float minWidth, + float maxWidth, + out bool isDragging, + bool invert = false, + bool showToggle = false, + bool isCollapsed = false) { var scale = ImGuiHelpers.GlobalScale; - var splitterWidth = 8f * scale; + var splitterWidth = (showToggle + ? (isCollapsed ? TextureDetailSplitterCollapsedWidth : TextureDetailSplitterWidth) + : TextureFilterSplitterWidth) * scale; ImGui.SameLine(); var cursor = ImGui.GetCursorPos(); - ImGui.SetCursorPos(new Vector2(cursor.X, topY)); - ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("ButtonDefault")); - ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessPurple")); - ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessPurpleActive")); - ImGui.Button(id, new Vector2(splitterWidth, height)); - ImGui.PopStyleColor(3); + var contentMin = ImGui.GetWindowContentRegionMin(); + var contentMax = ImGui.GetWindowContentRegionMax(); + var clampedTop = MathF.Max(topY, contentMin.Y); + var clampedBottom = MathF.Min(topY + height, contentMax.Y); + var clampedHeight = MathF.Max(0f, clampedBottom - clampedTop); + var splitterRounding = ImGui.GetStyle().FrameRounding; + ImGui.SetCursorPos(new Vector2(cursor.X, clampedTop)); + if (clampedHeight <= 0f) + { + isDragging = false; + ImGui.SetCursorPos(new Vector2(cursor.X + splitterWidth + ImGui.GetStyle().ItemSpacing.X, cursor.Y)); + return false; + } - if (ImGui.IsItemActive()) + ImGui.InvisibleButton(id, new Vector2(splitterWidth, clampedHeight)); + var drawList = ImGui.GetWindowDrawList(); + var rectMin = ImGui.GetItemRectMin(); + var rectMax = ImGui.GetItemRectMax(); + var windowPos = ImGui.GetWindowPos(); + var clipMin = windowPos + contentMin; + var clipMax = windowPos + contentMax; + drawList.PushClipRect(clipMin, clipMax, true); + var clipInset = 1f * scale; + var drawMin = new Vector2( + MathF.Max(rectMin.X, clipMin.X), + MathF.Max(rectMin.Y, clipMin.Y)); + var drawMax = new Vector2( + MathF.Min(rectMax.X, clipMax.X - clipInset), + MathF.Min(rectMax.Y, clipMax.Y)); + var hovered = ImGui.IsItemHovered(); + isDragging = ImGui.IsItemActive(); + var baseColor = UIColors.Get("ButtonDefault"); + var hoverColor = UIColors.Get("LightlessPurple"); + var activeColor = UIColors.Get("LightlessPurpleActive"); + var handleColor = isDragging ? activeColor : hovered ? hoverColor : baseColor; + drawList.AddRectFilled(drawMin, drawMax, UiSharedService.Color(handleColor), splitterRounding); + drawList.AddRect(drawMin, drawMax, UiSharedService.Color(new Vector4(1f, 1f, 1f, 0.12f)), splitterRounding); + + bool toggleHovered = false; + bool toggleClicked = false; + if (showToggle) + { + var icon = isCollapsed ? FontAwesomeIcon.ChevronRight : FontAwesomeIcon.ChevronLeft; + Vector2 iconSize; + using (_uiSharedService.IconFont.Push()) + { + iconSize = ImGui.CalcTextSize(icon.ToIconString()); + } + + var toggleHeight = MathF.Min(clampedHeight, 64f * scale); + var toggleMin = new Vector2( + drawMin.X, + drawMin.Y + (drawMax.Y - drawMin.Y - toggleHeight) / 2f); + var toggleMax = new Vector2( + drawMax.X, + toggleMin.Y + toggleHeight); + var toggleColorBase = UIColors.Get("LightlessPurple"); + toggleHovered = ImGui.IsMouseHoveringRect(toggleMin, toggleMax); + var toggleBg = toggleHovered + ? new Vector4(toggleColorBase.X, toggleColorBase.Y, toggleColorBase.Z, 0.65f) + : new Vector4(toggleColorBase.X, toggleColorBase.Y, toggleColorBase.Z, 0.35f); + if (toggleHovered) + { + UiSharedService.AttachToolTip(isCollapsed ? "Show texture details." : "Hide texture details."); + } + + drawList.AddRectFilled(toggleMin, toggleMax, UiSharedService.Color(toggleBg), splitterRounding); + drawList.AddRect(toggleMin, toggleMax, UiSharedService.Color(toggleColorBase), splitterRounding); + + var iconPos = new Vector2( + drawMin.X + (drawMax.X - drawMin.X - iconSize.X) / 2f, + drawMin.Y + (drawMax.Y - drawMin.Y - iconSize.Y) / 2f); + using (_uiSharedService.IconFont.Push()) + { + drawList.AddText(iconPos, ImGui.GetColorU32(ImGuiCol.Text), icon.ToIconString()); + } + + if (toggleHovered && ImGui.IsMouseReleased(ImGuiMouseButton.Left) && !ImGui.IsMouseDragging(ImGuiMouseButton.Left)) + { + toggleClicked = true; + } + } + + if (isDragging && !toggleHovered) { var delta = ImGui.GetIO().MouseDelta.X / scale; leftWidth += invert ? -delta : delta; leftWidth = Math.Clamp(leftWidth, minWidth, maxWidth); } + + drawList.PopClipRect(); + ImGui.SetCursorPos(new Vector2(cursor.X + splitterWidth + ImGui.GetStyle().ItemSpacing.X, cursor.Y)); + return toggleClicked; } private (IDalamudTextureWrap? Texture, bool IsLoading, string? Error) GetTexturePreview(TextureRow row) @@ -2094,7 +2231,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase } else { - ImGui.TextDisabled("-"); + _uiSharedService.IconText(FontAwesomeIcon.Check, ImGuiColors.DalamudWhite); UiSharedService.AttachToolTip("Already stored in a compressed format; additional compression is disabled."); } @@ -2175,6 +2312,10 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase _textureSelections[key] = target; currentSelection = target; } + if (TextureMetadataHelper.TryGetRecommendationInfo(target, out var targetInfo)) + { + UiSharedService.AttachToolTip($"{targetInfo.Title}{UiSharedService.TooltipSeparator}{targetInfo.Description}"); + } if (targetSelected) { ImGui.SetItemDefaultFocus(); diff --git a/Penumbra.Api b/Penumbra.Api index 1750c41..52a3216 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 1750c41b53e1000c99a7fb9d8a0f082aef639a41 +Subproject commit 52a3216a525592205198303df2844435e382cf87 From 03105e0755b7c842bd81dc5314bd72eec9b43eb4 Mon Sep 17 00:00:00 2001 From: azyges Date: Sun, 21 Dec 2025 04:28:36 +0900 Subject: [PATCH 4/4] fix log level --- LightlessSync/Services/LightFinder/LightFinderPlateHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LightlessSync/Services/LightFinder/LightFinderPlateHandler.cs b/LightlessSync/Services/LightFinder/LightFinderPlateHandler.cs index 544ada1..d78563c 100644 --- a/LightlessSync/Services/LightFinder/LightFinderPlateHandler.cs +++ b/LightlessSync/Services/LightFinder/LightFinderPlateHandler.cs @@ -641,8 +641,8 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe return; _activeBroadcastingCids = newSet; - if (_logger.IsEnabled(LogLevel.Information)) - _logger.LogInformation("Active broadcast CIDs: {Cids}", string.Join(',', _activeBroadcastingCids)); + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("Active broadcast IDs: {Cids}", string.Join(',', _activeBroadcastingCids)); FlagRefresh(); }