diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index f0914bb21..600e423e1 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -1,7 +1,10 @@ +using System.Diagnostics.CodeAnalysis; 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.Utility; using Dalamud.Logging.Internal; using FFXIVClientStructs.FFXIV.Client.UI; @@ -20,6 +23,11 @@ public abstract class Window private bool internalLastIsOpen = false; private bool internalIsOpen = false; + private bool internalIsPinned = false; + private bool internalIsClickthrough = false; + private DateTimeOffset internalLastDisableClick = DateTimeOffset.MinValue; + private bool didPushInternalAlpha = false; + private float? internalAlpha = null; private bool nextFrameBringToFront = false; /// @@ -130,6 +138,16 @@ public abstract class Window /// public bool ShowCloseButton { get; set; } = true; + /// + /// Gets or sets a value indicating whether or not this window should offer to be pinned via the window's titlebar context menu. + /// + public bool AllowPinning { get; set; } = true; + + /// + /// Gets or sets a value indicating whether or not this window should offer to be made click-through via the window's titlebar context menu. + /// + public bool AllowClickthrough { get; set; } = true; + /// /// Gets or sets a value indicating whether or not this window will stay open. /// @@ -185,6 +203,11 @@ public abstract class Window /// public virtual void PreDraw() { + if (this.internalAlpha.HasValue) + { + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, this.internalAlpha.Value); + this.didPushInternalAlpha = true; + } } /// @@ -192,6 +215,11 @@ public abstract class Window /// public virtual void PostDraw() { + if (this.didPushInternalAlpha) + { + ImGui.PopStyleVar(); + this.didPushInternalAlpha = false; + } } /// @@ -286,7 +314,15 @@ public abstract class Window this.nextFrameBringToFront = false; } - if (this.ShowCloseButton ? ImGui.Begin(this.WindowName, ref this.internalIsOpen, this.Flags) : ImGui.Begin(this.WindowName, this.Flags)) + var flags = this.Flags; + + if (this.internalIsPinned) + flags |= ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize; + + if (this.internalIsClickthrough) + flags |= ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoMouseInputs; + + if (this.ShowCloseButton ? ImGui.Begin(this.WindowName, ref this.internalIsOpen, flags) : ImGui.Begin(this.WindowName, flags)) { // Draw the actual window contents try @@ -297,6 +333,80 @@ public abstract class Window { Log.Error(ex, $"Error during Draw(): {this.WindowName}"); } + + if (this.AllowPinning || this.AllowClickthrough) + { + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 1f); + + var popupName = "WindowSystemContextActions"; + if (ImGui.BeginPopup(popupName)) + { + if (this.internalIsClickthrough) + ImGui.BeginDisabled(); + + if (this.AllowPinning) + ImGui.Checkbox(Loc.Localize("WindowSystemContextActionPin", "Pin Window"), ref this.internalIsPinned); + + if (this.internalIsClickthrough) + ImGui.EndDisabled(); + + if (this.AllowClickthrough) + { + if (ImGui.Checkbox(Loc.Localize("WindowSystemContextActionClickthrough", "Make clickthrough"), + ref this.internalIsClickthrough)) + { + if (this.internalIsClickthrough) + this.internalIsPinned = true; + } + } + + var alpha = (this.internalAlpha ?? ImGui.GetStyle().Alpha) * 100f; + if (ImGui.SliderFloat(Loc.Localize("WindowSystemContextActionAlpha", "Opacity"), ref alpha, 20f, + 100f)) + { + this.internalAlpha = alpha / 100f; + } + + ImGui.SameLine(); + if (ImGui.Button(Loc.Localize("WindowSystemContextActionReset", "Reset"))) + { + this.internalAlpha = null; + } + + ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("WindowSystemContextActionDoubleClick", "Double click the title bar to disable clickthrough.")); + ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("WindowSystemContextActionDisclaimer", "These options may not work for all plugins at the moment.")); + + ImGui.EndPopup(); + } + + ImGui.PopStyleVar(); + + var titleBarRect = Vector4.Zero; + unsafe + { + var window = ImGuiNativeAdditions.igGetCurrentWindow(); + ImGuiNativeAdditions.ImGuiWindow_TitleBarRect(&titleBarRect, window); + } + + if (ImGui.IsMouseHoveringRect(new Vector2(titleBarRect.X, titleBarRect.Y), new Vector2(titleBarRect.Z, titleBarRect.W), false)) + { + if (ImGui.IsWindowHovered() && ImGui.IsMouseClicked(ImGuiMouseButton.Right)) + { + ImGui.OpenPopup(popupName); + } + + if (ImGui.IsMouseDown(ImGuiMouseButton.Left)) + { + if (DateTime.Now - this.internalLastDisableClick < TimeSpan.FromMilliseconds(100)) + { + this.internalIsPinned = false; + this.internalIsClickthrough = false; + } + + this.internalLastDisableClick = DateTime.Now; + } + } + } } if (wasFocused) @@ -375,6 +485,12 @@ public abstract class Window { ImGui.SetNextWindowBgAlpha(this.BgAlpha.Value); } + + // Manually set alpha takes precedence, if devs don't want that, they should turn it off + if (this.internalAlpha.HasValue) + { + ImGui.SetNextWindowBgAlpha(this.internalAlpha.Value); + } } /// @@ -392,4 +508,14 @@ public abstract class Window /// public Vector2 MaximumSize { get; set; } } + + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "imports")] + private static unsafe class ImGuiNativeAdditions + { + [DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)] + public static extern unsafe void* igGetCurrentWindow(); + + [DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)] + public static extern unsafe void ImGuiWindow_TitleBarRect(Vector4* pOut, void* window); + } }