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 <me@aurieh.me>
This commit is contained in:
Dean Sheather
2024-07-11 15:50:17 +10:00
parent 05495705fd
commit c3c60e7e43
6 changed files with 115 additions and 10 deletions
+10
View File
@@ -20,6 +20,8 @@ internal class Configuration : IPluginConfiguration
public bool HideWhenUiHidden = true; public bool HideWhenUiHidden = true;
public bool HideInLoadingScreens; public bool HideInLoadingScreens;
public bool HideInBattle; public bool HideInBattle;
public bool HideWhenInactive;
public int InactivityHideTimeout = 10;
public bool ShowHideButton = true; public bool ShowHideButton = true;
public bool NativeItemTooltips = true; public bool NativeItemTooltips = true;
public bool PrettierTimestamps = true; public bool PrettierTimestamps = true;
@@ -78,6 +80,8 @@ internal class Configuration : IPluginConfiguration
HideWhenUiHidden = other.HideWhenUiHidden; HideWhenUiHidden = other.HideWhenUiHidden;
HideInLoadingScreens = other.HideInLoadingScreens; HideInLoadingScreens = other.HideInLoadingScreens;
HideInBattle = other.HideInBattle; HideInBattle = other.HideInBattle;
HideWhenInactive = other.HideWhenInactive;
InactivityHideTimeout = other.InactivityHideTimeout;
ShowHideButton = other.ShowHideButton; ShowHideButton = other.ShowHideButton;
NativeItemTooltips = other.NativeItemTooltips; NativeItemTooltips = other.NativeItemTooltips;
PrettierTimestamps = other.PrettierTimestamps; PrettierTimestamps = other.PrettierTimestamps;
@@ -168,6 +172,9 @@ internal class Tab
[NonSerialized] [NonSerialized]
public uint Unread; public uint Unread;
[NonSerialized]
public long LastMessageTime;
[NonSerialized] [NonSerialized]
public MessageList Messages = new(); public MessageList Messages = new();
@@ -192,7 +199,10 @@ internal class Tab
{ {
Messages.AddPrune(message, MessageManager.MessageDisplayLimit); Messages.AddPrune(message, MessageManager.MessageDisplayLimit);
if (unread) if (unread)
{
Unread += 1; Unread += 1;
LastMessageTime = Environment.TickCount64;
}
} }
internal void Clear() internal void Clear()
-3
View File
@@ -253,9 +253,6 @@ internal sealed unsafe class Chat : IDisposable
LastRefresh = Environment.TickCount64; LastRefresh = Environment.TickCount64;
} }
if (Plugin.ChatLogWindow is { CurrentTab.InputDisabled: true, IsHidden: false })
return;
// Vanilla text input has focus // Vanilla text input has focus
if (RaptureAtkModule.Instance()->AtkModule.IsTextInputActive()) if (RaptureAtkModule.Instance()->AtkModule.IsTextInputActive())
return; return;
+36
View File
@@ -2417,6 +2417,24 @@ namespace ChatTwo.Resources {
} }
} }
/// <summary>
/// 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..
/// </summary>
internal static string Options_HideWhenInactive_Description {
get {
return ResourceManager.GetString("Options_HideWhenInactive_Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Hide when inactive.
/// </summary>
internal static string Options_HideWhenInactive_Name {
get {
return ResourceManager.GetString("Options_HideWhenInactive_Name", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Hide {0} when you are not logged in to a character.. /// Looks up a localized string similar to Hide {0} when you are not logged in to a character..
/// </summary> /// </summary>
@@ -2453,6 +2471,24 @@ namespace ChatTwo.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to How long to wait (in seconds) before considering the chat log inactive..
/// </summary>
internal static string Options_InactivityHideTimeout_Description {
get {
return ResourceManager.GetString("Options_InactivityHideTimeout_Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Inactivity timeout.
/// </summary>
internal static string Options_InactivityHideTimeout_Name {
get {
return ResourceManager.GetString("Options_InactivityHideTimeout_Name", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to The font {0} will use to display Japanese text.. /// Looks up a localized string similar to The font {0} will use to display Japanese text..
/// </summary> /// </summary>
+12
View File
@@ -406,6 +406,18 @@
<data name="Options_HideWhenUiHidden_Description"> <data name="Options_HideWhenUiHidden_Description">
<value>Hide {0} when the game UI is hidden.</value> <value>Hide {0} when the game UI is hidden.</value>
</data> </data>
<data name="Options_HideWhenInactive_Name">
<value>Hide when inactive</value>
</data>
<data name="Options_HideWhenInactive_Description">
<value>Hide the chat after a configurable period of inactivity. The current tab and any tabs with unread indicators enabled are considered for activity.</value>
</data>
<data name="Options_InactivityHideTimeout_Name">
<value>Inactivity timeout</value>
</data>
<data name="Options_InactivityHideTimeout_Description">
<value>How long to wait (in seconds) before considering the chat log inactive.</value>
</data>
<data name="Options_KeybindMode_Name"> <data name="Options_KeybindMode_Name">
<value>Keybind mode</value> <value>Keybind mode</value>
</data> </data>
+44 -5
View File
@@ -85,6 +85,9 @@ public sealed class ChatLogWindow : Window
private const uint ChatCloseSfx = 3u; private const uint ChatCloseSfx = 3u;
private bool PlayedClosingSound = true; private bool PlayedClosingSound = true;
private long FrameTime; // set every frame
private long LastActivityTime = Environment.TickCount64;
private readonly ExcelSheet<World> WorldSheet; private readonly ExcelSheet<World> WorldSheet;
private readonly ExcelSheet<LogFilter> LogFilterSheet; private readonly ExcelSheet<LogFilter> LogFilterSheet;
private readonly ExcelSheet<TextCommand> TextCommandSheet; private readonly ExcelSheet<TextCommand> TextCommandSheet;
@@ -148,6 +151,18 @@ public sealed class ChatLogWindow : Window
private void Activated(ChatActivatedArgs args) private void Activated(ChatActivatedArgs args)
{ {
Activate = true; 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)) if (args.AddIfNotPresent != null && !Chat.Contains(args.AddIfNotPresent))
Chat += args.AddIfNotPresent; Chat += args.AddIfNotPresent;
@@ -203,10 +218,6 @@ public sealed class ChatLogWindow : Window
if (info.Text != null && Chat.Length == 0) if (info.Text != null && Chat.Length == 0)
Chat = info.Text; Chat = info.Text;
PlayedClosingSound = false;
if (Plugin.Config.PlaySounds)
UIModule.PlaySound(ChatOpenSfx);
} }
private bool IsValidCommand(string command) private bool IsValidCommand(string command)
@@ -471,7 +482,23 @@ public sealed class ChatLogWindow : Window
public override bool DrawConditions() 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() public override void PreDraw()
@@ -485,6 +512,12 @@ public sealed class ChatLogWindow : Window
public override void PostDraw() 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 }) if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null })
StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == Plugin.Config.ChosenStyle)?.Pop(); StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == Plugin.Config.ChosenStyle)?.Pop();
} }
@@ -740,7 +773,10 @@ public sealed class ChatLogWindow : Window
} }
if (ImGui.IsItemActive()) if (ImGui.IsItemActive())
{
HandleKeybinds(true); HandleKeybinds(true);
LastActivityTime = FrameTime;
}
// Only trigger unfocused if we are currently not calling the auto complete // Only trigger unfocused if we are currently not calling the auto complete
if (!Activate && !ImGui.IsItemActive() && AutoCompleteInfo == null) if (!Activate && !ImGui.IsItemActive() && AutoCompleteInfo == null)
@@ -785,6 +821,9 @@ public sealed class ChatLogWindow : Window
UserHide(); UserHide();
} }
if (ImGui.IsWindowHovered(ImGuiHoveredFlags.ChildWindows))
LastActivityTime = FrameTime;
if (!showNovice) if (!showNovice)
return; return;
+13 -2
View File
@@ -1,7 +1,5 @@
using ChatTwo.Resources; using ChatTwo.Resources;
using ChatTwo.Util; using ChatTwo.Util;
using Dalamud.Interface.Style;
using Dalamud.Interface.Utility.Raii;
using ImGuiNET; using ImGuiNET;
namespace ChatTwo.Ui.SettingsTabs; 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); ImGuiUtil.OptionCheckbox(ref Mutable.HideInBattle, Language.Options_HideInBattle_Name, Language.Options_HideInBattle_Description);
ImGui.Spacing(); 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); ImGuiUtil.OptionCheckbox(ref Mutable.PrettierTimestamps, Language.Options_PrettierTimestamps_Name, Language.Options_PrettierTimestamps_Description);
if (Mutable.PrettierTimestamps) if (Mutable.PrettierTimestamps)