mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 18:27:23 +01:00
feat: add title bar buttons API to Window, make clickthrough/pinning window a title bar button
This commit is contained in:
parent
b73ac2f3f7
commit
f10a597566
3 changed files with 252 additions and 84 deletions
|
|
@ -231,6 +231,12 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable
|
|||
/// </summary>
|
||||
public bool EnablePluginUISoundEffects { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
public bool EnablePluginUiAdditionalOptions { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether viewports should always be disabled.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -109,6 +109,12 @@ public class SettingsTabLook : SettingsTab
|
|||
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."),
|
||||
c => c.EnablePluginUISoundEffects,
|
||||
(v, c) => c.EnablePluginUISoundEffects = 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("DalamudSettingToggleGamepadNavigation", "Control plugins via gamepad"),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
|
|
@ -6,10 +8,12 @@ using CheapLoc;
|
|||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Game.ClientState.Keys;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Logging.Internal;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using ImGuiNET;
|
||||
using PInvoke;
|
||||
|
||||
namespace Dalamud.Interface.Windowing;
|
||||
|
||||
|
|
@ -21,12 +25,11 @@ public abstract class Window
|
|||
private static readonly ModuleLog Log = new("WindowSystem");
|
||||
|
||||
private static bool wasEscPressedLastFrame = false;
|
||||
|
||||
|
||||
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;
|
||||
|
|
@ -149,6 +152,15 @@ public abstract class Window
|
|||
/// </summary>
|
||||
public bool AllowClickthrough { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a list of available title bar buttons.
|
||||
///
|
||||
/// If <see cref="AllowPinning"/> or <see cref="AllowClickthrough"/> are set to true, and this features is not
|
||||
/// disabled globally by the user, an internal title bar button to manage these is added when drawing, but it will
|
||||
/// not appear in this collection. If you wish to remove this button, set both of these values to false.
|
||||
/// </summary>
|
||||
public List<TitleBarButton> TitleBarButtons { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not this window will stay open.
|
||||
/// </summary>
|
||||
|
|
@ -157,6 +169,8 @@ public abstract class Window
|
|||
get => this.internalIsOpen;
|
||||
set => this.internalIsOpen = value;
|
||||
}
|
||||
|
||||
private bool CanShowCloseButton => this.ShowCloseButton && !this.internalIsClickthrough;
|
||||
|
||||
/// <summary>
|
||||
/// Toggle window is open state.
|
||||
|
|
@ -252,7 +266,7 @@ public abstract class Window
|
|||
public virtual void Update()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Draw the window via ImGui.
|
||||
/// </summary>
|
||||
|
|
@ -317,13 +331,13 @@ public abstract class Window
|
|||
|
||||
var flags = this.Flags;
|
||||
|
||||
if (this.internalIsPinned)
|
||||
if (this.internalIsPinned || this.internalIsClickthrough)
|
||||
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))
|
||||
if (this.CanShowCloseButton ? ImGui.Begin(this.WindowName, ref this.internalIsOpen, flags) : ImGui.Begin(this.WindowName, flags))
|
||||
{
|
||||
// Draw the actual window contents
|
||||
try
|
||||
|
|
@ -334,79 +348,87 @@ public abstract class Window
|
|||
{
|
||||
Log.Error(ex, $"Error during Draw(): {this.WindowName}");
|
||||
}
|
||||
}
|
||||
|
||||
if (this.AllowPinning || this.AllowClickthrough)
|
||||
var additionsPopupName = "WindowSystemContextActions";
|
||||
var flagsApplicableForTitleBarIcons = !flags.HasFlag(ImGuiWindowFlags.NoDecoration) &&
|
||||
!flags.HasFlag(ImGuiWindowFlags.NoTitleBar);
|
||||
var showAdditions = (this.AllowPinning || this.AllowClickthrough) &&
|
||||
(configuration?.EnablePluginUiAdditionalOptions ?? true) &&
|
||||
flagsApplicableForTitleBarIcons;
|
||||
if (showAdditions)
|
||||
{
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 1f);
|
||||
|
||||
if (ImGui.BeginPopup(additionsPopupName, ImGuiWindowFlags.NoMove))
|
||||
{
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 1f);
|
||||
if (this.internalIsClickthrough)
|
||||
ImGui.BeginDisabled();
|
||||
|
||||
var popupName = "WindowSystemContextActions";
|
||||
if (ImGui.BeginPopup(popupName))
|
||||
if (this.AllowPinning)
|
||||
{
|
||||
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();
|
||||
var showAsPinned = this.internalIsPinned || this.internalIsClickthrough;
|
||||
if (ImGui.Checkbox(Loc.Localize("WindowSystemContextActionPin", "Pin Window"), ref showAsPinned))
|
||||
this.internalIsPinned = showAsPinned;
|
||||
}
|
||||
|
||||
ImGui.PopStyleVar();
|
||||
|
||||
var titleBarRect = Vector4.Zero;
|
||||
unsafe
|
||||
if (this.internalIsClickthrough)
|
||||
ImGui.EndDisabled();
|
||||
|
||||
if (this.AllowClickthrough)
|
||||
ImGui.Checkbox(Loc.Localize("WindowSystemContextActionClickthrough", "Make clickthrough"), ref this.internalIsClickthrough);
|
||||
|
||||
var alpha = (this.internalAlpha ?? ImGui.GetStyle().Alpha) * 100f;
|
||||
if (ImGui.SliderFloat(Loc.Localize("WindowSystemContextActionAlpha", "Opacity"), ref alpha, 20f,
|
||||
100f))
|
||||
{
|
||||
var window = ImGuiNativeAdditions.igGetCurrentWindow();
|
||||
ImGuiNativeAdditions.ImGuiWindow_TitleBarRect(&titleBarRect, window);
|
||||
this.internalAlpha = alpha / 100f;
|
||||
}
|
||||
|
||||
if (ImGui.IsMouseHoveringRect(new Vector2(titleBarRect.X, titleBarRect.Y), new Vector2(titleBarRect.Z, titleBarRect.W), false))
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button(Loc.Localize("WindowSystemContextActionReset", "Reset")))
|
||||
{
|
||||
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;
|
||||
}
|
||||
this.internalAlpha = null;
|
||||
}
|
||||
|
||||
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."));
|
||||
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
|
||||
ImGui.PopStyleVar();
|
||||
}
|
||||
|
||||
var titleBarRect = Vector4.Zero;
|
||||
unsafe
|
||||
{
|
||||
var window = ImGuiNativeAdditions.igGetCurrentWindow();
|
||||
ImGuiNativeAdditions.ImGuiWindow_TitleBarRect(&titleBarRect, window);
|
||||
|
||||
var additionsButton = new TitleBarButton
|
||||
{
|
||||
Icon = FontAwesomeIcon.Bars,
|
||||
IconOffset = new Vector2(2.5f, 1),
|
||||
Click = _ =>
|
||||
{
|
||||
this.internalIsClickthrough = false;
|
||||
ImGui.OpenPopup(additionsPopupName);
|
||||
},
|
||||
Priority = int.MinValue,
|
||||
AvailableClickthrough = true,
|
||||
};
|
||||
|
||||
if (flagsApplicableForTitleBarIcons)
|
||||
{
|
||||
this.DrawTitleBarButtons(window, flags, titleBarRect,
|
||||
showAdditions
|
||||
? this.TitleBarButtons.Append(additionsButton)
|
||||
: this.TitleBarButtons);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -440,21 +462,6 @@ public abstract class Window
|
|||
ImGui.PopID();
|
||||
}
|
||||
|
||||
// private void CheckState()
|
||||
// {
|
||||
// if (this.internalLastIsOpen != this.internalIsOpen)
|
||||
// {
|
||||
// if (this.internalIsOpen)
|
||||
// {
|
||||
// this.OnOpen();
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// this.OnClose();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
private void ApplyConditionals()
|
||||
{
|
||||
if (this.Position.HasValue)
|
||||
|
|
@ -494,6 +501,106 @@ public abstract class Window
|
|||
}
|
||||
}
|
||||
|
||||
private unsafe void DrawTitleBarButtons(void* window, ImGuiWindowFlags flags, Vector4 titleBarRect, IEnumerable<TitleBarButton> buttons)
|
||||
{
|
||||
ImGui.PushClipRect(ImGui.GetWindowPos(), ImGui.GetWindowPos() + ImGui.GetWindowSize(), false);
|
||||
|
||||
var style = ImGui.GetStyle();
|
||||
var fontSize = ImGui.GetFontSize();
|
||||
var drawList = ImGui.GetWindowDrawList();
|
||||
|
||||
var padR = 0f;
|
||||
var buttonSize = ImGui.GetFontSize();
|
||||
|
||||
var numNativeButtons = 0;
|
||||
if (this.CanShowCloseButton)
|
||||
numNativeButtons++;
|
||||
|
||||
if (!flags.HasFlag(ImGuiWindowFlags.NoCollapse) && style.WindowMenuButtonPosition == ImGuiDir.Right)
|
||||
numNativeButtons++;
|
||||
|
||||
// If there are no native buttons, pad from the right to make some space
|
||||
if (numNativeButtons == 0)
|
||||
padR += style.FramePadding.X;
|
||||
|
||||
// Pad to the left, to get out of the way of the native buttons
|
||||
padR += numNativeButtons * (buttonSize + style.ItemInnerSpacing.X);
|
||||
|
||||
Vector2 GetCenter(Vector4 rect) => new((rect.X + rect.Z) * 0.5f, (rect.Y + rect.W) * 0.5f);
|
||||
|
||||
var numButtons = 0;
|
||||
bool DrawButton(TitleBarButton button, Vector2 pos)
|
||||
{
|
||||
var id = ImGui.GetID($"###CustomTbButton{numButtons}");
|
||||
numButtons++;
|
||||
|
||||
var min = pos;
|
||||
var max = pos + new Vector2(fontSize, fontSize);
|
||||
Vector4 bb = new(min.X, min.Y, max.X, max.Y);
|
||||
var isClipped = !ImGuiNativeAdditions.igItemAdd(bb, id, null, 0);
|
||||
bool hovered, held;
|
||||
var pressed = false;
|
||||
|
||||
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(min, max))
|
||||
{
|
||||
hovered = true;
|
||||
|
||||
// We can't use ImGui native functions here, because they don't work with clickthrough
|
||||
if ((User32.GetKeyState((int)VirtualKey.LBUTTON) & 0x8000) != 0)
|
||||
{
|
||||
held = true;
|
||||
pressed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
pressed = ImGuiNativeAdditions.igButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags.None);
|
||||
}
|
||||
|
||||
if (isClipped)
|
||||
return pressed;
|
||||
|
||||
// Render
|
||||
var bgCol = ImGui.GetColorU32((held && hovered) ? ImGuiCol.ButtonActive : hovered ? ImGuiCol.ButtonHovered : ImGuiCol.Button);
|
||||
var textCol = ImGui.GetColorU32(ImGuiCol.Text);
|
||||
if (hovered || held)
|
||||
drawList.AddCircleFilled(GetCenter(bb) + new Vector2(0.0f, -0.5f), (fontSize * 0.5f) + 1.0f, bgCol);
|
||||
|
||||
var offset = button.IconOffset * ImGuiHelpers.GlobalScale;
|
||||
drawList.AddText(InterfaceManager.IconFont, (float)(fontSize * 0.8), new Vector2(bb.X + offset.X, bb.Y + offset.Y), textCol, button.Icon.ToIconString());
|
||||
|
||||
if (hovered)
|
||||
button.ShowTooltip?.Invoke();
|
||||
|
||||
// Switch to moving the window after mouse is moved beyond the initial drag threshold
|
||||
if (ImGui.IsItemActive() && ImGui.IsMouseDragging(ImGuiMouseButton.Left) && !this.internalIsClickthrough)
|
||||
ImGuiNativeAdditions.igStartMouseMovingWindow(window);
|
||||
|
||||
return pressed;
|
||||
}
|
||||
|
||||
foreach (var button in buttons.OrderBy(x => x.Priority))
|
||||
{
|
||||
if (this.internalIsClickthrough && !button.AvailableClickthrough)
|
||||
return;
|
||||
|
||||
Vector2 position = new(titleBarRect.Z - padR - buttonSize, titleBarRect.Y + style.FramePadding.Y);
|
||||
padR += buttonSize + style.ItemInnerSpacing.X;
|
||||
|
||||
if (DrawButton(button, position))
|
||||
button.Click?.Invoke(ImGuiMouseButton.Left);
|
||||
}
|
||||
|
||||
ImGui.PopClipRect();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Structure detailing the size constraints of a window.
|
||||
/// </summary>
|
||||
|
|
@ -509,14 +616,63 @@ public abstract class Window
|
|||
/// </summary>
|
||||
public Vector2 MaximumSize { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Structure describing a title bar button.
|
||||
/// </summary>
|
||||
public struct TitleBarButton
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the icon of the button.
|
||||
/// </summary>
|
||||
public FontAwesomeIcon Icon { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a vector by which the position of the icon within the button shall be offset.
|
||||
/// Automatically scaled by the global font scale for you.
|
||||
/// </summary>
|
||||
public Vector2 IconOffset { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an action that is called when a tooltip shall be drawn.
|
||||
/// May be null if no tooltip shall be drawn.
|
||||
/// </summary>
|
||||
public Action? ShowTooltip { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an action that is called when the button is clicked.
|
||||
/// </summary>
|
||||
public Action<ImGuiMouseButton> Click { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the priority the button shall be shown in.
|
||||
/// Lower = closer to ImGui default buttons.
|
||||
/// </summary>
|
||||
public int Priority { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not the button shall be clickable
|
||||
/// when the respective window is set to clickthrough.
|
||||
/// </summary>
|
||||
public bool AvailableClickthrough { 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();
|
||||
public static extern bool igItemAdd(Vector4 bb, uint id, Vector4* navBb, uint flags);
|
||||
|
||||
[DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)]
|
||||
public static extern bool igButtonBehavior(Vector4 bb, uint id, bool* outHovered, bool* outHeld, ImGuiButtonFlags flags);
|
||||
|
||||
[DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)]
|
||||
public static extern void* igGetCurrentWindow();
|
||||
|
||||
[DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)]
|
||||
public static extern void igStartMouseMovingWindow(void* window);
|
||||
|
||||
[DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)]
|
||||
public static extern unsafe void ImGuiWindow_TitleBarRect(Vector4* pOut, void* window);
|
||||
public static extern void ImGuiWindow_TitleBarRect(Vector4* pOut, void* window);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue