WindowSystem: fix clickthrough option not applying to child windows, persist options

Persistence is pretty WIP. I want to offer multiple presets in the future, and save more things like window positions.
This commit is contained in:
goat 2024-12-30 21:14:08 +01:00
parent 49a18e3c1e
commit 35b49823e5
9 changed files with 301 additions and 80 deletions

View file

@ -12,6 +12,7 @@ using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.ReShadeHandling;
using Dalamud.Interface.Style;
using Dalamud.Interface.Windowing.Persistence;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Internal.AutoUpdate;
using Dalamud.Plugin.Internal.Profiles;
@ -264,8 +265,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// Gets or sets a value indicating whether or not an additional button allowing pinning and clickthrough options should be shown
/// on plugin title bars when using the Window System.
/// </summary>
[JsonProperty("EnablePluginUiAdditionalOptionsExperimental")]
public bool EnablePluginUiAdditionalOptions { get; set; } = false;
public bool EnablePluginUiAdditionalOptions { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether viewports should always be disabled.
@ -351,6 +351,11 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// </summary>
public bool ProfilesHasSeenTutorial { get; set; } = false;
/// <summary>
/// Gets or sets the default UI preset.
/// </summary>
public PresetModel DefaultUiPreset { get; set; } = new();
/// <summary>
/// Gets or sets the order of DTR elements, by title.
/// </summary>

View file

@ -27,6 +27,8 @@ using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Interface.Style;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Windowing;
using Dalamud.Interface.Windowing.Persistence;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Utility;
using Dalamud.Utility.Timing;
@ -60,6 +62,7 @@ namespace Dalamud.Interface.Internal;
/// This class manages interaction with the ImGui interface.
/// </summary>
[ServiceManager.EarlyLoadedService]
[InherentDependency<WindowSystemPersistence>] // Used by window system windows to restore state from the configuration
internal partial class InterfaceManager : IInternalDisposableService
{
/// <summary>

View file

@ -39,18 +39,6 @@ public class SettingsTabExperimental : SettingsTab
new GapSettingsEntry(5),
new SettingsEntry<bool>(
Loc.Localize(
"DalamudSettingEnablePluginUIAdditionalOptions",
"Add a button to the title bar of plugin windows to open additional options"),
Loc.Localize(
"DalamudSettingEnablePluginUIAdditionalOptionsHint",
"This will allow you to pin certain plugin windows, make them clickthrough or adjust their opacity.\nThis may not be supported by all of your plugins. Contact the plugin author if you want them to support this feature."),
c => c.EnablePluginUiAdditionalOptions,
(v, c) => c.EnablePluginUiAdditionalOptions = v),
new GapSettingsEntry(5),
new ButtonSettingsEntry(
Loc.Localize("DalamudSettingsClearHidden", "Clear hidden plugins"),
Loc.Localize(

View file

@ -108,6 +108,16 @@ public class SettingsTabLook : SettingsTab
c => c.IsDocking,
(v, c) => c.IsDocking = v),
new SettingsEntry<bool>(
Loc.Localize(
"DalamudSettingEnablePluginUIAdditionalOptions",
"Add a button to the title bar of plugin windows to open additional options"),
Loc.Localize(
"DalamudSettingEnablePluginUIAdditionalOptionsHint",
"This will allow you to pin certain plugin windows, make them clickthrough or adjust their opacity.\nThis may not be supported by all of your plugins. Contact the plugin author if you want them to support this feature."),
c => c.EnablePluginUiAdditionalOptions,
(v, c) => c.EnablePluginUiAdditionalOptions = v),
new SettingsEntry<bool>(
Loc.Localize("DalamudSettingEnablePluginUISoundEffects", "Enable sound effects for plugin windows"),
Loc.Localize("DalamudSettingEnablePluginUISoundEffectsHint", "This will allow you to enable or disable sound effects generated by plugin user interfaces.\nThis is affected by your in-game `System Sounds` volume settings."),

View file

@ -0,0 +1,53 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Dalamud.Interface.Windowing.Persistence;
/// <summary>
/// Class representing a Window System preset.
/// </summary>
internal class PresetModel
{
/// <summary>
/// Gets or sets the ID of this preset.
/// </summary>
[JsonProperty("id")]
public Guid Id { get; set; }
/// <summary>
/// Gets or sets the name of this preset.
/// </summary>
[JsonProperty("n")]
public string Name { get; set; } = "New Preset";
/// <summary>
/// Gets or sets a dictionary containing the windows in the preset, mapping their ID to the preset.
/// </summary>
[JsonProperty("w")]
public Dictionary<uint, PresetWindow> Windows { get; set; } = new();
/// <summary>
/// Class representing a window in a preset.
/// </summary>
internal class PresetWindow
{
/// <summary>
/// Gets or sets a value indicating whether the window is pinned.
/// </summary>
[JsonProperty("p")]
public bool IsPinned { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the window is clickthrough.
/// </summary>
[JsonProperty("ct")]
public bool IsClickThrough { get; set; }
/// <summary>
/// Gets or sets the window's opacity override.
/// </summary>
[JsonProperty("a")]
public float? Alpha { get; set; }
}
}

View file

@ -0,0 +1,47 @@
using Dalamud.Configuration.Internal;
namespace Dalamud.Interface.Windowing.Persistence;
/// <summary>
/// Class handling persistence for window system windows.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal class WindowSystemPersistence : IServiceType
{
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration config = Service<DalamudConfiguration>.Get();
/// <summary>
/// Initializes a new instance of the <see cref="WindowSystemPersistence"/> class.
/// </summary>
[ServiceManager.ServiceConstructor]
public WindowSystemPersistence()
{
}
/// <summary>
/// Gets the active window system preset.
/// </summary>
public PresetModel ActivePreset => this.config.DefaultUiPreset;
/// <summary>
/// Get or add a window to the active preset.
/// </summary>
/// <param name="id">The ID of the window.</param>
/// <returns>The preset window instance, or null if the preset does not contain this window.</returns>
public PresetModel.PresetWindow? GetWindow(uint id)
{
return this.ActivePreset.Windows.TryGetValue(id, out var window) ? window : null;
}
/// <summary>
/// Persist the state of a window to the active preset.
/// </summary>
/// <param name="id">The ID of the window.</param>
/// <param name="window">The preset window instance.</param>
public void SaveWindow(uint id, PresetModel.PresetWindow window)
{
this.ActivePreset.Windows[id] = window;
this.config.QueueSave();
}
}

View file

@ -1,15 +1,17 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using CheapLoc;
using Dalamud.Configuration.Internal;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Windowing.Persistence;
using Dalamud.Logging.Internal;
using FFXIVClientStructs.FFXIV.Client.UI;
@ -35,15 +37,19 @@ public abstract class Window
private float? internalAlpha = null;
private bool nextFrameBringToFront = false;
private bool hasInitializedFromPreset = false;
private PresetModel.PresetWindow? presetWindow;
private bool presetDirty = false;
/// <summary>
/// Initializes a new instance of the <see cref="Window"/> class.
/// </summary>
/// <param name="name">The name/ID of this window.
/// If you have multiple windows with the same name, you will need to
/// append an unique ID to it by specifying it after "###" behind the window title.
/// append a unique ID to it by specifying it after "###" behind the window title.
/// </param>
/// <param name="flags">The <see cref="ImGuiWindowFlags"/> of this window.</param>
/// <param name="forceMainWindow">Whether or not this window should be limited to the main game window.</param>
/// <param name="forceMainWindow">Whether this window should be limited to the main game window.</param>
protected Window(string name, ImGuiWindowFlags flags = ImGuiWindowFlags.None, bool forceMainWindow = false)
{
this.WindowName = name;
@ -51,6 +57,33 @@ public abstract class Window
this.ForceMainWindow = forceMainWindow;
}
/// <summary>
/// Flags to control window behavior.
/// </summary>
[Flags]
internal enum WindowDrawFlags
{
/// <summary>
/// Nothing.
/// </summary>
None = 0,
/// <summary>
/// Enable window opening/closing sound effects.
/// </summary>
UseSoundEffects = 1 << 0,
/// <summary>
/// Hook into the game's focus management.
/// </summary>
UseFocusManagement = 1 << 1,
/// <summary>
/// Enable the built-in "additional options" menu on the title bar.
/// </summary>
UseAdditionalOptions = 1 << 2,
}
/// <summary>
/// Gets or sets the namespace of the window.
/// </summary>
@ -271,13 +304,12 @@ public abstract class Window
/// <summary>
/// Draw the window via ImGui.
/// </summary>
/// <param name="configuration">Configuration instance used to check if certain window management features should be enabled.</param>
internal void DrawInternal(DalamudConfiguration? configuration)
/// <param name="internalDrawFlags">Flags controlling window behavior.</param>
/// <param name="persistence">Handler for window persistence data.</param>
internal void DrawInternal(WindowDrawFlags internalDrawFlags, WindowSystemPersistence? persistence)
{
this.PreOpenCheck();
var doSoundEffects = configuration?.EnablePluginUISoundEffects ?? false;
if (!this.IsOpen)
{
if (this.internalIsOpen != this.internalLastIsOpen)
@ -287,7 +319,8 @@ public abstract class Window
this.IsFocused = false;
if (doSoundEffects && !this.DisableWindowSounds) UIGlobals.PlaySoundEffect(this.OnCloseSfxId);
if (internalDrawFlags.HasFlag(WindowDrawFlags.UseSoundEffects) && !this.DisableWindowSounds)
UIGlobals.PlaySoundEffect(this.OnCloseSfxId);
}
return;
@ -302,12 +335,15 @@ public abstract class Window
if (hasNamespace)
ImGui.PushID(this.Namespace);
this.PreHandlePreset(persistence);
if (this.internalLastIsOpen != this.internalIsOpen && this.internalIsOpen)
{
this.internalLastIsOpen = this.internalIsOpen;
this.OnOpen();
if (doSoundEffects && !this.DisableWindowSounds) UIGlobals.PlaySoundEffect(this.OnOpenSfxId);
if (internalDrawFlags.HasFlag(WindowDrawFlags.UseSoundEffects) && !this.DisableWindowSounds)
UIGlobals.PlaySoundEffect(this.OnOpenSfxId);
}
this.PreDraw();
@ -340,6 +376,8 @@ public abstract class Window
if (this.CanShowCloseButton ? ImGui.Begin(this.WindowName, ref this.internalIsOpen, flags) : ImGui.Begin(this.WindowName, flags))
{
ImGuiNativeAdditions.igCustom_WindowSetInheritNoInputs(this.internalIsClickthrough);
// Draw the actual window contents
try
{
@ -355,7 +393,7 @@ public abstract class Window
var flagsApplicableForTitleBarIcons = !flags.HasFlag(ImGuiWindowFlags.NoDecoration) &&
!flags.HasFlag(ImGuiWindowFlags.NoTitleBar);
var showAdditions = (this.AllowPinning || this.AllowClickthrough) &&
(configuration?.EnablePluginUiAdditionalOptions ?? true) &&
internalDrawFlags.HasFlag(WindowDrawFlags.UseAdditionalOptions) &&
flagsApplicableForTitleBarIcons;
if (showAdditions)
{
@ -375,36 +413,51 @@ public abstract class Window
{
var showAsPinned = this.internalIsPinned || this.internalIsClickthrough;
if (ImGui.Checkbox(Loc.Localize("WindowSystemContextActionPin", "Pin Window"), ref showAsPinned))
{
this.internalIsPinned = showAsPinned;
this.presetDirty = true;
}
ImGuiComponents.HelpMarker(
Loc.Localize("WindowSystemContextActionPinHint", "Pinned windows will not move or resize when you click and drag them."));
}
if (this.internalIsClickthrough)
ImGui.EndDisabled();
if (this.AllowClickthrough)
ImGui.Checkbox(Loc.Localize("WindowSystemContextActionClickthrough", "Make clickthrough"), ref this.internalIsClickthrough);
{
if (ImGui.Checkbox(
Loc.Localize("WindowSystemContextActionClickthrough", "Make clickthrough"),
ref this.internalIsClickthrough))
{
this.presetDirty = true;
}
ImGuiComponents.HelpMarker(
Loc.Localize("WindowSystemContextActionClickthroughHint", "Clickthrough windows will not receive mouse input, move or resize. They are completely inert."));
}
var alpha = (this.internalAlpha ?? ImGui.GetStyle().Alpha) * 100f;
if (ImGui.SliderFloat(Loc.Localize("WindowSystemContextActionAlpha", "Opacity"), ref alpha, 20f,
100f))
{
this.internalAlpha = alpha / 100f;
this.presetDirty = true;
}
ImGui.SameLine();
if (ImGui.Button(Loc.Localize("WindowSystemContextActionReset", "Reset")))
{
this.internalAlpha = null;
this.presetDirty = true;
}
if (isAvailable)
{
ImGui.TextColored(ImGuiColors.DalamudGrey,
Loc.Localize("WindowSystemContextActionClickthroughDisclaimer",
"Open this menu again to disable clickthrough."));
ImGui.TextColored(ImGuiColors.DalamudGrey,
Loc.Localize("WindowSystemContextActionDisclaimer",
"These options may not work for all plugins at the moment."));
"Open this menu again by clicking the three dashes to disable clickthrough."));
}
else
{
@ -457,8 +510,7 @@ public abstract class Window
this.IsFocused = ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows);
var isAllowed = configuration?.IsFocusManagementEnabled ?? false;
if (isAllowed)
if (internalDrawFlags.HasFlag(WindowDrawFlags.UseFocusManagement))
{
var escapeDown = Service<KeyState>.Get()[VirtualKey.ESCAPE];
if (escapeDown && this.IsFocused && !wasEscPressedLastFrame && this.RespectCloseHotkey)
@ -476,6 +528,8 @@ public abstract class Window
this.PostDraw();
this.PostHandlePreset(persistence);
if (hasNamespace)
ImGui.PopID();
}
@ -519,6 +573,50 @@ public abstract class Window
}
}
private void PreHandlePreset(WindowSystemPersistence? persistence)
{
if (persistence == null || this.hasInitializedFromPreset)
return;
var id = ImGui.GetID(this.WindowName);
this.presetWindow = persistence.GetWindow(id);
this.hasInitializedFromPreset = true;
// Fresh preset - don't apply anything
if (this.presetWindow == null)
{
this.presetWindow = new PresetModel.PresetWindow();
this.presetDirty = true;
return;
}
this.internalIsPinned = this.presetWindow.IsPinned;
this.internalIsClickthrough = this.presetWindow.IsClickThrough;
this.internalAlpha = this.presetWindow.Alpha;
}
private void PostHandlePreset(WindowSystemPersistence? persistence)
{
if (persistence == null)
return;
Debug.Assert(this.presetWindow != null, "this.presetWindow != null");
if (this.presetDirty)
{
this.presetWindow.IsPinned = this.internalIsPinned;
this.presetWindow.IsClickThrough = this.internalIsClickthrough;
this.presetWindow.Alpha = this.internalAlpha;
var id = ImGui.GetID(this.WindowName);
persistence.SaveWindow(id, this.presetWindow!);
this.presetDirty = false;
Log.Verbose("Saved preset for {WindowName}", this.WindowName);
}
}
private unsafe void DrawTitleBarButtons(void* window, ImGuiWindowFlags flags, Vector4 titleBarRect, IEnumerable<TitleBarButton> buttons)
{
ImGui.PushClipRect(ImGui.GetWindowPos(), ImGui.GetWindowPos() + ImGui.GetWindowSize(), false);
@ -715,5 +813,8 @@ public abstract class Window
[DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)]
public static extern void ImGuiWindow_TitleBarRect(Vector4* pOut, void* window);
[DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)]
public static extern void igCustom_WindowSetInheritNoInputs(bool inherit);
}
}

View file

@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Linq;
using Dalamud.Configuration.Internal;
using Dalamud.Interface.Windowing.Persistence;
using ImGuiNET;
using Serilog;
@ -103,7 +104,20 @@ public class WindowSystem
if (hasNamespace)
ImGui.PushID(this.Namespace);
// These must be nullable, people are using stock WindowSystems and Windows without Dalamud for tests
var config = Service<DalamudConfiguration>.GetNullable();
var persistence = Service<WindowSystemPersistence>.GetNullable();
var flags = Window.WindowDrawFlags.None;
if (config?.EnablePluginUISoundEffects ?? false)
flags |= Window.WindowDrawFlags.UseSoundEffects;
if (config?.EnablePluginUiAdditionalOptions ?? false)
flags |= Window.WindowDrawFlags.UseAdditionalOptions;
if (config?.IsFocusManagementEnabled ?? false)
flags |= Window.WindowDrawFlags.UseFocusManagement;
// Shallow clone the list of windows so that we can edit it without modifying it while the loop is iterating
foreach (var window in this.windows.ToArray())
@ -111,7 +125,7 @@ public class WindowSystem
#if DEBUG
// Log.Verbose($"[WS{(hasNamespace ? "/" + this.Namespace : string.Empty)}] Drawing {window.WindowName}");
#endif
window.DrawInternal(config);
window.DrawInternal(flags, persistence);
}
var focusedWindow = this.windows.FirstOrDefault(window => window.IsFocused && window.RespectCloseHotkey);

@ -1 +1 @@
Subproject commit 7002b2884e9216d8bef3e792722d88abe31788f8
Subproject commit 122ee16819437eea7eefe0c04398b44174106d86