diff --git a/ChatTwo/Configuration.cs b/ChatTwo/Configuration.cs index 139282a..3c66b4f 100755 --- a/ChatTwo/Configuration.cs +++ b/ChatTwo/Configuration.cs @@ -3,6 +3,7 @@ using ChatTwo.Code; using ChatTwo.GameFunctions.Types; using ChatTwo.Resources; using ChatTwo.Ui; +using ChatTwo.Util; using Dalamud.Configuration; using Dalamud.Game.ClientState.Keys; using ImGuiNET; @@ -43,6 +44,10 @@ internal class Configuration : IPluginConfiguration public bool HideInBattle; public bool HideWhenInactive; public int InactivityHideTimeout = 10; + public bool InactivityHideActiveDuringBattle = true; + public Dictionary InactivityHideChannels = TabsUtil.AllChannels(); + public bool InactivityHideExtraChatAll = true; + public HashSet InactivityHideExtraChatChannels = []; public bool ShowHideButton = true; public bool NativeItemTooltips = true; public bool PrettierTimestamps = true; @@ -106,6 +111,10 @@ internal class Configuration : IPluginConfiguration HideInBattle = other.HideInBattle; HideWhenInactive = other.HideWhenInactive; InactivityHideTimeout = other.InactivityHideTimeout; + InactivityHideActiveDuringBattle = other.InactivityHideActiveDuringBattle; + InactivityHideChannels = other.InactivityHideChannels.ToDictionary(entry => entry.Key, entry => entry.Value); + InactivityHideExtraChatAll = other.InactivityHideExtraChatAll; + InactivityHideExtraChatChannels = other.InactivityHideExtraChatChannels.ToHashSet(); ShowHideButton = other.ShowHideButton; NativeItemTooltips = other.NativeItemTooltips; PrettierTimestamps = other.PrettierTimestamps; @@ -188,6 +197,7 @@ internal class Tab public HashSet ExtraChatChannels = []; public UnreadMode UnreadMode = UnreadMode.Unseen; + public bool UnhideOnActivity; public bool DisplayTimestamp = true; public InputChannel? Channel; public bool PopOut; @@ -199,7 +209,7 @@ internal class Tab public uint Unread; [NonSerialized] - public long LastMessageTime; + public long LastActivity; [NonSerialized] public MessageList Messages = new(); @@ -210,31 +220,20 @@ internal class Tab [NonSerialized] public Guid Identifier = Guid.NewGuid(); - internal bool Matches(Message message) - { - if (message.ExtraChatChannel != Guid.Empty) - return ExtraChatAll || ExtraChatChannels.Contains(message.ExtraChatChannel); - - return message.Code.Type.IsGm() - || ChatCodes.TryGetValue(message.Code.Type, out var sources) - && (message.Code.Source is 0 or (ChatSource) 1 - || sources.HasFlag(message.Code.Source)); - } + internal bool Matches(Message message) => message.Matches(ChatCodes, ExtraChatAll, ExtraChatChannels); internal void AddMessage(Message message, bool unread = true) { Messages.AddPrune(message, MessageManager.MessageDisplayLimit); - if (unread) - { - Unread += 1; - LastMessageTime = Environment.TickCount64; - } + if (!unread) + return; + Unread += 1; + + if (message.Matches(Plugin.Config.InactivityHideChannels, Plugin.Config.InactivityHideExtraChatAll, Plugin.Config.InactivityHideExtraChatChannels)) + LastActivity = Environment.TickCount64; } - internal void Clear() - { - Messages.Clear(); - } + internal void Clear() => Messages.Clear(); internal Tab Clone() { @@ -245,6 +244,7 @@ internal class Tab ExtraChatAll = ExtraChatAll, ExtraChatChannels = ExtraChatChannels.ToHashSet(), UnreadMode = UnreadMode, + UnhideOnActivity = UnhideOnActivity, DisplayTimestamp = DisplayTimestamp, Channel = Channel, PopOut = PopOut, diff --git a/ChatTwo/GameFunctions/KeybindManager.cs b/ChatTwo/GameFunctions/KeybindManager.cs index 811921c..ecc3a95 100644 --- a/ChatTwo/GameFunctions/KeybindManager.cs +++ b/ChatTwo/GameFunctions/KeybindManager.cs @@ -10,8 +10,6 @@ using FFXIVClientStructs.FFXIV.Client.UI; using ImGuiNET; using ModifierFlag = ChatTwo.GameFunctions.Types.ModifierFlag; -using ModifierFlag = ChatTwo.GameFunctions.Types.ModifierFlag; - namespace ChatTwo.GameFunctions; internal enum KeyboardSource { @@ -95,8 +93,8 @@ internal unsafe class KeybindManager : IDisposable { // List of keys that can be used as a part of keybinds while the chat is // focused WITHOUT modifiers. All other keys can only be used if their - // configured keybind contains modifiers. This allows for using e.g. F11 to - // change chat channel while typing. + // configured keybind contains modifiers (except only SHIFT). This allows + // for using e.g. F11 to change chat channel while typing. private static readonly IReadOnlyCollection ModifierlessChatKeys = new[] { // VirtualKey.NO_KEY, diff --git a/ChatTwo/Message.cs b/ChatTwo/Message.cs index d517da0..499f19d 100755 --- a/ChatTwo/Message.cs +++ b/ChatTwo/Message.cs @@ -124,6 +124,17 @@ internal partial class Message return new Message(0, 0, code, [], content, new SeString(), new SeString()); } + internal bool Matches(Dictionary channels, bool allExtraChatChannels, HashSet extraChatChannels) + { + if (ExtraChatChannel != Guid.Empty) + return allExtraChatChannels || extraChatChannels.Contains(ExtraChatChannel); + + return Code.Type.IsGm() + || channels.TryGetValue(Code.Type, out var sources) + && (Code.Source is 0 or (ChatSource) 1 + || sources.HasFlag(Code.Source)); + } + private int GenerateHash() { return SortCode.GetHashCode() diff --git a/ChatTwo/Resources/Language.Designer.cs b/ChatTwo/Resources/Language.Designer.cs index def1633..d1336b9 100755 --- a/ChatTwo/Resources/Language.Designer.cs +++ b/ChatTwo/Resources/Language.Designer.cs @@ -2481,7 +2481,7 @@ 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.. + /// Looks up a localized string similar to Hide the chat after a configurable period of inactivity. The current tab and any other tabs with the setting enabled are considered for activity.. /// internal static string Options_HideWhenInactive_Description { get { @@ -2534,6 +2534,69 @@ namespace ChatTwo.Resources { } } + /// + /// Looks up a localized string similar to When disabled, the chat log will stay active during battle.. + /// + internal static string Options_InactivityHideActiveDuringBattle_Description { + get { + return ResourceManager.GetString("Options_InactivityHideActiveDuringBattle_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Enable inactivity hide during battle. + /// + internal static string Options_InactivityHideActiveDuringBattle_Name { + get { + return ResourceManager.GetString("Options_InactivityHideActiveDuringBattle_Name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Select All. + /// + internal static string Options_InactivityHideChannels_All_Label { + get { + return ResourceManager.GetString("Options_InactivityHideChannels_All_Label", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Hold Ctrl+Shift to click.. + /// + internal static string Options_InactivityHideChannels_Button_Tooltip { + get { + return ResourceManager.GetString("Options_InactivityHideChannels_Button_Tooltip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Which chat channels should be considered for activity. Other channels will not restore the chat regardless of which tab they occur in.. + /// + internal static string Options_InactivityHideChannels_Description { + get { + return ResourceManager.GetString("Options_InactivityHideChannels_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Chat channels considered for activity. + /// + internal static string Options_InactivityHideChannels_Name { + get { + return ResourceManager.GetString("Options_InactivityHideChannels_Name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unselect All. + /// + internal static string Options_InactivityHideChannels_None_Label { + get { + return ResourceManager.GetString("Options_InactivityHideChannels_None_Label", resourceCulture); + } + } + /// /// Looks up a localized string similar to How long to wait (in seconds) before considering the chat log inactive.. /// @@ -3137,6 +3200,15 @@ namespace ChatTwo.Resources { } } + /// + /// Looks up a localized string similar to Unhide the chat window on activity. + /// + internal static string Options_Tabs_InactivityBehaviour { + get { + return ResourceManager.GetString("Options_Tabs_InactivityBehaviour", resourceCulture); + } + } + /// /// Looks up a localized string similar to Use different opacity than main window. /// diff --git a/ChatTwo/Resources/Language.resx b/ChatTwo/Resources/Language.resx index 9018653..4a457e4 100644 --- a/ChatTwo/Resources/Language.resx +++ b/ChatTwo/Resources/Language.resx @@ -220,6 +220,9 @@ Unread mode + + Unhide the chat window on activity + <None> @@ -410,7 +413,7 @@ 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. + Hide the chat after a configurable period of inactivity. The current tab and any other tabs with the setting enabled are considered for activity. Inactivity timeout @@ -418,6 +421,27 @@ How long to wait (in seconds) before considering the chat log inactive. + + Enable inactivity hide during battle + + + When disabled, the chat log will stay active during battle. + + + Chat channels considered for activity + + + Which chat channels should be considered for activity. Other channels will not restore the chat regardless of which tab they occur in. + + + Select All + + + Unselect All + + + Hold Ctrl+Shift to click. + Keybind mode diff --git a/ChatTwo/Ui/ChatLogWindow.cs b/ChatTwo/Ui/ChatLogWindow.cs index 4e5b48f..344b7a9 100644 --- a/ChatTwo/Ui/ChatLogWindow.cs +++ b/ChatTwo/Ui/ChatLogWindow.cs @@ -87,7 +87,7 @@ public sealed class ChatLogWindow : Window private bool PlayedClosingSound = true; private long FrameTime; // set every frame - private long LastActivityTime = Environment.TickCount64; + internal long LastActivityTime = Environment.TickCount64; private readonly ExcelSheet WorldSheet; private readonly ExcelSheet LogFilterSheet; @@ -348,7 +348,10 @@ public sealed class ChatLogWindow : Window return height; } - internal void ChangeTab(int index) => WantedTab = index; + internal void ChangeTab(int index) { + WantedTab = index; + LastActivityTime = FrameTime; + } internal void ChangeTabDelta(int offset) { @@ -373,7 +376,7 @@ public sealed class ChatLogWindow : Window SetChannel(tab.Channel ?? tab.PreviousChannel); } - private static bool InBattle => Plugin.Condition[ConditionFlag.InCombat]; + internal static bool InBattle => Plugin.Condition[ConditionFlag.InCombat]; private static bool GposeActive => Plugin.Condition[ConditionFlag.WatchingCutscene]; private static bool CutsceneActive => Plugin.Condition[ConditionFlag.OccupiedInCutSceneEvent] || Plugin.Condition[ConditionFlag.WatchingCutscene78]; @@ -448,7 +451,8 @@ public sealed class ChatLogWindow : Window FrameTime = Environment.TickCount64; if (IsHidden) return false; - if (!Plugin.Config.HideWhenInactive || Activate) + + if (!Plugin.Config.HideWhenInactive || (!Plugin.Config.InactivityHideActiveDuringBattle && InBattle) || Activate) { LastActivityTime = FrameTime; return true; @@ -456,11 +460,10 @@ public sealed class ChatLogWindow : Window 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) + .Where(tab => !tab.PopOut && (tab.UnhideOnActivity || tab == currentTab)) + .Select(tab => tab.LastActivity) + .Append(LastActivityTime) .Max(); - lastActivityTime = Math.Max(lastActivityTime, LastActivityTime); return FrameTime - lastActivityTime <= 1000 * Plugin.Config.InactivityHideTimeout; } diff --git a/ChatTwo/Ui/Popout.cs b/ChatTwo/Ui/Popout.cs index 18a9e18..b713c06 100644 --- a/ChatTwo/Ui/Popout.cs +++ b/ChatTwo/Ui/Popout.cs @@ -12,6 +12,9 @@ internal class Popout : Window private readonly Tab Tab; private readonly int Idx; + private long FrameTime; // set every frame + private long LastActivityTime = Environment.TickCount64; + public Popout(ChatLogWindow chatLogWindow, Tab tab, int idx) : base($"{tab.Name}##popout") { ChatLogWindow = chatLogWindow; @@ -34,7 +37,20 @@ internal class Popout : Window public override bool DrawConditions() { - return !ChatLogWindow.IsHidden; + FrameTime = Environment.TickCount64; + if (ChatLogWindow.IsHidden) + return false; + + if (!Plugin.Config.HideWhenInactive || (!Plugin.Config.InactivityHideActiveDuringBattle && ChatLogWindow.InBattle) || !Tab.UnhideOnActivity) + { + LastActivityTime = FrameTime; + return true; + } + + // Activity in the tab, this popout window, or the main chat log window. + var lastActivityTime = Math.Max(Tab.LastActivity, LastActivityTime); + lastActivityTime = Math.Max(lastActivityTime, ChatLogWindow.LastActivityTime); + return FrameTime - lastActivityTime <= 1000 * Plugin.Config.InactivityHideTimeout; } public override void PreDraw() @@ -65,6 +81,9 @@ internal class Popout : Window var handler = ChatLogWindow.HandlerLender.Borrow(); ChatLogWindow.DrawMessageLog(Tab, handler, ImGui.GetContentRegionAvail().Y, false); + + if (ImGui.IsWindowHovered(ImGuiHoveredFlags.ChildWindows)) + LastActivityTime = FrameTime; } public override void PostDraw() diff --git a/ChatTwo/Ui/SettingsTabs/Display.cs b/ChatTwo/Ui/SettingsTabs/Display.cs index e3d773f..66d9844 100755 --- a/ChatTwo/Ui/SettingsTabs/Display.cs +++ b/ChatTwo/Ui/SettingsTabs/Display.cs @@ -1,5 +1,6 @@ using ChatTwo.Resources; using ChatTwo.Util; +using Dalamud.Interface.Utility.Raii; using ImGuiNET; namespace ChatTwo.Ui.SettingsTabs; @@ -37,27 +38,70 @@ internal sealed class Display : ISettingsTab ImGuiUtil.OptionCheckbox(ref Mutable.HideInBattle, Language.Options_HideInBattle_Name, Language.Options_HideInBattle_Description); ImGui.Spacing(); + ImGui.Separator(); + ImGui.Spacing(); + ImGuiUtil.OptionCheckbox(ref Mutable.HideWhenInactive, Language.Options_HideWhenInactive_Name, Language.Options_HideWhenInactive_Description); ImGui.Spacing(); if (Mutable.HideWhenInactive) { + using var _ = ImRaii.PushIndent(); 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(); + + // This setting conflicts with HideInBattle, so it's disabled. + using (ImRaii.Disabled(Mutable.HideInBattle)) + { + ImGuiUtil.OptionCheckbox(ref Mutable.InactivityHideActiveDuringBattle, + Language.Options_InactivityHideActiveDuringBattle_Name, + Language.Options_InactivityHideActiveDuringBattle_Description); + ImGui.Spacing(); + } + + using var channelTree = ImRaii.TreeNode(Language.Options_InactivityHideChannels_Name); + if (channelTree.Success) + { + if (ImGuiUtil.CtrlShiftButton(Language.Options_InactivityHideChannels_All_Label, + Language.Options_InactivityHideChannels_Button_Tooltip)) + { + Mutable.InactivityHideChannels = TabsUtil.AllChannels(); + Mutable.InactivityHideExtraChatAll = true; + Mutable.InactivityHideExtraChatChannels = []; + } + + ImGui.SameLine(); + if (ImGuiUtil.CtrlShiftButton(Language.Options_InactivityHideChannels_None_Label, + Language.Options_InactivityHideChannels_Button_Tooltip)) + { + Mutable.InactivityHideChannels = new(); + Mutable.InactivityHideExtraChatAll = false; + Mutable.InactivityHideExtraChatChannels = []; + } + + ImGui.Spacing(); + + ImGuiUtil.ChannelSelector(Language.Options_Tabs_Channels, Mutable.InactivityHideChannels); + ImGuiUtil.ExtraChatSelector(Language.Options_Tabs_ExtraChatChannels, + ref Mutable.InactivityHideExtraChatAll, Mutable.InactivityHideExtraChatChannels); + } + ImGui.Spacing(); } + ImGui.Separator(); + ImGui.Spacing(); + ImGuiUtil.OptionCheckbox(ref Mutable.PrettierTimestamps, Language.Options_PrettierTimestamps_Name, Language.Options_PrettierTimestamps_Description); if (Mutable.PrettierTimestamps) { - ImGui.TreePush(); + using var _ = ImRaii.PushIndent(); ImGuiUtil.OptionCheckbox(ref Mutable.MoreCompactPretty, Language.Options_MoreCompactPretty_Name, Language.Options_MoreCompactPretty_Description); ImGuiUtil.OptionCheckbox(ref Mutable.HideSameTimestamps, Language.Options_HideSameTimestamps_Name, Language.Options_HideSameTimestamps_Description); - ImGui.TreePop(); } ImGui.Spacing(); diff --git a/ChatTwo/Ui/SettingsTabs/Tabs.cs b/ChatTwo/Ui/SettingsTabs/Tabs.cs index be98190..8c99b34 100755 --- a/ChatTwo/Ui/SettingsTabs/Tabs.cs +++ b/ChatTwo/Ui/SettingsTabs/Tabs.cs @@ -108,6 +108,9 @@ internal sealed class Tabs : ISettingsTab } } + if (Mutable.HideWhenInactive) + ImGui.Checkbox(Language.Options_Tabs_InactivityBehaviour, ref tab.UnhideOnActivity); + ImGui.Checkbox(Language.Options_Tabs_NoInput, ref tab.InputDisabled); if (!tab.InputDisabled) { @@ -124,81 +127,8 @@ internal sealed class Tabs : ISettingsTab } } - using (var channelNode = ImRaii.TreeNode(Language.Options_Tabs_Channels)) - { - if (channelNode) - { - foreach (var (header, types) in ChatTypeExt.SortOrder) - { - using var headerNode = ImRaii.TreeNode(header + $"##{i}"); - if (!headerNode.Success) - continue; - - foreach (var type in types) - { - if (type.IsGm()) - continue; - - var enabled = tab.ChatCodes.ContainsKey(type); - if (ImGui.Checkbox($"##{type.Name()}-{i}", ref enabled)) - { - if (enabled) - tab.ChatCodes[type] = ChatSourceExt.All; - else - tab.ChatCodes.Remove(type); - } - - ImGui.SameLine(); - - if (!type.HasSource()) - { - ImGui.TextUnformatted(type.Name()); - continue; - } - - using var typeNode = ImRaii.TreeNode($"{type.Name()}##{i}"); - if (!typeNode.Success) - continue; - - tab.ChatCodes.TryGetValue(type, out var sourcesEnum); - var sources = (uint) sourcesEnum; - - foreach (var source in Enum.GetValues()) - if (ImGui.CheckboxFlags(source.Name(), ref sources, (uint) source)) - tab.ChatCodes[type] = (ChatSource) sources; - } - } - } - } - - - if (Plugin.ExtraChat.ChannelNames.Count <= 0) - continue; - - using var extraTree = ImRaii.TreeNode(Language.Options_Tabs_ExtraChatChannels); - if (!extraTree.Success) - continue; - - ImGui.Checkbox(Language.Options_Tabs_ExtraChatAll, ref tab.ExtraChatAll); - ImGui.Separator(); - - if (tab.ExtraChatAll) - ImGui.BeginDisabled(); - - foreach (var (id, name) in Plugin.ExtraChat.ChannelNames) - { - var enabled = tab.ExtraChatChannels.Contains(id); - if (!ImGui.Checkbox($"{name}##ec-{id}", ref enabled)) - continue; - - if (enabled) - tab.ExtraChatChannels.Add(id); - else - tab.ExtraChatChannels.Remove(id); - } - - if (tab.ExtraChatAll) - ImGui.EndDisabled(); + ImGuiUtil.ChannelSelector(Language.Options_Tabs_Channels, tab.ChatCodes); + ImGuiUtil.ExtraChatSelector(Language.Options_Tabs_ExtraChatChannels, ref tab.ExtraChatAll, tab.ExtraChatChannels); } if (toRemove > -1) diff --git a/ChatTwo/Util/ImGuiUtil.cs b/ChatTwo/Util/ImGuiUtil.cs index 4937854..f26fd24 100755 --- a/ChatTwo/Util/ImGuiUtil.cs +++ b/ChatTwo/Util/ImGuiUtil.cs @@ -1,5 +1,6 @@ using System.Numerics; using System.Text; +using ChatTwo.Code; using ChatTwo.GameFunctions.Types; using ChatTwo.Resources; using Dalamud.Game.ClientState.Keys; @@ -549,6 +550,80 @@ internal static class ImGuiUtil return new EndUnconditionally(ImGui.PopTextWrapPos, true); } + public static void ChannelSelector(string headerText, Dictionary chatCodes) + { + using var channelNode = ImRaii.TreeNode(headerText); + if (!channelNode) + return; + + foreach (var (header, types) in ChatTypeExt.SortOrder) + { + using var headerNode = ImRaii.TreeNode(header); + if (!headerNode.Success) + continue; + + foreach (var type in types) + { + if (type.IsGm()) + continue; + + var enabled = chatCodes.ContainsKey(type); + if (ImGui.Checkbox($"##{type.Name()}", ref enabled)) + { + if (enabled) + chatCodes[type] = ChatSourceExt.All; + else + chatCodes.Remove(type); + } + + ImGui.SameLine(); + + if (!type.HasSource()) + { + ImGui.TextUnformatted(type.Name()); + continue; + } + + using var typeNode = ImRaii.TreeNode($"{type.Name()}"); + if (!typeNode.Success) + continue; + + chatCodes.TryGetValue(type, out var sourcesEnum); + var sources = (uint)sourcesEnum; + + foreach (var source in Enum.GetValues()) + if (ImGui.CheckboxFlags(source.Name(), ref sources, (uint)source)) + chatCodes[type] = (ChatSource)sources; + } + } + } + + public static void ExtraChatSelector(string headerText, ref bool all, HashSet extraChatChannels) + { + if (Plugin.ExtraChat.ChannelNames.Count <= 0) + return; + + using var extraTree = ImRaii.TreeNode(headerText); + if (!extraTree.Success) + return; + + ImGui.Checkbox(Language.Options_Tabs_ExtraChatAll, ref all); + ImGui.Separator(); + + using var _ = ImRaii.Disabled(all); + foreach (var (id, name) in Plugin.ExtraChat.ChannelNames) + { + var enabled = extraChatChannels.Contains(id); + if (!ImGui.Checkbox($"{name}##ec-{id}", ref enabled)) + continue; + + if (enabled) + extraChatChannels.Add(id); + else + extraChatChannels.Remove(id); + } + } + // Used to avoid pops if condition is false for Push. private static void Nop() { } } diff --git a/ChatTwo/Util/TabsUtil.cs b/ChatTwo/Util/TabsUtil.cs index 1f9cb2c..7749938 100755 --- a/ChatTwo/Util/TabsUtil.cs +++ b/ChatTwo/Util/TabsUtil.cs @@ -4,6 +4,14 @@ using ChatTwo.Resources; namespace ChatTwo.Util; internal static class TabsUtil { + internal static Dictionary AllChannels() + { + var channels = new Dictionary(); + foreach (var chatType in Enum.GetValues()) + channels[chatType] = ChatSourceExt.All; + return channels; + } + internal static Tab VanillaGeneral => new() { Name = Language.Tabs_Presets_General, ChatCodes = new Dictionary {