diff --git a/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs b/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs
index ca75e5ce0..9420fe42c 100644
--- a/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs
+++ b/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs
@@ -9,6 +9,7 @@ using Dalamud.Configuration.Internal;
using Dalamud.Interface.Colors;
using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.ManagedFontAtlas;
+using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Interface.Utility;
using Dalamud.Utility;
@@ -84,11 +85,22 @@ public sealed class SingleFontChooserDialog : IDisposable
private IFontHandle? fontHandle;
private SingleFontSpec selectedFont;
- ///
- /// Initializes a new instance of the class.
- ///
+ private bool popupPositionChanged;
+ private bool popupSizeChanged;
+ private Vector2 popupPosition = new(float.NaN);
+ private Vector2 popupSize = new(float.NaN);
+
+ /// Initializes a new instance of the class.
/// A new instance of created using
/// as its auto-rebuild mode.
+ /// The passed instance of will be disposed after use. If you pass an atlas
+ /// that is already being used, then all the font handles under the passed atlas will be invalidated upon disposing
+ /// this font chooser. Consider using for automatic
+ /// handling of font atlas derived from a , or even for automatic
+ /// registration and unregistration of event handler in addition to automatic disposal of this
+ /// class and the temporary font atlas for this font chooser dialog.
+ [Obsolete("See remarks, and use the other constructor.", false)]
+ [Api10ToDo("Make private.")]
public SingleFontChooserDialog(IFontAtlas newAsyncAtlas)
{
this.counter = Interlocked.Increment(ref counterStatic);
@@ -99,6 +111,39 @@ public sealed class SingleFontChooserDialog : IDisposable
Encoding.UTF8.GetBytes("Font preview.\n0123456789!", this.fontPreviewText);
}
+#pragma warning disable CS0618 // Type or member is obsolete
+ // TODO: Api10ToDo; Remove this pragma warning disable line
+
+ /// Initializes a new instance of the class.
+ /// The relevant instance of UiBuilder.
+ /// Whether the fonts in the atlas is global scaled.
+ /// Atlas name for debugging purposes.
+ ///
+ /// The passed is only used for creating a temporary font atlas. It will not
+ /// automatically register a hander for .
+ /// Consider using for automatic registration and unregistration of
+ /// event handler in addition to automatic disposal of this class and the temporary font atlas
+ /// for this font chooser dialog.
+ ///
+ public SingleFontChooserDialog(UiBuilder uiBuilder, bool isGlobalScaled = true, string? debugAtlasName = null)
+ : this(uiBuilder.CreateFontAtlas(FontAtlasAutoRebuildMode.Async, isGlobalScaled, debugAtlasName))
+ {
+ }
+
+ /// Initializes a new instance of the class.
+ /// An instance of .
+ /// The temporary atlas name.
+ internal SingleFontChooserDialog(FontAtlasFactory factory, string debugAtlasName)
+ : this(factory.CreateFontAtlas(debugAtlasName, FontAtlasAutoRebuildMode.Async))
+ {
+ }
+
+#pragma warning restore CS0618 // Type or member is obsolete
+ // TODO: Api10ToDo; Remove this pragma warning restore line
+
+ /// Called when the selected font spec has changed.
+ public event Action? SelectedFontSpecChanged;
+
///
/// Gets or sets the title of this font chooser dialog popup.
///
@@ -153,6 +198,8 @@ public sealed class SingleFontChooserDialog : IDisposable
this.useAdvancedOptions |= Math.Abs(value.LineHeight - 1f) > 0.000001;
this.useAdvancedOptions |= value.GlyphOffset != default;
this.useAdvancedOptions |= value.LetterSpacing != 0f;
+
+ this.SelectedFontSpecChanged?.Invoke(this.selectedFont);
}
}
@@ -166,15 +213,55 @@ public sealed class SingleFontChooserDialog : IDisposable
///
public bool IgnorePreviewGlobalScale { get; set; }
- ///
- /// Creates a new instance of that will automatically draw and dispose itself as
- /// needed.
+ /// Gets or sets a value indicating whether this popup should be modal, blocking everything behind from
+ /// being interacted.
+ /// If true, then will be
+ /// used. Otherwise, will be used.
+ public bool IsModal { get; set; } = true;
+
+ /// Gets or sets the window flags.
+ public ImGuiWindowFlags WindowFlags { get; set; }
+
+ /// Gets or sets the popup window position.
+ ///
+ /// Setting the position only works before the first call to .
+ /// If any of the coordinates are , default position will be used.
+ /// The position will be clamped into the work area of the selected monitor.
+ ///
+ public Vector2 PopupPosition
+ {
+ get => this.popupPosition;
+ set
+ {
+ this.popupPositionChanged = true;
+ this.popupPosition = value;
+ }
+ }
+
+ /// Gets or sets the popup window size.
+ ///
+ /// Setting the size only works before the first call to .
+ /// If any of the coordinates are , default size will be used.
+ /// The size will be clamped into the work area of the selected monitor.
+ ///
+ public Vector2 PopupSize
+ {
+ get => this.popupSize;
+ set
+ {
+ this.popupSizeChanged = true;
+ this.popupSize = value;
+ }
+ }
+
+ /// Creates a new instance of that will automatically draw and
+ /// dispose itself as needed; calling and are handled automatically.
///
/// An instance of .
/// The new instance of .
public static SingleFontChooserDialog CreateAuto(UiBuilder uiBuilder)
{
- var fcd = new SingleFontChooserDialog(uiBuilder.CreateFontAtlas(FontAtlasAutoRebuildMode.Async));
+ var fcd = new SingleFontChooserDialog(uiBuilder);
uiBuilder.Draw += fcd.Draw;
fcd.tcs.Task.ContinueWith(
r =>
@@ -187,6 +274,14 @@ public sealed class SingleFontChooserDialog : IDisposable
return fcd;
}
+ /// Gets the default popup size before clamping to monitor work area.
+ /// The default popup size.
+ public static Vector2 GetDefaultPopupSizeNonClamped()
+ {
+ ThreadSafety.AssertMainThread();
+ return new Vector2(40, 30) * ImGui.GetTextLineHeight();
+ }
+
///
public void Dispose()
{
@@ -204,13 +299,28 @@ public sealed class SingleFontChooserDialog : IDisposable
ImGui.GetIO().WantTextInput = false;
}
+ /// Sets and to be at the center of the current window
+ /// being drawn.
+ /// The preferred popup size.
+ public void SetPopupPositionAndSizeToCurrentWindowCenter(Vector2 preferredPopupSize)
+ {
+ ThreadSafety.AssertMainThread();
+ this.PopupSize = preferredPopupSize;
+ this.PopupPosition = ImGui.GetWindowPos() + ((ImGui.GetWindowSize() - preferredPopupSize) / 2);
+ }
+
+ /// Sets and to be at the center of the current window
+ /// being drawn.
+ public void SetPopupPositionAndSizeToCurrentWindowCenter() =>
+ this.SetPopupPositionAndSizeToCurrentWindowCenter(GetDefaultPopupSizeNonClamped());
+
///
/// Draws this dialog.
///
public void Draw()
{
- if (this.firstDraw)
- ImGui.OpenPopup(this.popupImGuiName);
+ const float popupMinWidth = 320;
+ const float popupMinHeight = 240;
ImGui.GetIO().WantCaptureKeyboard = true;
ImGui.GetIO().WantTextInput = true;
@@ -220,12 +330,70 @@ public sealed class SingleFontChooserDialog : IDisposable
return;
}
- var open = true;
- ImGui.SetNextWindowSize(new(640, 480), ImGuiCond.Appearing);
- if (!ImGui.BeginPopupModal(this.popupImGuiName, ref open) || !open)
+ if (this.firstDraw)
{
- this.Cancel();
- return;
+ if (this.IsModal)
+ ImGui.OpenPopup(this.popupImGuiName);
+ }
+
+ if (this.firstDraw || this.popupPositionChanged || this.popupSizeChanged)
+ {
+ var preferProvidedSize = !float.IsNaN(this.popupSize.X) && !float.IsNaN(this.popupSize.Y);
+ var size = preferProvidedSize ? this.popupSize : GetDefaultPopupSizeNonClamped();
+ size.X = Math.Max(size.X, popupMinWidth);
+ size.Y = Math.Max(size.Y, popupMinHeight);
+
+ var preferProvidedPos = !float.IsNaN(this.popupPosition.X) && !float.IsNaN(this.popupPosition.Y);
+ var monitorLocatorPos = preferProvidedPos ? this.popupPosition + (size / 2) : ImGui.GetMousePos();
+
+ var monitors = ImGui.GetPlatformIO().Monitors;
+ var preferredMonitor = 0;
+ var preferredDistance = GetDistanceFromMonitor(monitorLocatorPos, monitors[0]);
+ for (var i = 1; i < monitors.Size; i++)
+ {
+ var distance = GetDistanceFromMonitor(monitorLocatorPos, monitors[i]);
+ if (distance < preferredDistance)
+ {
+ preferredMonitor = i;
+ preferredDistance = distance;
+ }
+ }
+
+ var lt = monitors[preferredMonitor].WorkPos;
+ var workSize = monitors[preferredMonitor].WorkSize;
+ size.X = Math.Min(size.X, workSize.X);
+ size.Y = Math.Min(size.Y, workSize.Y);
+ var rb = (lt + workSize) - size;
+
+ var pos =
+ preferProvidedPos
+ ? new(Math.Clamp(this.PopupPosition.X, lt.X, rb.X), Math.Clamp(this.PopupPosition.Y, lt.Y, rb.Y))
+ : (lt + rb) / 2;
+
+ ImGui.SetNextWindowSize(size, ImGuiCond.Always);
+ ImGui.SetNextWindowPos(pos, ImGuiCond.Always);
+ this.popupPositionChanged = this.popupSizeChanged = false;
+ }
+
+ ImGui.SetNextWindowSizeConstraints(new(popupMinWidth, popupMinHeight), new(float.MaxValue));
+ if (this.IsModal)
+ {
+ var open = true;
+ if (!ImGui.BeginPopupModal(this.popupImGuiName, ref open, this.WindowFlags) || !open)
+ {
+ this.Cancel();
+ return;
+ }
+ }
+ else
+ {
+ var open = true;
+ if (!ImGui.Begin(this.popupImGuiName, ref open, this.WindowFlags) || !open)
+ {
+ ImGui.End();
+ this.Cancel();
+ return;
+ }
}
var framePad = ImGui.GetStyle().FramePadding;
@@ -261,12 +429,36 @@ public sealed class SingleFontChooserDialog : IDisposable
ImGui.EndChild();
- ImGui.EndPopup();
+ this.popupPosition = ImGui.GetWindowPos();
+ this.popupSize = ImGui.GetWindowSize();
+ if (this.IsModal)
+ ImGui.EndPopup();
+ else
+ ImGui.End();
this.firstDraw = false;
this.firstDrawAfterRefresh = false;
}
+ private static float GetDistanceFromMonitor(Vector2 point, ImGuiPlatformMonitorPtr monitor)
+ {
+ var lt = monitor.MainPos;
+ var rb = monitor.MainPos + monitor.MainSize;
+ var xoff =
+ point.X < lt.X
+ ? lt.X - point.X
+ : point.X > rb.X
+ ? point.X - rb.X
+ : 0;
+ var yoff =
+ point.Y < lt.Y
+ ? lt.Y - point.Y
+ : point.Y > rb.Y
+ ? point.Y - rb.Y
+ : 0;
+ return MathF.Sqrt((xoff * xoff) + (yoff * yoff));
+ }
+
private void DrawChoices()
{
var lineHeight = ImGui.GetTextLineHeight();
@@ -338,15 +530,20 @@ public sealed class SingleFontChooserDialog : IDisposable
}
}
- if (this.IgnorePreviewGlobalScale)
+ if (this.fontHandle is null)
{
- this.fontHandle ??= this.selectedFont.CreateFontHandle(
- this.atlas,
- tk => tk.OnPreBuild(e => e.SetFontScaleMode(e.Font, FontScaleMode.UndoGlobalScale)));
- }
- else
- {
- this.fontHandle ??= this.selectedFont.CreateFontHandle(this.atlas);
+ if (this.IgnorePreviewGlobalScale)
+ {
+ this.fontHandle = this.selectedFont.CreateFontHandle(
+ this.atlas,
+ tk => tk.OnPreBuild(e => e.SetFontScaleMode(e.Font, FontScaleMode.UndoGlobalScale)));
+ }
+ else
+ {
+ this.fontHandle = this.selectedFont.CreateFontHandle(this.atlas);
+ }
+
+ this.SelectedFontSpecChanged?.InvokeSafely(this.selectedFont);
}
if (this.fontHandle is null)
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs
index 8bb999557..469ef3dc3 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs
@@ -44,6 +44,8 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
private bool useBold;
private bool useMinimumBuild;
+ private SingleFontChooserDialog? chooserDialog;
+
///
public string[]? CommandShortcuts { get; init; }
@@ -126,32 +128,75 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
if (ImGui.Button("Test Lock"))
Task.Run(this.TestLock);
- ImGui.SameLine();
if (ImGui.Button("Choose Editor Font"))
{
- var fcd = new SingleFontChooserDialog(
- Service.Get().CreateFontAtlas(
- $"{nameof(GamePrebakedFontsTestWidget)}:EditorFont",
- FontAtlasAutoRebuildMode.Async));
- fcd.SelectedFont = this.fontSpec;
- fcd.IgnorePreviewGlobalScale = !this.atlasScaleMode;
- Service.Get().Draw += fcd.Draw;
- fcd.ResultTask.ContinueWith(
- r => Service.Get().RunOnFrameworkThread(
- () =>
- {
- Service.Get().Draw -= fcd.Draw;
- fcd.Dispose();
+ if (this.chooserDialog is null)
+ {
+ DoNext();
+ }
+ else
+ {
+ this.chooserDialog.Cancel();
+ this.chooserDialog.ResultTask.ContinueWith(_ => Service.Get().RunOnFrameworkThread(DoNext));
+ this.chooserDialog = null;
+ }
- _ = r.Exception;
- if (!r.IsCompletedSuccessfully)
- return;
+ void DoNext()
+ {
+ var fcd = new SingleFontChooserDialog(
+ Service.Get(),
+ $"{nameof(GamePrebakedFontsTestWidget)}:EditorFont");
+ this.chooserDialog = fcd;
+ fcd.SelectedFont = this.fontSpec;
+ fcd.IgnorePreviewGlobalScale = !this.atlasScaleMode;
+ fcd.IsModal = false;
+ Service.Get().Draw += fcd.Draw;
+ var prevSpec = this.fontSpec;
+ fcd.SelectedFontSpecChanged += spec =>
+ {
+ this.fontSpec = spec;
+ Log.Information("Selected font: {font}", this.fontSpec);
+ this.fontDialogHandle?.Dispose();
+ this.fontDialogHandle = null;
+ };
+ fcd.ResultTask.ContinueWith(
+ r => Service.Get().RunOnFrameworkThread(
+ () =>
+ {
+ Service.Get().Draw -= fcd.Draw;
+ fcd.Dispose();
- this.fontSpec = r.Result;
- Log.Information("Selected font: {font}", this.fontSpec);
- this.fontDialogHandle?.Dispose();
- this.fontDialogHandle = null;
- }));
+ _ = r.Exception;
+ var spec = r.IsCompletedSuccessfully ? r.Result : prevSpec;
+ if (this.fontSpec != spec)
+ {
+ this.fontSpec = spec;
+ this.fontDialogHandle?.Dispose();
+ this.fontDialogHandle = null;
+ }
+
+ this.chooserDialog = null;
+ }));
+ }
+ }
+
+ if (this.chooserDialog is not null)
+ {
+ ImGui.SameLine();
+ ImGui.TextUnformatted($"{this.chooserDialog.PopupPosition}, {this.chooserDialog.PopupSize}");
+
+ ImGui.SameLine();
+ if (ImGui.Button("Random Location"))
+ {
+ var monitors = ImGui.GetPlatformIO().Monitors;
+ var monitor = monitors[Random.Shared.Next() % monitors.Size];
+ this.chooserDialog.PopupPosition = monitor.WorkPos + (monitor.WorkSize * new Vector2(
+ Random.Shared.NextSingle(),
+ Random.Shared.NextSingle()));
+ this.chooserDialog.PopupSize = monitor.WorkSize * new Vector2(
+ Random.Shared.NextSingle(),
+ Random.Shared.NextSingle());
+ }
}
this.privateAtlas ??=
diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs
index ea6400121..5ccace850 100644
--- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs
+++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs
@@ -12,7 +12,6 @@ using Dalamud.Interface.GameFonts;
using Dalamud.Interface.ImGuiFontChooserDialog;
using Dalamud.Interface.Internal.Windows.PluginInstaller;
using Dalamud.Interface.Internal.Windows.Settings.Widgets;
-using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Interface.Utility;
using Dalamud.Utility;
@@ -199,10 +198,10 @@ public class SettingsTabLook : SettingsTab
if (ImGui.Button(Loc.Localize("DalamudSettingChooseDefaultFont", "Choose Default Font")))
{
var faf = Service.Get();
- var fcd = new SingleFontChooserDialog(
- faf.CreateFontAtlas($"{nameof(SettingsTabLook)}:Default", FontAtlasAutoRebuildMode.Async));
+ var fcd = new SingleFontChooserDialog(faf, $"{nameof(SettingsTabLook)}:Default");
fcd.SelectedFont = (SingleFontSpec)this.defaultFontSpec;
fcd.FontFamilyExcludeFilter = x => x is DalamudDefaultFontAndFamilyId;
+ fcd.SetPopupPositionAndSizeToCurrentWindowCenter();
interfaceManager.Draw += fcd.Draw;
fcd.ResultTask.ContinueWith(
r => Service.Get().RunOnFrameworkThread(
diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs
index 0445499c8..a79ab099d 100644
--- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs
+++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs
@@ -82,21 +82,25 @@ public interface IFontAtlas : IDisposable
///
public IDisposable SuppressAutoRebuild();
- ///
- /// Creates a new from game's built-in fonts.
- ///
+ /// Creates a new from game's built-in fonts.
/// Font to use.
/// Handle to a font that may or may not be ready yet.
+ /// This function does not throw. will be populated instead, if
+ /// the build procedure has failed. can be used regardless of the state of the font
+ /// handle.
public IFontHandle NewGameFontHandle(GameFontStyle style);
- ///
- /// Creates a new IFontHandle using your own callbacks.
- ///
+ /// Creates a new IFontHandle using your own callbacks.
/// Callback for .
/// Handle to a font that may or may not be ready yet.
///
- /// Consider calling to support
- /// glyphs that are not supplied by the game by default; this mostly affects Chinese and Korean language users.
+ /// Consider calling to
+ /// support glyphs that are not supplied by the game by default; this mostly affects Chinese and Korean language
+ /// users.
+ /// This function does not throw, even if would throw exceptions.
+ /// Instead, if it fails, the returned handle will contain an property
+ /// containing the exception happened during the build process. can be used even if
+ /// the build process has not been completed yet or failed.
///
///
/// On initialization:
diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs
index 70799bb9c..0a9e9072e 100644
--- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs
+++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs
@@ -58,10 +58,27 @@ public interface IFontHandle : IDisposable
/// A disposable object that will pop the font on dispose.
/// If called outside of the main thread.
///
- /// This function uses , and may do extra things.
+ /// This function uses , and may do extra things.
/// Use or to undo this operation.
- /// Do not use .
+ /// Do not use .
///
+ ///
+ /// Push a font with `using` clause.
+ ///
+ /// using (fontHandle.Push())
+ /// ImGui.TextUnformatted("Test");
+ ///
+ /// Push a font with a matching call to .
+ ///
+ /// fontHandle.Push();
+ /// ImGui.TextUnformatted("Test 2");
+ ///
+ /// Push a font between two choices.
+ ///
+ /// using ((someCondition ? myFontHandle : dalamudPluginInterface.UiBuilder.MonoFontHandle).Push())
+ /// ImGui.TextUnformatted("Test 3");
+ ///
+ ///
IDisposable Push();
///
diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs
index 47254a5c9..89d968158 100644
--- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs
+++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs
@@ -136,13 +136,18 @@ internal abstract class FontHandle : IFontHandle
/// An instance of that must be disposed after use on success;
/// null with populated on failure.
///
- /// Still may be thrown.
public ILockedImFont? TryLock(out string? errorMessage)
{
IFontHandleSubstance? prevSubstance = default;
while (true)
{
- var substance = this.Manager.Substance;
+ if (this.manager is not { } nonDisposedManager)
+ {
+ errorMessage = "The font handle has been disposed.";
+ return null;
+ }
+
+ var substance = nonDisposedManager.Substance;
// Does the associated IFontAtlas have a built substance?
if (substance is null)