mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 10:17:22 +01:00
952 lines
33 KiB
C#
952 lines
33 KiB
C#
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;
|
|
using Dalamud.Bindings.ImGui;
|
|
using Dalamud.Game.ClientState.Keys;
|
|
using Dalamud.Interface.Colors;
|
|
using Dalamud.Interface.Components;
|
|
using Dalamud.Interface.Internal;
|
|
using Dalamud.Interface.Textures.Internal;
|
|
using Dalamud.Interface.Textures.TextureWraps;
|
|
using Dalamud.Interface.Utility;
|
|
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;
|
|
|
|
namespace Dalamud.Interface.Windowing;
|
|
|
|
/// <summary>
|
|
/// Base class you can use to implement an ImGui window for use with the built-in <see cref="WindowSystem"/>.
|
|
/// </summary>
|
|
public abstract class Window
|
|
{
|
|
private const float FadeInOutTime = 0.072f;
|
|
|
|
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 bool didPushInternalAlpha = false;
|
|
private float? internalAlpha = null;
|
|
private bool nextFrameBringToFront = false;
|
|
|
|
private bool hasInitializedFromPreset = false;
|
|
private PresetModel.PresetWindow? presetWindow;
|
|
private bool presetDirty = false;
|
|
|
|
private bool pushedFadeInAlpha = false;
|
|
private float fadeInTimer = 0f;
|
|
private float fadeOutTimer = 0f;
|
|
private IDrawListTextureWrap? fadeOutTexture = null;
|
|
private Vector2 fadeOutSize = Vector2.Zero;
|
|
private Vector2 fadeOutOrigin = Vector2.Zero;
|
|
|
|
/// <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 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 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;
|
|
this.Flags = flags;
|
|
this.ForceMainWindow = forceMainWindow;
|
|
}
|
|
|
|
/// <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 a unique ID to it by specifying it after "###" behind the window title.
|
|
/// </param>
|
|
protected Window(string name)
|
|
: this(name, ImGuiWindowFlags.None)
|
|
{
|
|
}
|
|
|
|
/// <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>
|
|
/// Do not draw non-critical animations.
|
|
/// </summary>
|
|
IsReducedMotion = 1 << 3,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the namespace of the window.
|
|
/// </summary>
|
|
public string? Namespace { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the name of the 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.
|
|
/// </summary>
|
|
public string WindowName { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether the window is focused.
|
|
/// </summary>
|
|
public bool IsFocused { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether this window is to be closed with a hotkey, like Escape, and keep game addons open in turn if it is closed.
|
|
/// </summary>
|
|
public bool RespectCloseHotkey { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether this window should not generate sound effects when opening and closing.
|
|
/// </summary>
|
|
public bool DisableWindowSounds { get; set; } = false;
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value representing the sound effect id to be played when the window is opened.
|
|
/// </summary>
|
|
public uint OnOpenSfxId { get; set; } = 23u;
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value representing the sound effect id to be played when the window is closed.
|
|
/// </summary>
|
|
public uint OnCloseSfxId { get; set; } = 24u;
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether this window should not fade in and out, regardless of the users'
|
|
/// preference.
|
|
/// </summary>
|
|
public bool DisableFadeInFadeOut { get; set; } = false;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the position of this window.
|
|
/// </summary>
|
|
public Vector2? Position { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the condition that defines when the position of this window is set.
|
|
/// </summary>
|
|
public ImGuiCond PositionCondition { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the size of the window. The size provided will be scaled by the global scale.
|
|
/// </summary>
|
|
public Vector2? Size { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the condition that defines when the size of this window is set.
|
|
/// </summary>
|
|
public ImGuiCond SizeCondition { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the size constraints of the window. The size constraints provided will be scaled by the global scale.
|
|
/// </summary>
|
|
public WindowSizeConstraints? SizeConstraints { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether this window is collapsed.
|
|
/// </summary>
|
|
public bool? Collapsed { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the condition that defines when the collapsed state of this window is set.
|
|
/// </summary>
|
|
public ImGuiCond CollapsedCondition { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the window flags.
|
|
/// </summary>
|
|
public ImGuiWindowFlags Flags { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether this ImGui window will be forced to stay inside the main game window.
|
|
/// </summary>
|
|
public bool ForceMainWindow { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets this window's background alpha value.
|
|
/// </summary>
|
|
public float? BgAlpha { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether this ImGui window should display a close button in the title bar.
|
|
/// </summary>
|
|
public bool ShowCloseButton { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether this window should offer to be pinned via the window's titlebar context menu.
|
|
/// </summary>
|
|
public bool AllowPinning { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether this window should offer to be made click-through via the window's titlebar context menu.
|
|
/// </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 this window will stay open.
|
|
/// </summary>
|
|
public bool IsOpen
|
|
{
|
|
get => this.internalIsOpen;
|
|
set => this.internalIsOpen = value;
|
|
}
|
|
|
|
private bool CanShowCloseButton => this.ShowCloseButton && !this.internalIsClickthrough;
|
|
|
|
/// <summary>
|
|
/// Toggle window is open state.
|
|
/// </summary>
|
|
public void Toggle()
|
|
{
|
|
this.IsOpen ^= true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bring this window to the front.
|
|
/// </summary>
|
|
public void BringToFront()
|
|
{
|
|
if (!this.IsOpen)
|
|
return;
|
|
|
|
this.nextFrameBringToFront = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Code to always be executed before the open-state of the window is checked.
|
|
/// </summary>
|
|
public virtual void PreOpenCheck()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Additional conditions for the window to be drawn, regardless of its open-state.
|
|
/// </summary>
|
|
/// <returns>
|
|
/// True if the window should be drawn, false otherwise.
|
|
/// </returns>
|
|
/// <remarks>
|
|
/// Not being drawn due to failing this condition will not change focus or trigger OnClose.
|
|
/// This is checked before PreDraw, but after Update.
|
|
/// </remarks>
|
|
public virtual bool DrawConditions()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Code to be executed before conditionals are applied and the window is drawn.
|
|
/// </summary>
|
|
public virtual void PreDraw()
|
|
{
|
|
if (this.internalAlpha.HasValue)
|
|
{
|
|
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, this.internalAlpha.Value);
|
|
this.didPushInternalAlpha = true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Code to be executed after the window is drawn.
|
|
/// </summary>
|
|
public virtual void PostDraw()
|
|
{
|
|
if (this.didPushInternalAlpha)
|
|
{
|
|
ImGui.PopStyleVar();
|
|
this.didPushInternalAlpha = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Code to be executed every time the window renders.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// In this method, implement your drawing code.
|
|
/// You do NOT need to ImGui.Begin your window.
|
|
/// </remarks>
|
|
public abstract void Draw();
|
|
|
|
/// <summary>
|
|
/// Code to be executed when the window is opened.
|
|
/// </summary>
|
|
public virtual void OnOpen()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Code to be executed when the window is closed.
|
|
/// </summary>
|
|
public virtual void OnClose()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Code to be executed when the window is safe to be disposed or removed from the window system.
|
|
/// Doing so in <see cref="OnClose"/> may result in animations not playing correctly.
|
|
/// </summary>
|
|
public virtual void OnSafeToRemove()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Code to be executed every frame, even when the window is collapsed.
|
|
/// </summary>
|
|
public virtual void Update()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Draw the window via ImGui.
|
|
/// </summary>
|
|
/// <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 doFades = !internalDrawFlags.HasFlag(WindowDrawFlags.IsReducedMotion) && !this.DisableFadeInFadeOut;
|
|
|
|
if (!this.IsOpen)
|
|
{
|
|
if (this.internalIsOpen != this.internalLastIsOpen)
|
|
{
|
|
this.internalLastIsOpen = this.internalIsOpen;
|
|
this.OnClose();
|
|
|
|
this.IsFocused = false;
|
|
|
|
if (internalDrawFlags.HasFlag(WindowDrawFlags.UseSoundEffects) && !this.DisableWindowSounds)
|
|
UIGlobals.PlaySoundEffect(this.OnCloseSfxId);
|
|
}
|
|
|
|
if (this.fadeOutTexture != null)
|
|
{
|
|
this.fadeOutTimer -= ImGui.GetIO().DeltaTime;
|
|
if (this.fadeOutTimer <= 0f)
|
|
{
|
|
this.fadeOutTexture.Dispose();
|
|
this.fadeOutTexture = null;
|
|
this.OnSafeToRemove();
|
|
}
|
|
else
|
|
{
|
|
this.DrawFakeFadeOutWindow();
|
|
}
|
|
}
|
|
|
|
this.fadeInTimer = doFades ? 0f : FadeInOutTime;
|
|
return;
|
|
}
|
|
|
|
this.fadeInTimer += ImGui.GetIO().DeltaTime;
|
|
if (this.fadeInTimer > FadeInOutTime)
|
|
this.fadeInTimer = FadeInOutTime;
|
|
|
|
this.Update();
|
|
if (!this.DrawConditions())
|
|
return;
|
|
|
|
var hasNamespace = !string.IsNullOrEmpty(this.Namespace);
|
|
|
|
if (hasNamespace)
|
|
ImGui.PushID(this.Namespace);
|
|
|
|
this.PreHandlePreset(persistence);
|
|
|
|
if (this.internalLastIsOpen != this.internalIsOpen && this.internalIsOpen)
|
|
{
|
|
this.internalLastIsOpen = this.internalIsOpen;
|
|
this.OnOpen();
|
|
|
|
if (internalDrawFlags.HasFlag(WindowDrawFlags.UseSoundEffects) && !this.DisableWindowSounds)
|
|
UIGlobals.PlaySoundEffect(this.OnOpenSfxId);
|
|
}
|
|
|
|
this.PreDraw();
|
|
this.ApplyConditionals();
|
|
|
|
if (this.ForceMainWindow)
|
|
ImGuiHelpers.ForceNextWindowMainViewport();
|
|
|
|
var wasFocused = this.IsFocused;
|
|
if (wasFocused)
|
|
{
|
|
var style = ImGui.GetStyle();
|
|
var focusedHeaderColor = style.Colors[(int)ImGuiCol.TitleBgActive];
|
|
ImGui.PushStyleColor(ImGuiCol.TitleBgCollapsed, focusedHeaderColor);
|
|
}
|
|
|
|
if (this.nextFrameBringToFront)
|
|
{
|
|
ImGui.SetNextWindowFocus();
|
|
this.nextFrameBringToFront = false;
|
|
}
|
|
|
|
var flags = this.Flags;
|
|
|
|
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.CanShowCloseButton ? ImGui.Begin(this.WindowName, ref this.internalIsOpen, flags) : ImGui.Begin(this.WindowName, flags))
|
|
{
|
|
var context = ImGui.GetCurrentContext();
|
|
if (!context.IsNull)
|
|
{
|
|
ImGuiP.GetCurrentWindow().InheritNoInputs = this.internalIsClickthrough;
|
|
}
|
|
|
|
// Not supported yet on non-main viewports
|
|
if ((this.internalIsPinned || this.internalIsClickthrough || this.internalAlpha.HasValue) &&
|
|
ImGui.GetWindowViewport().ID != ImGui.GetMainViewport().ID)
|
|
{
|
|
this.internalAlpha = null;
|
|
this.internalIsPinned = false;
|
|
this.internalIsClickthrough = false;
|
|
this.presetDirty = true;
|
|
}
|
|
|
|
// Draw the actual window contents
|
|
try
|
|
{
|
|
this.Draw();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Error(ex, "Error during Draw(): {WindowName}", this.WindowName);
|
|
}
|
|
}
|
|
|
|
const string additionsPopupName = "WindowSystemContextActions";
|
|
var flagsApplicableForTitleBarIcons = !flags.HasFlag(ImGuiWindowFlags.NoDecoration) &&
|
|
!flags.HasFlag(ImGuiWindowFlags.NoTitleBar);
|
|
var showAdditions = (this.AllowPinning || this.AllowClickthrough) &&
|
|
internalDrawFlags.HasFlag(WindowDrawFlags.UseAdditionalOptions) &&
|
|
flagsApplicableForTitleBarIcons;
|
|
var printWindow = false;
|
|
if (showAdditions)
|
|
{
|
|
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 1f);
|
|
|
|
if (ImGui.BeginPopup(additionsPopupName, ImGuiWindowFlags.NoMove))
|
|
{
|
|
var isAvailable = ImGuiHelpers.CheckIsWindowOnMainViewport();
|
|
|
|
if (!isAvailable)
|
|
ImGui.BeginDisabled();
|
|
|
|
if (this.internalIsClickthrough)
|
|
ImGui.BeginDisabled();
|
|
|
|
if (this.AllowPinning)
|
|
{
|
|
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, nor will they close when escape is pressed."));
|
|
}
|
|
|
|
if (this.internalIsClickthrough)
|
|
ImGui.EndDisabled();
|
|
|
|
if (this.AllowClickthrough)
|
|
{
|
|
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 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."));
|
|
}
|
|
|
|
if (!isAvailable)
|
|
ImGui.EndDisabled();
|
|
|
|
if (ImGui.Button(Loc.Localize("WindowSystemContextActionPrintWindow", "Print window")))
|
|
printWindow = true;
|
|
|
|
ImGui.EndPopup();
|
|
}
|
|
|
|
ImGui.PopStyleVar();
|
|
}
|
|
|
|
unsafe
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
|
|
if (wasFocused)
|
|
{
|
|
ImGui.PopStyleColor();
|
|
}
|
|
|
|
this.IsFocused = ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows);
|
|
|
|
if (internalDrawFlags.HasFlag(WindowDrawFlags.UseFocusManagement) && !this.internalIsPinned)
|
|
{
|
|
var escapeDown = Service<KeyState>.Get()[VirtualKey.ESCAPE];
|
|
if (escapeDown && this.IsFocused && !wasEscPressedLastFrame && this.RespectCloseHotkey)
|
|
{
|
|
this.IsOpen = false;
|
|
wasEscPressedLastFrame = true;
|
|
}
|
|
else if (!escapeDown && wasEscPressedLastFrame)
|
|
{
|
|
wasEscPressedLastFrame = false;
|
|
}
|
|
}
|
|
|
|
this.fadeOutSize = ImGui.GetWindowSize();
|
|
this.fadeOutOrigin = ImGui.GetWindowPos();
|
|
var isCollapsed = ImGui.IsWindowCollapsed();
|
|
var isDocked = ImGui.IsWindowDocked();
|
|
|
|
ImGui.End();
|
|
|
|
if (this.pushedFadeInAlpha)
|
|
{
|
|
ImGui.PopStyleVar();
|
|
this.pushedFadeInAlpha = false;
|
|
}
|
|
|
|
// TODO: No fade-out if the window is collapsed. We could do this if we knew the "FullSize" of the window
|
|
// from the internal ImGuiWindow, but I don't want to mess with that here for now. We can do this a lot
|
|
// easier with the new bindings.
|
|
// TODO: No fade-out if docking is enabled and the window is docked, since this makes them "unsnap".
|
|
// Ideally we should get rid of this "fake window" thing and just insert a new drawlist at the correct spot.
|
|
if (!this.internalIsOpen && this.fadeOutTexture == null && doFades && !isCollapsed && !isDocked)
|
|
{
|
|
this.fadeOutTexture = Service<TextureManager>.Get().CreateDrawListTexture(
|
|
"WindowFadeOutTexture");
|
|
this.fadeOutTexture.ResizeAndDrawWindow(this.WindowName, Vector2.One);
|
|
this.fadeOutTimer = FadeInOutTime;
|
|
}
|
|
|
|
if (printWindow)
|
|
{
|
|
var tex = Service<TextureManager>.Get().CreateDrawListTexture(
|
|
Loc.Localize("WindowSystemContextActionPrintWindow", "Print window"));
|
|
tex.ResizeAndDrawWindow(this.WindowName, Vector2.One);
|
|
_ = Service<DevTextureSaveMenu>.Get().ShowTextureSaveMenuAsync(
|
|
this.WindowName,
|
|
this.WindowName,
|
|
Task.FromResult<IDalamudTextureWrap>(tex));
|
|
}
|
|
|
|
this.PostDraw();
|
|
|
|
this.PostHandlePreset(persistence);
|
|
|
|
if (hasNamespace)
|
|
ImGui.PopID();
|
|
}
|
|
|
|
private unsafe void ApplyConditionals()
|
|
{
|
|
if (this.Position.HasValue)
|
|
{
|
|
var pos = this.Position.Value;
|
|
|
|
if (this.ForceMainWindow)
|
|
pos += ImGuiHelpers.MainViewport.Pos;
|
|
|
|
ImGui.SetNextWindowPos(pos, this.PositionCondition);
|
|
}
|
|
|
|
if (this.Size.HasValue)
|
|
{
|
|
ImGui.SetNextWindowSize(this.Size.Value * ImGuiHelpers.GlobalScale, this.SizeCondition);
|
|
}
|
|
|
|
if (this.Collapsed.HasValue)
|
|
{
|
|
ImGui.SetNextWindowCollapsed(this.Collapsed.Value, this.CollapsedCondition);
|
|
}
|
|
|
|
if (this.SizeConstraints.HasValue)
|
|
{
|
|
ImGui.SetNextWindowSizeConstraints(this.SizeConstraints.Value.MinimumSize * ImGuiHelpers.GlobalScale, this.SizeConstraints.Value.MaximumSize * ImGuiHelpers.GlobalScale);
|
|
}
|
|
|
|
var maxBgAlpha = this.internalAlpha ?? this.BgAlpha;
|
|
var fadeInAlpha = this.fadeInTimer / FadeInOutTime;
|
|
if (fadeInAlpha < 1f)
|
|
{
|
|
maxBgAlpha = maxBgAlpha.HasValue ?
|
|
Math.Clamp(maxBgAlpha.Value * fadeInAlpha, 0f, 1f) :
|
|
(*ImGui.GetStyleColorVec4(ImGuiCol.WindowBg)).W * fadeInAlpha;
|
|
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * fadeInAlpha);
|
|
this.pushedFadeInAlpha = true;
|
|
}
|
|
|
|
if (maxBgAlpha.HasValue)
|
|
{
|
|
ImGui.SetNextWindowBgAlpha(maxBgAlpha.Value);
|
|
}
|
|
}
|
|
|
|
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(ImGuiWindowPtr window, ImGuiWindowFlags flags, ImRect 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(ImRect rect) => new((rect.Min.X + rect.Max.X) * 0.5f, (rect.Min.Y + rect.Max.Y) * 0.5f);
|
|
|
|
var numButtons = 0;
|
|
bool DrawButton(TitleBarButton button, Vector2 pos)
|
|
{
|
|
var id = ImGui.GetID($"###CustomTbButton{numButtons}");
|
|
numButtons++;
|
|
|
|
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;
|
|
|
|
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;
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
pressed = ImGuiP.ButtonBehavior(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.Min.X + offset.X, bb.Min.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)
|
|
ImGuiP.StartMouseMovingWindow(window);
|
|
|
|
return pressed;
|
|
}
|
|
|
|
foreach (var button in buttons.OrderBy(x => x.Priority))
|
|
{
|
|
if (this.internalIsClickthrough && !button.AvailableClickthrough)
|
|
return;
|
|
|
|
Vector2 position = new(titleBarRect.Max.X - padR - buttonSize, titleBarRect.Min.Y + style.FramePadding.Y);
|
|
padR += buttonSize + style.ItemInnerSpacing.X;
|
|
|
|
if (DrawButton(button, position))
|
|
button.Click?.Invoke(ImGuiMouseButton.Left);
|
|
}
|
|
|
|
ImGui.PopClipRect();
|
|
}
|
|
|
|
private void DrawFakeFadeOutWindow()
|
|
{
|
|
// Draw a fake window to fade out, so that the fade out texture stays in the right place in the
|
|
// focus order
|
|
ImGui.SetNextWindowPos(this.fadeOutOrigin);
|
|
ImGui.SetNextWindowSize(this.fadeOutSize);
|
|
|
|
using var style = ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, Vector2.Zero);
|
|
style.Push(ImGuiStyleVar.WindowBorderSize, 0);
|
|
style.Push(ImGuiStyleVar.FrameBorderSize, 0);
|
|
|
|
const ImGuiWindowFlags flags = ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav |
|
|
ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoMouseInputs |
|
|
ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoBackground;
|
|
if (ImGui.Begin(this.WindowName, flags))
|
|
{
|
|
var dl = ImGui.GetWindowDrawList();
|
|
dl.AddImage(
|
|
this.fadeOutTexture!.Handle,
|
|
this.fadeOutOrigin,
|
|
this.fadeOutOrigin + this.fadeOutSize,
|
|
Vector2.Zero,
|
|
Vector2.One,
|
|
ImGui.ColorConvertFloat4ToU32(new(1f, 1f, 1f, Math.Clamp(this.fadeOutTimer / FadeInOutTime, 0f, 1f))));
|
|
}
|
|
|
|
ImGui.End();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Structure detailing the size constraints of a window.
|
|
/// </summary>
|
|
public struct WindowSizeConstraints
|
|
{
|
|
private Vector2 internalMaxSize = new(float.MaxValue);
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="WindowSizeConstraints"/> struct.
|
|
/// </summary>
|
|
public WindowSizeConstraints()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the minimum size of the window.
|
|
/// </summary>
|
|
public Vector2 MinimumSize { get; set; } = new(0);
|
|
|
|
/// <summary>
|
|
/// Gets or sets the maximum size of the window.
|
|
/// </summary>
|
|
public Vector2 MaximumSize
|
|
{
|
|
get => this.GetSafeMaxSize();
|
|
set => this.internalMaxSize = value;
|
|
}
|
|
|
|
private Vector2 GetSafeMaxSize()
|
|
{
|
|
var currentMin = this.MinimumSize;
|
|
|
|
if (this.internalMaxSize.X < currentMin.X || this.internalMaxSize.Y < currentMin.Y)
|
|
return new Vector2(float.MaxValue);
|
|
|
|
return this.internalMaxSize;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Structure describing a title bar button.
|
|
/// </summary>
|
|
public class 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 the button shall be clickable
|
|
/// when the respective window is set to clickthrough.
|
|
/// </summary>
|
|
public bool AvailableClickthrough { get; set; }
|
|
}
|
|
}
|