From c3c60e7e43de63d1920c2a91febcf307377643e6 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 11 Jul 2024 15:50:17 +1000 Subject: [PATCH] feat: add autohide after inactivity After not receiving a message for X seconds (configurable) in the current tab or any tab with unread mode enabled, the chat will be hidden. Focus can be returned with return or slash as usual. Having input focus or hovering the mouse over the chat window "bumps" it every frame. Also fixes a bug that prevented focus from being restored to tabs with input disabled. The chat window will be brought back but the activated event won't be fully processed. Co-authored-by: Auri --- ChatTwo/Configuration.cs | 10 ++++++ ChatTwo/GameFunctions/Chat.cs | 3 -- ChatTwo/Resources/Language.Designer.cs | 36 +++++++++++++++++++ ChatTwo/Resources/Language.resx | 12 +++++++ ChatTwo/Ui/ChatLogWindow.cs | 49 +++++++++++++++++++++++--- ChatTwo/Ui/SettingsTabs/Display.cs | 15 ++++++-- 6 files changed, 115 insertions(+), 10 deletions(-) diff --git a/ChatTwo/Configuration.cs b/ChatTwo/Configuration.cs index f9bf730..2afa0c7 100755 --- a/ChatTwo/Configuration.cs +++ b/ChatTwo/Configuration.cs @@ -20,6 +20,8 @@ internal class Configuration : IPluginConfiguration public bool HideWhenUiHidden = true; public bool HideInLoadingScreens; public bool HideInBattle; + public bool HideWhenInactive; + public int InactivityHideTimeout = 10; public bool ShowHideButton = true; public bool NativeItemTooltips = true; public bool PrettierTimestamps = true; @@ -78,6 +80,8 @@ internal class Configuration : IPluginConfiguration HideWhenUiHidden = other.HideWhenUiHidden; HideInLoadingScreens = other.HideInLoadingScreens; HideInBattle = other.HideInBattle; + HideWhenInactive = other.HideWhenInactive; + InactivityHideTimeout = other.InactivityHideTimeout; ShowHideButton = other.ShowHideButton; NativeItemTooltips = other.NativeItemTooltips; PrettierTimestamps = other.PrettierTimestamps; @@ -168,6 +172,9 @@ internal class Tab [NonSerialized] public uint Unread; + [NonSerialized] + public long LastMessageTime; + [NonSerialized] public MessageList Messages = new(); @@ -192,7 +199,10 @@ internal class Tab { Messages.AddPrune(message, MessageManager.MessageDisplayLimit); if (unread) + { Unread += 1; + LastMessageTime = Environment.TickCount64; + } } internal void Clear() diff --git a/ChatTwo/GameFunctions/Chat.cs b/ChatTwo/GameFunctions/Chat.cs index 0eea03f..421784f 100755 --- a/ChatTwo/GameFunctions/Chat.cs +++ b/ChatTwo/GameFunctions/Chat.cs @@ -253,9 +253,6 @@ internal sealed unsafe class Chat : IDisposable LastRefresh = Environment.TickCount64; } - if (Plugin.ChatLogWindow is { CurrentTab.InputDisabled: true, IsHidden: false }) - return; - // Vanilla text input has focus if (RaptureAtkModule.Instance()->AtkModule.IsTextInputActive()) return; diff --git a/ChatTwo/Resources/Language.Designer.cs b/ChatTwo/Resources/Language.Designer.cs index 022c8ac..606f01a 100755 --- a/ChatTwo/Resources/Language.Designer.cs +++ b/ChatTwo/Resources/Language.Designer.cs @@ -2417,6 +2417,24 @@ namespace ChatTwo.Resources { } } + /// + /// Looks up a localized string similar to Hide the chat after a configurable period of inactivity. The current tab and any tabs with unread indicators enabled are considered for activity.. + /// + internal static string Options_HideWhenInactive_Description { + get { + return ResourceManager.GetString("Options_HideWhenInactive_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Hide when inactive. + /// + internal static string Options_HideWhenInactive_Name { + get { + return ResourceManager.GetString("Options_HideWhenInactive_Name", resourceCulture); + } + } + /// /// Looks up a localized string similar to Hide {0} when you are not logged in to a character.. /// @@ -2453,6 +2471,24 @@ namespace ChatTwo.Resources { } } + /// + /// Looks up a localized string similar to How long to wait (in seconds) before considering the chat log inactive.. + /// + internal static string Options_InactivityHideTimeout_Description { + get { + return ResourceManager.GetString("Options_InactivityHideTimeout_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Inactivity timeout. + /// + internal static string Options_InactivityHideTimeout_Name { + get { + return ResourceManager.GetString("Options_InactivityHideTimeout_Name", resourceCulture); + } + } + /// /// Looks up a localized string similar to The font {0} will use to display Japanese text.. /// diff --git a/ChatTwo/Resources/Language.resx b/ChatTwo/Resources/Language.resx index f535927..ec6eb53 100644 --- a/ChatTwo/Resources/Language.resx +++ b/ChatTwo/Resources/Language.resx @@ -406,6 +406,18 @@ Hide {0} when the game UI is hidden. + + Hide when inactive + + + Hide the chat after a configurable period of inactivity. The current tab and any tabs with unread indicators enabled are considered for activity. + + + Inactivity timeout + + + How long to wait (in seconds) before considering the chat log inactive. + Keybind mode diff --git a/ChatTwo/Ui/ChatLogWindow.cs b/ChatTwo/Ui/ChatLogWindow.cs index 74ac4dd..b8ff8b1 100644 --- a/ChatTwo/Ui/ChatLogWindow.cs +++ b/ChatTwo/Ui/ChatLogWindow.cs @@ -85,6 +85,9 @@ public sealed class ChatLogWindow : Window private const uint ChatCloseSfx = 3u; private bool PlayedClosingSound = true; + private long FrameTime; // set every frame + private long LastActivityTime = Environment.TickCount64; + private readonly ExcelSheet WorldSheet; private readonly ExcelSheet LogFilterSheet; private readonly ExcelSheet TextCommandSheet; @@ -148,6 +151,18 @@ public sealed class ChatLogWindow : Window private void Activated(ChatActivatedArgs args) { Activate = true; + PlayedClosingSound = false; + if (Plugin.Config.PlaySounds) + UIModule.PlaySound(ChatOpenSfx); + + // Don't set the channel or text content when activating a disabled tab. + if (CurrentTab?.InputDisabled == true) + { + // The closing sound would've been immediately played in this case. + PlayedClosingSound = true; + return; + } + if (args.AddIfNotPresent != null && !Chat.Contains(args.AddIfNotPresent)) Chat += args.AddIfNotPresent; @@ -203,10 +218,6 @@ public sealed class ChatLogWindow : Window if (info.Text != null && Chat.Length == 0) Chat = info.Text; - - PlayedClosingSound = false; - if (Plugin.Config.PlaySounds) - UIModule.PlaySound(ChatOpenSfx); } private bool IsValidCommand(string command) @@ -471,7 +482,23 @@ public sealed class ChatLogWindow : Window public override bool DrawConditions() { - return !IsHidden; + FrameTime = Environment.TickCount64; + if (IsHidden) + return false; + if (!Plugin.Config.HideWhenInactive || Activate) + { + LastActivityTime = FrameTime; + return true; + } + + var currentTab = CurrentTab; // local to avoid calling the getter repeatedly + var lastActivityTime = Plugin.Config.Tabs + .Where(tab => tab.UnreadMode is not UnreadMode.None || tab == currentTab) + .Select(tab => tab.LastMessageTime) + .DefaultIfEmpty(0) + .Max(); + lastActivityTime = Math.Max(lastActivityTime, LastActivityTime); + return FrameTime - lastActivityTime <= 1000 * Plugin.Config.InactivityHideTimeout; } public override void PreDraw() @@ -485,6 +512,12 @@ public sealed class ChatLogWindow : Window public override void PostDraw() { + // Set Activate to false after draw to avoid repeatedly trying to focus + // the text input in a tab with input disabled. The usual way that + // Activate gets disabled is via the text input callback, but that + // doesn't get called if the input is disabled. + if (CurrentTab?.InputDisabled == true) + Activate = false; if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null }) StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == Plugin.Config.ChosenStyle)?.Pop(); } @@ -740,7 +773,10 @@ public sealed class ChatLogWindow : Window } if (ImGui.IsItemActive()) + { HandleKeybinds(true); + LastActivityTime = FrameTime; + } // Only trigger unfocused if we are currently not calling the auto complete if (!Activate && !ImGui.IsItemActive() && AutoCompleteInfo == null) @@ -785,6 +821,9 @@ public sealed class ChatLogWindow : Window UserHide(); } + if (ImGui.IsWindowHovered(ImGuiHoveredFlags.ChildWindows)) + LastActivityTime = FrameTime; + if (!showNovice) return; diff --git a/ChatTwo/Ui/SettingsTabs/Display.cs b/ChatTwo/Ui/SettingsTabs/Display.cs index 36a0171..e3d773f 100755 --- a/ChatTwo/Ui/SettingsTabs/Display.cs +++ b/ChatTwo/Ui/SettingsTabs/Display.cs @@ -1,7 +1,5 @@ using ChatTwo.Resources; using ChatTwo.Util; -using Dalamud.Interface.Style; -using Dalamud.Interface.Utility.Raii; using ImGuiNET; namespace ChatTwo.Ui.SettingsTabs; @@ -39,6 +37,19 @@ internal sealed class Display : ISettingsTab ImGuiUtil.OptionCheckbox(ref Mutable.HideInBattle, Language.Options_HideInBattle_Name, Language.Options_HideInBattle_Description); ImGui.Spacing(); + ImGuiUtil.OptionCheckbox(ref Mutable.HideWhenInactive, Language.Options_HideWhenInactive_Name, Language.Options_HideWhenInactive_Description); + ImGui.Spacing(); + + if (Mutable.HideWhenInactive) + { + ImGuiUtil.InputIntVertical(Language.Options_InactivityHideTimeout_Name, + Language.Options_InactivityHideTimeout_Description, ref Mutable.InactivityHideTimeout, 1, 10); + // Enforce a minimum of 2 seconds to avoid people soft locking + // themselves. + Mutable.InactivityHideTimeout = Math.Max(2, Mutable.InactivityHideTimeout); + ImGui.Spacing(); + } + ImGuiUtil.OptionCheckbox(ref Mutable.PrettierTimestamps, Language.Options_PrettierTimestamps_Name, Language.Options_PrettierTimestamps_Description); if (Mutable.PrettierTimestamps)