From 544f8b28bfc115968ea53fbfb08207c6d7d010ea Mon Sep 17 00:00:00 2001 From: Soreepeong <3614868+Soreepeong@users.noreply.github.com> Date: Sat, 16 Aug 2025 16:42:30 +0900 Subject: [PATCH] Support make clickthrough --- ...Win32InputHandler.StaticLookupFunctions.cs | 9 +- .../InputHandler/Win32InputHandler.cs | 28 ++-- Dalamud/Interface/Windowing/Window.cs | 133 ++++++++---------- 3 files changed, 79 insertions(+), 91 deletions(-) diff --git a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.StaticLookupFunctions.cs b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.StaticLookupFunctions.cs index 5710a5991..a7b70ce35 100644 --- a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.StaticLookupFunctions.cs +++ b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.StaticLookupFunctions.cs @@ -299,11 +299,12 @@ internal sealed partial class Win32InputHandler private static void ViewportFlagsToWin32Styles(ImGuiViewportFlags flags, out int style, out int exStyle) { - style = (int)(flags.HasFlag(ImGuiViewportFlags.NoDecoration) ? WS.WS_POPUP : WS.WS_OVERLAPPEDWINDOW); - exStyle = - (int)(flags.HasFlag(ImGuiViewportFlags.NoTaskBarIcon) ? WS.WS_EX_TOOLWINDOW : (uint)WS.WS_EX_APPWINDOW); + style = (flags & ImGuiViewportFlags.NoDecoration) != 0 ? unchecked((int)WS.WS_POPUP) : WS.WS_OVERLAPPEDWINDOW; + exStyle = (flags & ImGuiViewportFlags.NoTaskBarIcon) != 0 ? WS.WS_EX_TOOLWINDOW : WS.WS_EX_APPWINDOW; exStyle |= WS.WS_EX_NOREDIRECTIONBITMAP; - if (flags.HasFlag(ImGuiViewportFlags.TopMost)) + if ((flags & ImGuiViewportFlags.TopMost) != 0) exStyle |= WS.WS_EX_TOPMOST; + if ((flags & ImGuiViewportFlags.NoInputs) != 0) + exStyle |= WS.WS_EX_TRANSPARENT | WS.WS_EX_LAYERED; } } diff --git a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs index 62e254a1a..0b2e27b57 100644 --- a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs +++ b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs @@ -8,6 +8,7 @@ using System.Text; using Dalamud.Bindings.ImGui; using Dalamud.Memory; +using Dalamud.Utility; using Serilog; @@ -446,19 +447,19 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler ClientToScreen(this.hWnd, &pos); SetCursorPos(pos.x, pos.y); } + } - // (Optional) Fallback to provide mouse position when focused (WM_MOUSEMOVE already provides this when hovered or captured) - if (!io.WantSetMousePos && !this.mouseTracked && hasMouseScreenPos) - { - // Single viewport mode: mouse position in client window coordinates (io.MousePos is (0,0) when the mouse is on the upper-left corner of the app window) - // (This is the position you can get with ::GetCursorPos() + ::ScreenToClient() or WM_MOUSEMOVE.) - // Multi-viewport mode: mouse position in OS absolute coordinates (io.MousePos is (0,0) when the mouse is on the upper-left of the primary monitor) - // (This is the position you can get with ::GetCursorPos() or WM_MOUSEMOVE + ::ClientToScreen(). In theory adding viewport->Pos to a client position would also be the same.) - var mousePos = mouseScreenPos; - if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) == 0) - ClientToScreen(focusedWindow, &mousePos); - io.AddMousePosEvent(mousePos.x, mousePos.y); - } + // (Optional) Fallback to provide mouse position when focused (WM_MOUSEMOVE already provides this when hovered or captured) + if (!io.WantSetMousePos && !this.mouseTracked && hasMouseScreenPos) + { + // Single viewport mode: mouse position in client window coordinates (io.MousePos is (0,0) when the mouse is on the upper-left corner of the app window) + // (This is the position you can get with ::GetCursorPos() + ::ScreenToClient() or WM_MOUSEMOVE.) + // Multi-viewport mode: mouse position in OS absolute coordinates (io.MousePos is (0,0) when the mouse is on the upper-left of the primary monitor) + // (This is the position you can get with ::GetCursorPos() or WM_MOUSEMOVE + ::ClientToScreen(). In theory adding viewport->Pos to a client position would also be the same.) + var mousePos = mouseScreenPos; + if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) == 0) + ClientToScreen(focusedWindow, &mousePos); + io.AddMousePosEvent(mousePos.x, mousePos.y); } // (Optional) When using multiple viewports: call io.AddMouseViewportEvent() with the viewport the OS mouse cursor is hovering. @@ -827,6 +828,9 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler null); } + if (data->Hwnd == 0) + Util.Fatal($"CreateWindowExW failed: {GetLastError()}", "ImGui Viewport error"); + data->HwndOwned = true; viewport.PlatformRequestResize = false; viewport.PlatformHandle = viewport.PlatformHandleRaw = data->Hwnd; diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index a6b5e0801..e24f96ff8 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -1,9 +1,6 @@ using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Numerics; -using System.Runtime.InteropServices; using System.Threading.Tasks; using CheapLoc; @@ -19,10 +16,13 @@ using Dalamud.Interface.Utility.Internal; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing.Persistence; using Dalamud.Logging.Internal; -using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.UI; +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; + namespace Dalamud.Interface.Windowing; /// @@ -31,11 +31,15 @@ namespace Dalamud.Interface.Windowing; public abstract class Window { private const float FadeInOutTime = 0.072f; + private const string AdditionsPopupName = "WindowSystemContextActions"; private static readonly ModuleLog Log = new("WindowSystem"); private static bool wasEscPressedLastFrame = false; + private readonly TitleBarButton additionsButton; + private readonly List allButtons = []; + private bool internalLastIsOpen = false; private bool internalIsOpen = false; private bool internalIsPinned = false; @@ -69,6 +73,20 @@ public abstract class Window this.WindowName = name; this.Flags = flags; this.ForceMainWindow = forceMainWindow; + + this.additionsButton = new() + { + Icon = FontAwesomeIcon.Bars, + IconOffset = new Vector2(2.5f, 1), + Click = _ => + { + this.internalIsClickthrough = false; + this.presetDirty = false; + ImGui.OpenPopup(AdditionsPopupName); + }, + Priority = int.MinValue, + AvailableClickthrough = true, + }; } /// @@ -448,11 +466,12 @@ public abstract class Window ImGuiP.GetCurrentWindow().InheritNoInputs = this.internalIsClickthrough; } - // Not supported yet on non-main viewports - if (this.internalIsClickthrough && ImGui.GetWindowViewport().ID != ImGui.GetMainViewport().ID) + if (ImGui.GetWindowViewport().ID != ImGui.GetMainViewport().ID) { - this.internalIsClickthrough = false; - this.presetDirty = true; + if ((flags & ImGuiWindowFlags.NoInputs) == ImGuiWindowFlags.NoInputs) + ImGui.GetWindowViewport().Flags |= ImGuiViewportFlags.NoInputs; + else + ImGui.GetWindowViewport().Flags &= ~ImGuiViewportFlags.NoInputs; } // Draw the actual window contents @@ -466,7 +485,6 @@ public abstract class Window } } - const string additionsPopupName = "WindowSystemContextActions"; var flagsApplicableForTitleBarIcons = !flags.HasFlag(ImGuiWindowFlags.NoDecoration) && !flags.HasFlag(ImGuiWindowFlags.NoTitleBar); var showAdditions = (this.AllowPinning || this.AllowClickthrough) && @@ -477,7 +495,7 @@ public abstract class Window { ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 1f); - if (ImGui.BeginPopup(additionsPopupName, ImGuiWindowFlags.NoMove)) + if (ImGui.BeginPopup(AdditionsPopupName, ImGuiWindowFlags.NoMove)) { if (this.internalIsClickthrough) ImGui.BeginDisabled(); @@ -498,11 +516,6 @@ public abstract class Window if (this.internalIsClickthrough) ImGui.EndDisabled(); - var isAvailable = ImGuiHelpers.CheckIsWindowOnMainViewport(); - - if (!isAvailable) - ImGui.BeginDisabled(); - if (this.AllowClickthrough) { if (ImGui.Checkbox( @@ -516,9 +529,6 @@ public abstract class Window Loc.Localize("WindowSystemContextActionClickthroughHint", "Clickthrough windows will not receive mouse input, move or resize. They are completely inert.")); } - if (!isAvailable) - ImGui.EndDisabled(); - var alpha = (this.internalAlpha ?? ImGui.GetStyle().Alpha) * 100f; if (ImGui.SliderFloat(Loc.Localize("WindowSystemContextActionAlpha", "Opacity"), ref alpha, 20f, 100f)) @@ -534,18 +544,11 @@ public abstract class Window this.presetDirty = true; } - if (isAvailable) - { - ImGui.TextColored(ImGuiColors.DalamudGrey, - Loc.Localize("WindowSystemContextActionClickthroughDisclaimer", - "Open this menu again by clicking the three dashes to disable clickthrough.")); - } - else - { - ImGui.TextColored(ImGuiColors.DalamudGrey, - Loc.Localize("WindowSystemContextActionViewportDisclaimer", - "These features are only available if this window is inside the game window.")); - } + ImGui.TextColored( + ImGuiColors.DalamudGrey, + Loc.Localize( + "WindowSystemContextActionClickthroughDisclaimer", + "Open this menu again by clicking the three dashes to disable clickthrough.")); if (ImGui.Button(Loc.Localize("WindowSystemContextActionPrintWindow", "Print window"))) printWindow = true; @@ -556,34 +559,15 @@ public abstract class Window ImGui.PopStyleVar(); } - unsafe + if (flagsApplicableForTitleBarIcons) { - var window = ImGuiP.GetCurrentWindow(); - - ImRect outRect; - ImGuiP.TitleBarRect(&outRect, window); - - var additionsButton = new TitleBarButton - { - Icon = FontAwesomeIcon.Bars, - IconOffset = new Vector2(2.5f, 1), - Click = _ => - { - this.internalIsClickthrough = false; - this.presetDirty = false; - ImGui.OpenPopup(additionsPopupName); - }, - Priority = int.MinValue, - AvailableClickthrough = true, - }; - - if (flagsApplicableForTitleBarIcons) - { - this.DrawTitleBarButtons(window, flags, outRect, - showAdditions - ? this.TitleBarButtons.Append(additionsButton) - : this.TitleBarButtons); - } + this.allButtons.Clear(); + this.allButtons.EnsureCapacity(this.TitleBarButtons.Count + 1); + this.allButtons.AddRange(this.TitleBarButtons); + if (showAdditions) + this.allButtons.Add(this.additionsButton); + this.allButtons.Sort(static (a, b) => b.Priority - a.Priority); + this.DrawTitleBarButtons(); } if (wasFocused) @@ -740,8 +724,11 @@ public abstract class Window } } - private unsafe void DrawTitleBarButtons(ImGuiWindowPtr window, ImGuiWindowFlags flags, ImRect titleBarRect, IEnumerable buttons) + private unsafe void DrawTitleBarButtons() { + var window = ImGuiP.GetCurrentWindow(); + var flags = window.Flags; + var titleBarRect = window.TitleBarRect(); ImGui.PushClipRect(ImGui.GetWindowPos(), ImGui.GetWindowPos() + ImGui.GetWindowSize(), false); var style = ImGui.GetStyle(); @@ -776,26 +763,22 @@ public abstract class Window var max = pos + new Vector2(fontSize, fontSize); ImRect bb = new(pos, max); var isClipped = !ImGuiP.ItemAdd(bb, id, null, 0); - bool hovered, held; - var pressed = false; + bool hovered, held, pressed; if (this.internalIsClickthrough) { - hovered = false; - held = false; - // ButtonBehavior does not function if the window is clickthrough, so we have to do it ourselves - if (ImGui.IsMouseHoveringRect(pos, max)) - { - hovered = true; + var pad = ImGui.GetStyle().TouchExtraPadding; + var rect = new ImRect(pos - pad, max + pad); + hovered = rect.Contains(ImGui.GetMousePos()); - // We can't use ImGui native functions here, because they don't work with clickthrough - if ((global::Windows.Win32.PInvoke.GetKeyState((int)VirtualKey.LBUTTON) & 0x8000) != 0) - { - held = true; - pressed = true; - } - } + // Temporarily enable inputs + // This will be reset on next frame, and then enabled again if it is still being hovered + if (hovered && ImGui.GetWindowViewport().ID != ImGui.GetMainViewport().ID) + ImGui.GetWindowViewport().Flags &= ~ImGuiViewportFlags.NoInputs; + + // We can't use ImGui native functions here, because they don't work with clickthrough + pressed = held = hovered && (GetKeyState(VK.VK_LBUTTON) & 0x8000) != 0; } else { @@ -824,7 +807,7 @@ public abstract class Window return pressed; } - foreach (var button in buttons.OrderBy(x => x.Priority)) + foreach (var button in this.allButtons) { if (this.internalIsClickthrough && !button.AvailableClickthrough) return; @@ -932,7 +915,7 @@ public abstract class Window /// /// Gets or sets an action that is called when the button is clicked. /// - public Action Click { get; set; } + public Action? Click { get; set; } /// /// Gets or sets the priority the button shall be shown in.