Merge remote-tracking branch 'origin/master' into new_im_hooks-rollup

This commit is contained in:
github-actions[bot] 2024-04-02 16:27:26 +00:00
commit 6764cac648
23 changed files with 1148 additions and 67 deletions

View file

@ -40,6 +40,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DalamudCrashHandler", "Dala
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Common", "Dalamud.Common\Dalamud.Common.csproj", "{F21B13D2-D7D0-4456-B70F-3F8D695064E2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.LocExporter", "tools\Dalamud.LocExporter\Dalamud.LocExporter.csproj", "{A568929D-6FF6-4DFA-9D14-5D7DC08FA5E0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -102,6 +104,10 @@ Global
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|Any CPU.Build.0 = Release|Any CPU
{A568929D-6FF6-4DFA-9D14-5D7DC08FA5E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A568929D-6FF6-4DFA-9D14-5D7DC08FA5E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A568929D-6FF6-4DFA-9D14-5D7DC08FA5E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A568929D-6FF6-4DFA-9D14-5D7DC08FA5E0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
namespace Dalamud.Configuration.Internal;
@ -21,4 +22,9 @@ internal sealed class DevPluginSettings
/// Gets or sets an ID uniquely identifying this specific instance of a devPlugin.
/// </summary>
public Guid WorkingPluginId { get; set; } = Guid.Empty;
/// <summary>
/// Gets or sets a list of validation problems that have been dismissed by the user.
/// </summary>
public List<string> DismissedValidationProblems { get; set; } = new();
}

View file

@ -8,7 +8,7 @@
</PropertyGroup>
<PropertyGroup Label="Feature">
<DalamudVersion>9.1.0.2</DalamudVersion>
<DalamudVersion>9.1.0.5</DalamudVersion>
<Description>XIV Launcher addon framework</Description>
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
<Version>$(DalamudVersion)</Version>

View file

@ -0,0 +1,96 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Interface.Internal.Windows;
using Dalamud.Networking.Http;
using Serilog;
namespace Dalamud.Interface;
[ServiceManager.EarlyLoadedService]
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "One-off")]
internal class Fools24 : IServiceType
{
private readonly HappyHttpClient httpClient;
private CancellationTokenSource? cancellation;
private Task? horseIconTask = null;
private string[]? applicableIcons = null;
[ServiceManager.ServiceConstructor]
public Fools24(HappyHttpClient httpClient)
{
this.httpClient = httpClient;
}
public bool IsWaitingForIconList => this.horseIconTask?.IsCompleted == false;
public bool Failed { get; private set; }
public static bool IsDayApplicable()
{
var utcNow = DateTime.UtcNow;
var dateAhead = utcNow.AddHours(14);
var dateBehind = utcNow.AddHours(-12);
return dateAhead is { Day: 1, Month: 4 } || dateBehind is { Day: 1, Month: 4 } || DateTime.Now is { Day: 1, Month: 4 };
}
public string? GetHorseIconLink(string internalName)
{
if (this.applicableIcons == null || this.applicableIcons.All(x => x != $"{internalName}.png"))
return null;
return $"https://raw.githubusercontent.com/goaaats/horse-icons/main/icons/{internalName}.png";
}
public void NotifyInstallerWindowOpened()
{
if (!IsDayApplicable())
return;
Service<PluginImageCache>.Get().ClearIconCache();
if (this.horseIconTask?.IsCompleted == false)
return;
this.Failed = false;
try
{
this.cancellation = new CancellationTokenSource();
this.horseIconTask = this.httpClient.SharedHttpClient.GetStringAsync("https://raw.githubusercontent.com/goaaats/horse-icons/main/iconlist.txt", this.cancellation.Token)
.ContinueWith(
f =>
{
if (!f.IsCompletedSuccessfully)
{
this.Failed = true;
this.applicableIcons = null;
return;
}
if (f.Result is not { Length: > 0 })
{
this.Failed = true;
this.applicableIcons = null;
return;
}
this.applicableIcons = f.Result.Split(
'\n',
StringSplitOptions.RemoveEmptyEntries);
});
this.cancellation.CancelAfter(10000);
}
catch (Exception ex)
{
Log.Error(ex, "Failed to fetch horse icons");
this.Failed = true;
}
}
}

View file

@ -56,7 +56,7 @@ internal static class NotificationConstants
public const float ProgressWaveLoopMaxColorTimeRatio = 0.7f;
/// <summary>Default duration of the notification.</summary>
public static readonly TimeSpan DefaultDuration = TimeSpan.FromSeconds(3);
public static readonly TimeSpan DefaultDuration = TimeSpan.FromSeconds(7);
/// <summary>Duration of show animation.</summary>
public static readonly TimeSpan ShowAnimationDuration = TimeSpan.FromMilliseconds(300);

View file

@ -11,6 +11,7 @@ using System.Runtime.InteropServices;
using System.Text;
using System.Text.Unicode;
using Dalamud.Game;
using Dalamud.Game.Text;
using Dalamud.Hooking.WndProcHook;
using Dalamud.Interface.GameFonts;
@ -110,6 +111,7 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
/// <summary>Undo range for modifying the buffer while composition is in progress.</summary>
private (int Start, int End, int Cursor)? temporaryUndoSelection;
private bool hadWantTextInput;
private bool updateInputLanguage = true;
private bool updateImeStatusAgain;
@ -262,20 +264,37 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
if (!ImGuiHelpers.IsImGuiInitialized)
{
this.updateInputLanguage = true;
this.temporaryUndoSelection = null;
return;
}
// Are we not the target of text input?
if (!ImGui.GetIO().WantTextInput)
{
if (this.hadWantTextInput)
{
// Force the cancellation of whatever was being input.
var hImc2 = ImmGetContext(args.Hwnd);
if (hImc2 != 0)
{
ImmNotifyIME(hImc2, NI.NI_COMPOSITIONSTR, CPS_CANCEL, 0);
ImmReleaseContext(args.Hwnd, hImc2);
}
}
this.hadWantTextInput = false;
this.updateInputLanguage = true;
this.temporaryUndoSelection = null;
return;
}
this.hadWantTextInput = true;
var hImc = ImmGetContext(args.Hwnd);
if (hImc == nint.Zero)
{
this.updateInputLanguage = true;
this.temporaryUndoSelection = null;
return;
}
@ -338,9 +357,11 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
this.updateInputLanguage = false;
}
// Microsoft Korean IME and Google Japanese IME drop notifying us of a candidate list change.
// As the candidate list update is already there on the next WndProc call, update the candidate list again
// here.
if (this.updateImeStatusAgain)
{
this.ReplaceCompositionString(hImc, false);
this.UpdateCandidates(hImc);
this.updateImeStatusAgain = false;
}
@ -410,7 +431,8 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
or VK.VK_RIGHT
or VK.VK_DOWN
or VK.VK_RETURN:
if (this.candidateStrings.Count != 0)
// If key inputs that usually result in focus change, cancel the input process.
if (!string.IsNullOrEmpty(ImmGetCompositionString(hImc, GCS.GCS_COMPSTR)))
{
this.ClearState(hImc);
args.WParam = VK.VK_PROCESSKEY;
@ -428,7 +450,15 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
case WM.WM_RBUTTONDOWN:
case WM.WM_MBUTTONDOWN:
case WM.WM_XBUTTONDOWN:
ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_COMPLETE, 0);
// If mouse click happened while IME composition was in progress, force complete the input process.
if (!string.IsNullOrEmpty(ImmGetCompositionString(hImc, GCS.GCS_COMPSTR)))
{
ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_COMPLETE, 0);
// Disable further handling of mouse button down event, or something would lock up the cursor.
args.SuppressWithValue(1);
}
break;
}
}

View file

@ -943,7 +943,7 @@ internal class DalamudInterface : IInternalDisposableService
if (ImGui.MenuItem("Export localizable"))
{
localization.ExportLocalizable();
localization.ExportLocalizable(true);
}
if (ImGui.BeginMenu("Load language..."))

View file

@ -191,6 +191,11 @@ internal class PluginImageCache : IInternalDisposableService
{
iconTexture = null;
loadedSince = null;
// Wait for the horse icon list to be there, if applicable
var fools = Service<Fools24>.Get();
if (Fools24.IsDayApplicable() && fools.IsWaitingForIconList && !fools.Failed)
return false;
if (manifest == null || manifest.InternalName == null)
{
@ -638,6 +643,14 @@ internal class PluginImageCache : IInternalDisposableService
{
if (isThirdParty)
return manifest.IconUrl;
var fools = Service<Fools24>.Get();
if (Fools24.IsDayApplicable())
{
var iconLink = fools.GetHorseIconLink(manifest.InternalName);
if (iconLink != null)
return iconLink;
}
return MainRepoDip17ImageUrl.Format(manifest.Dip17Channel!, manifest.InternalName, "icon.png");
}

View file

@ -268,6 +268,8 @@ internal class PluginInstallerWindow : Window, IDisposable
/// <inheritdoc/>
public override void OnOpen()
{
Service<Fools24>.Get().NotifyInstallerWindowOpened();
var pluginManager = Service<PluginManager>.Get();
_ = pluginManager.ReloadPluginMastersAsync();
@ -596,6 +598,7 @@ internal class PluginInstallerWindow : Window, IDisposable
using (ImRaii.Disabled(isProfileManager))
{
var searchTextChanged = false;
var prevSearchText = this.searchText;
ImGui.SetNextItemWidth(searchInputWidth);
searchTextChanged |= ImGui.InputTextWithHint(
"###XlPluginInstaller_Search",
@ -615,7 +618,7 @@ internal class PluginInstallerWindow : Window, IDisposable
}
if (searchTextChanged)
this.UpdateCategoriesOnSearchChange();
this.UpdateCategoriesOnSearchChange(prevSearchText);
}
// Disable sort if changelogs or profile editor
@ -2458,6 +2461,12 @@ internal class PluginInstallerWindow : Window, IDisposable
}
}
if (plugin is LocalDevPlugin devPlugin)
{
this.DrawDevPluginValidationIssues(devPlugin);
ImGuiHelpers.ScaledDummy(5);
}
// Controls
this.DrawPluginControlButton(plugin, availablePluginUpdate);
this.DrawDevPluginButtons(plugin);
@ -2961,6 +2970,100 @@ internal class PluginInstallerWindow : Window, IDisposable
}
}
private void DrawDevPluginValidationIssues(LocalDevPlugin devPlugin)
{
if (!devPlugin.IsLoaded)
{
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, "You have to load this plugin to see validation issues.");
}
else
{
var problems = PluginValidator.CheckForProblems(devPlugin);
if (problems.Count == 0)
{
ImGui.PushFont(InterfaceManager.IconFont);
ImGui.Text(FontAwesomeIcon.Check.ToIconString());
ImGui.PopFont();
ImGui.SameLine();
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.HealerGreen, "No validation issues found in this plugin!");
}
else
{
var numValidProblems = problems.Count(
problem => devPlugin.DismissedValidationProblems.All(name => name != problem.GetType().Name));
var shouldBother = numValidProblems > 0;
var validationIssuesText = shouldBother ?
$"Found {problems.Count} validation issue{(problems.Count > 1 ? "s" : string.Empty)} in this plugin!" :
$"{problems.Count} dismissed validation issue{(problems.Count > 1 ? "s" : string.Empty)} in this plugin.";
using var col = ImRaii.PushColor(ImGuiCol.Text, shouldBother ? ImGuiColors.DalamudOrange : ImGuiColors.DalamudGrey);
using var tree = ImRaii.TreeNode($"{validationIssuesText}###validationIssueCollapsible");
if (tree.Success)
{
foreach (var problem in problems)
{
var thisProblemIsDismissed = devPlugin.DismissedValidationProblems.Contains(problem.GetType().Name);
if (!thisProblemIsDismissed)
{
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudWhite))
{
if (ImGuiComponents.IconButton(
$"##dismissValidationIssue{problem.GetType().Name}",
FontAwesomeIcon.TimesCircle))
{
devPlugin.DismissedValidationProblems.Add(problem.GetType().Name);
Service<DalamudConfiguration>.Get().QueueSave();
}
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("Dismiss this issue");
}
}
ImGui.SameLine();
}
var iconColor = problem.Severity switch
{
PluginValidator.ValidationSeverity.Fatal => ImGuiColors.DalamudRed,
PluginValidator.ValidationSeverity.Warning => ImGuiColors.DalamudOrange,
PluginValidator.ValidationSeverity.Information => ImGuiColors.TankBlue,
_ => ImGuiColors.DalamudGrey,
};
using (ImRaii.PushColor(ImGuiCol.Text, iconColor))
using (ImRaii.PushFont(InterfaceManager.IconFont))
{
switch (problem.Severity)
{
case PluginValidator.ValidationSeverity.Fatal:
ImGui.Text(FontAwesomeIcon.TimesCircle.ToIconString());
break;
case PluginValidator.ValidationSeverity.Warning:
ImGui.Text(FontAwesomeIcon.ExclamationTriangle.ToIconString());
break;
case PluginValidator.ValidationSeverity.Information:
ImGui.Text(FontAwesomeIcon.InfoCircle.ToIconString());
break;
default:
throw new ArgumentOutOfRangeException();
}
}
ImGui.SameLine();
using (ImRaii.PushColor(ImGuiCol.Text, thisProblemIsDismissed ? ImGuiColors.DalamudGrey : ImGuiColors.DalamudWhite))
{
ImGuiHelpers.SafeTextWrapped(problem.GetLocalizedDescription());
}
}
}
}
}
}
private void DrawDevPluginButtons(LocalPlugin localPlugin)
{
ImGui.SameLine();
@ -3341,15 +3444,27 @@ internal class PluginInstallerWindow : Window, IDisposable
return this.updateModalTaskCompletionSource.Task;
}
private void UpdateCategoriesOnSearchChange()
private void UpdateCategoriesOnSearchChange(string? previousSearchText)
{
if (string.IsNullOrEmpty(this.searchText))
{
this.categoryManager.SetCategoryHighlightsForPlugins(null);
// Reset here for good measure, as we're returning from a search
this.openPluginCollapsibles.Clear();
}
else
{
var pluginsMatchingSearch = this.pluginListAvailable.Where(rm => !this.IsManifestFiltered(rm));
var pluginsMatchingSearch = this.pluginListAvailable.Where(rm => !this.IsManifestFiltered(rm)).ToArray();
// Check if the search results are different, and clear the open collapsibles if they are
if (previousSearchText != null)
{
var previousSearchResults = this.pluginListAvailable.Where(rm => !this.IsManifestFiltered(rm)).ToArray();
if (!previousSearchResults.SequenceEqual(pluginsMatchingSearch))
this.openPluginCollapsibles.Clear();
}
this.categoryManager.SetCategoryHighlightsForPlugins(pluginsMatchingSearch);
}
}
@ -3357,7 +3472,7 @@ internal class PluginInstallerWindow : Window, IDisposable
private void UpdateCategoriesOnPluginsChange()
{
this.categoryManager.BuildCategories(this.pluginListAvailable);
this.UpdateCategoriesOnSearchChange();
this.UpdateCategoriesOnSearchChange(null);
}
private void DrawFontawesomeIconOutlined(FontAwesomeIcon icon, Vector4 outline, Vector4 iconColor)
@ -3760,7 +3875,7 @@ internal class PluginInstallerWindow : Window, IDisposable
public static string FeedbackModal_ContactInformationRequired => Loc.Localize("InstallerFeedbackContactInfoRequired", "Contact information has not been provided. We require contact information to respond to questions, or to request additional information to troubleshoot problems.");
public static string FeedbackModal_ContactInformationDiscordButton => Loc.Localize("ContactInformationDiscordButton", "Join Goat Place Discord");
public static string FeedbackModal_ContactInformationDiscordButton => Loc.Localize("ContactInformationDiscordButton", "Join XIVLauncher & Dalamud Discord");
public static string FeedbackModal_ContactInformationDiscordUrl => Loc.Localize("ContactInformationDiscordUrl", "https://goat.place/");

View file

@ -651,13 +651,13 @@ internal class ProfileManagerWidget
Loc.Localize("ProfileManagerTutorialCommands", "You can use the following commands in chat or in macros to manage active collections:");
public static string TutorialCommandsEnable =>
Loc.Localize("ProfileManagerTutorialCommandsEnable", "/xlenableprofile \"Collection Name\" - Enable a collection");
Loc.Localize("ProfileManagerTutorialCommandsEnable", "{0} \"Collection Name\" - Enable a collection").Format(ProfileCommandHandler.CommandEnable);
public static string TutorialCommandsDisable =>
Loc.Localize("ProfileManagerTutorialCommandsDisable", "/xldisableprofile \"Collection Name\" - Disable a collection");
Loc.Localize("ProfileManagerTutorialCommandsDisable", "{0} \"Collection Name\" - Disable a collection").Format(ProfileCommandHandler.CommandDisable);
public static string TutorialCommandsToggle =>
Loc.Localize("ProfileManagerTutorialCommandsToggle", "/xltoggleprofile \"Collection Name\" - Toggle a collection's state");
Loc.Localize("ProfileManagerTutorialCommandsToggle", "{0} \"Collection Name\" - Toggle a collection's state").Format(ProfileCommandHandler.CommandToggle);
public static string TutorialCommandsEnd =>
Loc.Localize("ProfileManagerTutorialCommandsEnd", "If you run multiple of these commands, they will be executed in order.");

View file

@ -182,7 +182,7 @@ internal class SettingsWindow : Window
{
ImGui.SetTooltip(!ImGui.IsKeyDown(ImGuiKey.ModShift)
? Loc.Localize("DalamudSettingsSaveAndExit", "Save changes and close")
: Loc.Localize("DalamudSettingsSaveAndExit", "Save changes"));
: Loc.Localize("DalamudSettingsSave", "Save changes"));
}
}

View file

@ -133,7 +133,7 @@ public class SettingsTabLook : SettingsTab
new SettingsEntry<bool>(
Loc.Localize("DalamudSettingReducedMotion", "Reduce motions"),
Loc.Localize("DalamudSettingReducedMotion", "This will suppress certain animations from Dalamud, such as the notification popup."),
Loc.Localize("DalamudSettingReducedMotionHint", "This will suppress certain animations from Dalamud, such as the notification popup."),
c => c.ReduceMotions ?? false,
(v, c) => c.ReduceMotions = v),
};

View file

@ -0,0 +1,361 @@
using System.Buffers;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Unicode;
using Dalamud.Interface.ManagedFontAtlas.Internals;
namespace Dalamud.Interface.ManagedFontAtlas;
/// <summary>A fluent ImGui glyph range builder.</summary>
[SuppressMessage(
"StyleCop.CSharp.SpacingRules",
"SA1010:Opening square brackets should be spaced correctly",
Justification = "No")]
public struct FluentGlyphRangeBuilder
{
private const int ImUnicodeCodepointMax = char.MaxValue;
private BitArray? characters;
/// <summary>Clears the builder.</summary>
/// <returns><c>this</c> for method chaining.</returns>
/// <remarks>A builder is in cleared state on first use.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public FluentGlyphRangeBuilder Clear()
{
this.characters?.SetAll(false);
return this;
}
/// <summary>Adds a single codepoint to the builder.</summary>
/// <param name="codepoint">The codepoint to add.</param>
/// <returns><c>this</c> for method chaining.</returns>
/// <remarks>Unsupported codepoints will be ignored.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public FluentGlyphRangeBuilder With(char codepoint) => this.With((int)codepoint);
/// <inheritdoc cref="With(char)"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public FluentGlyphRangeBuilder With(uint codepoint) =>
codepoint <= char.MaxValue ? this.With((int)codepoint) : this;
/// <inheritdoc cref="With(char)"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public FluentGlyphRangeBuilder With(int codepoint)
{
if (codepoint <= ImUnicodeCodepointMax)
this.EnsureCharacters().Set(codepoint, true);
return this;
}
/// <summary>Adds a unicode range to the builder.</summary>
/// <param name="range">The unicode range to add.</param>
/// <returns><c>this</c> for method chaining.</returns>
/// <remarks>Unsupported codepoints will be ignored.</remarks>
public FluentGlyphRangeBuilder With(UnicodeRange range) =>
this.With(range.FirstCodePoint, (range.FirstCodePoint + range.Length) - 1);
/// <summary>Adds unicode ranges to the builder.</summary>
/// <param name="range1">The 1st unicode range to add.</param>
/// <param name="range2">The 2st unicode range to add.</param>
/// <returns><c>this</c> for method chaining.</returns>
/// <remarks>Unsupported codepoints will be ignored.</remarks>
public FluentGlyphRangeBuilder With(UnicodeRange range1, UnicodeRange range2) =>
this.With(range1.FirstCodePoint, (range1.FirstCodePoint + range1.Length) - 1)
.With(range2.FirstCodePoint, (range2.FirstCodePoint + range2.Length) - 1);
/// <summary>Adds unicode ranges to the builder.</summary>
/// <param name="range1">The 1st unicode range to add.</param>
/// <param name="range2">The 2st unicode range to add.</param>
/// <param name="range3">The 3rd unicode range to add.</param>
/// <returns><c>this</c> for method chaining.</returns>
/// <remarks>Unsupported codepoints will be ignored.</remarks>
public FluentGlyphRangeBuilder With(UnicodeRange range1, UnicodeRange range2, UnicodeRange range3) =>
this.With(range1.FirstCodePoint, (range1.FirstCodePoint + range1.Length) - 1)
.With(range2.FirstCodePoint, (range2.FirstCodePoint + range2.Length) - 1)
.With(range3.FirstCodePoint, (range3.FirstCodePoint + range3.Length) - 1);
/// <summary>Adds unicode ranges to the builder.</summary>
/// <param name="range1">The 1st unicode range to add.</param>
/// <param name="range2">The 2st unicode range to add.</param>
/// <param name="range3">The 3rd unicode range to add.</param>
/// <param name="range4">The 4th unicode range to add.</param>
/// <param name="evenMoreRanges">Even more unicode ranges to add.</param>
/// <returns><c>this</c> for method chaining.</returns>
/// <remarks>Unsupported codepoints will be ignored.</remarks>
public FluentGlyphRangeBuilder With(
UnicodeRange range1,
UnicodeRange range2,
UnicodeRange range3,
UnicodeRange range4,
params UnicodeRange[] evenMoreRanges) =>
this.With(range1.FirstCodePoint, (range1.FirstCodePoint + range1.Length) - 1)
.With(range2.FirstCodePoint, (range2.FirstCodePoint + range2.Length) - 1)
.With(range3.FirstCodePoint, (range3.FirstCodePoint + range3.Length) - 1)
.With(range4.FirstCodePoint, (range4.FirstCodePoint + range4.Length) - 1)
.With(evenMoreRanges);
/// <summary>Adds unicode ranges to the builder.</summary>
/// <param name="ranges">Unicode ranges to add.</param>
/// <returns><c>this</c> for method chaining.</returns>
/// <remarks>Unsupported codepoints will be ignored.</remarks>
public FluentGlyphRangeBuilder With(IEnumerable<UnicodeRange> ranges)
{
foreach (var range in ranges)
this.With(range);
return this;
}
/// <summary>Adds a range of characters to the builder.</summary>
/// <param name="from">The first codepoint, inclusive.</param>
/// <param name="to">The last codepoint, inclusive.</param>
/// <returns><c>this</c> for method chaining.</returns>
/// <remarks>
/// <para>Unsupported codepoints will be ignored.</para>
/// <para>If <paramref name="from"/> is more than <paramref name="to"/>, then they will be swapped.</para>
/// </remarks>
public FluentGlyphRangeBuilder With(char from, char to) =>
this.With(Math.Clamp(from, int.MinValue, int.MaxValue), Math.Clamp(to, int.MinValue, int.MaxValue));
/// <inheritdoc cref="With(char,char)"/>
public FluentGlyphRangeBuilder With(uint from, uint to) =>
this.With((int)Math.Min(from, int.MaxValue), (int)Math.Min(to, int.MaxValue));
/// <inheritdoc cref="With(char,char)"/>
public FluentGlyphRangeBuilder With(int from, int to)
{
from = Math.Clamp(from, 1, ImUnicodeCodepointMax);
to = Math.Clamp(to, 1, ImUnicodeCodepointMax);
if (from > to)
(from, to) = (to, from);
var bits = this.EnsureCharacters();
for (; from <= to; from++)
bits.Set(from, true);
return this;
}
/// <summary>Adds characters from a UTF-8 character sequence.</summary>
/// <param name="utf8Sequence">The sequence.</param>
/// <returns><c>this</c> for method chaining.</returns>
/// <remarks>Unsupported codepoints will be ignored.</remarks>
public FluentGlyphRangeBuilder With(ReadOnlySpan<byte> utf8Sequence)
{
var bits = this.EnsureCharacters();
while (!utf8Sequence.IsEmpty)
{
if (Rune.DecodeFromUtf8(utf8Sequence, out var rune, out var len) == OperationStatus.Done
&& rune.Value < ImUnicodeCodepointMax)
bits.Set(rune.Value, true);
utf8Sequence = utf8Sequence[len..];
}
return this;
}
/// <summary>Adds characters from a UTF-8 character sequence.</summary>
/// <param name="utf8Sequence">The sequence.</param>
/// <returns><c>this</c> for method chaining.</returns>
/// <remarks>Unsupported codepoints will be ignored.</remarks>
public FluentGlyphRangeBuilder With(IEnumerable<byte> utf8Sequence)
{
Span<byte> buf = stackalloc byte[4];
var bufp = 0;
var bits = this.EnsureCharacters();
foreach (var b in utf8Sequence)
{
buf[bufp++] = b;
while (Rune.DecodeFromUtf8(buf[..bufp], out var rune, out var len) is var state
&& state != OperationStatus.NeedMoreData)
{
switch (state)
{
case OperationStatus.Done when rune.Value <= ImUnicodeCodepointMax:
bits.Set(rune.Value, true);
goto case OperationStatus.InvalidData;
case OperationStatus.InvalidData:
bufp -= len;
break;
case OperationStatus.NeedMoreData:
case OperationStatus.DestinationTooSmall:
default:
throw new InvalidOperationException($"Unexpected return from {Rune.DecodeFromUtf8}.");
}
}
}
return this;
}
/// <summary>Adds characters from a UTF-16 character sequence.</summary>
/// <param name="utf16Sequence">The sequence.</param>
/// <returns><c>this</c> for method chaining.</returns>
/// <remarks>Unsupported codepoints will be ignored.</remarks>
public FluentGlyphRangeBuilder With(ReadOnlySpan<char> utf16Sequence)
{
var bits = this.EnsureCharacters();
while (!utf16Sequence.IsEmpty)
{
if (Rune.DecodeFromUtf16(utf16Sequence, out var rune, out var len) == OperationStatus.Done
&& rune.Value <= ImUnicodeCodepointMax)
bits.Set(rune.Value, true);
utf16Sequence = utf16Sequence[len..];
}
return this;
}
/// <summary>Adds characters from a UTF-16 character sequence.</summary>
/// <param name="utf16Sequence">The sequence.</param>
/// <returns><c>this</c> for method chaining.</returns>
/// <remarks>Unsupported codepoints will be ignored.</remarks>
public FluentGlyphRangeBuilder With(IEnumerable<char> utf16Sequence)
{
var bits = this.EnsureCharacters();
foreach (var c in utf16Sequence)
{
if (!char.IsSurrogate(c))
bits.Set(c, true);
}
return this;
}
/// <summary>Adds characters from a string.</summary>
/// <param name="string">The string.</param>
/// <returns><c>this</c> for method chaining.</returns>
/// <remarks>Unsupported codepoints will be ignored.</remarks>
public FluentGlyphRangeBuilder With(string @string) => this.With(@string.AsSpan());
/// <summary>Adds glyphs that are likely to be used in the given culture to the builder.</summary>
/// <param name="cultureInfo">A culture info.</param>
/// <returns><c>this</c> for method chaining.</returns>
/// <remarks>Unsupported codepoints will be ignored. Unsupported culture will do nothing.
/// Do make a PR if you need more.</remarks>
public FluentGlyphRangeBuilder WithLanguage(CultureInfo cultureInfo)
{
// Call in chunks of three to avoid allocating arrays.
// Avoid adding ranges that goes over BMP; that is, ranges that goes over ImUnicodeCodepointMax.
switch (cultureInfo.TwoLetterISOLanguageName)
{
case "ja":
// http://www.rikai.com/library/kanjitables/kanji_codes.unicode.shtml
return
this
.With(
UnicodeRanges.CjkSymbolsandPunctuation,
UnicodeRanges.Hiragana,
UnicodeRanges.Katakana)
.With(
UnicodeRanges.HalfwidthandFullwidthForms,
UnicodeRanges.CjkUnifiedIdeographs,
UnicodeRanges.CjkUnifiedIdeographsExtensionA)
// Blame Japanese cell carriers for the below.
.With(
UnicodeRanges.EnclosedCjkLettersandMonths);
case "zh":
return
this
.With(
UnicodeRanges.CjkUnifiedIdeographs,
UnicodeRanges.CjkUnifiedIdeographsExtensionA);
case "ko":
return
this
.With(
UnicodeRanges.HangulJamo,
UnicodeRanges.HangulCompatibilityJamo,
UnicodeRanges.HangulSyllables)
.With(
UnicodeRanges.HangulJamoExtendedA,
UnicodeRanges.HangulJamoExtendedB);
default:
return this;
}
}
/// <summary>Adds glyphs that are likely to be used in the given culture to the builder.</summary>
/// <param name="languageTag">A language tag that will be used to locate the culture info.</param>
/// <returns><c>this</c> for method chaining.</returns>
/// <remarks>See <see cref="CultureInfo.GetCultureInfo(string)"/> documentation for supported language tags.
/// </remarks>
public FluentGlyphRangeBuilder WithLanguage(string languageTag) =>
this.WithLanguage(CultureInfo.GetCultureInfo(languageTag));
/// <summary>Builds the accumulated data into an ImGui glyph range.</summary>
/// <param name="addFallbackCodepoints">Whether to add the default fallback codepoints to the range.</param>
/// <param name="addEllipsisCodepoints">Whether to add the default ellipsis codepoints to the range.</param>
/// <returns>The built ImGui glyph ranges.</returns>
public ushort[] Build(bool addFallbackCodepoints = true, bool addEllipsisCodepoints = true)
{
if (addFallbackCodepoints)
this.With(FontAtlasFactory.FallbackCodepoints);
if (addEllipsisCodepoints)
this.With(FontAtlasFactory.EllipsisCodepoints).With('.');
return this.BuildExact();
}
/// <summary>Builds the accumulated data into an ImGui glyph range, exactly as specified.</summary>
/// <returns>The built ImGui glyph ranges.</returns>
public ushort[] BuildExact()
{
if (this.characters is null)
return [0];
var bits = this.characters;
// Count the number of ranges first.
var numRanges = 0;
var lastCodepoint = -1;
for (var i = 1; i <= ImUnicodeCodepointMax; i++)
{
if (bits.Get(i))
{
if (lastCodepoint == -1)
lastCodepoint = i;
}
else
{
if (lastCodepoint != -1)
{
numRanges++;
lastCodepoint = -1;
}
}
}
// Handle the final range that terminates on the ending boundary.
if (lastCodepoint != -1)
numRanges++;
// Allocate the array and build the range.
var res = GC.AllocateUninitializedArray<ushort>((numRanges * 2) + 1);
var resp = 0;
for (var i = 1; i <= ImUnicodeCodepointMax; i++)
{
if (bits.Get(i) == ((resp & 1) == 0))
res[resp++] = unchecked((ushort)i);
}
// Handle the final range that terminates on the ending boundary.
if ((resp & 1) == 1)
res[resp++] = ImUnicodeCodepointMax;
// Add the zero terminator.
res[resp] = 0;
return res;
}
/// <summary>Ensures that <see cref="characters"/> is not null, by creating one as necessary.</summary>
/// <returns>An instance of <see cref="BitArray"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private BitArray EnsureCharacters() => this.characters ??= new(ImUnicodeCodepointMax + 1);
}

View file

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text.Unicode;
using Dalamud.Interface.Utility;
@ -12,6 +13,34 @@ namespace Dalamud.Interface.ManagedFontAtlas;
/// </summary>
public static class FontAtlasBuildToolkitUtilities
{
/// <summary>Begins building a new array of <see cref="ushort"/> containing ImGui glyph ranges.</summary>
/// <param name="chars">The chars.</param>
/// <returns>A new range builder.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static FluentGlyphRangeBuilder BeginGlyphRange(this IEnumerable<char> chars) =>
default(FluentGlyphRangeBuilder).With(chars);
/// <summary>Begins building a new array of <see cref="ushort"/> containing ImGui glyph ranges.</summary>
/// <param name="chars">The chars.</param>
/// <returns>A new range builder.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static FluentGlyphRangeBuilder BeginGlyphRange(this ReadOnlySpan<char> chars) =>
default(FluentGlyphRangeBuilder).With(chars);
/// <summary>Begins building a new array of <see cref="ushort"/> containing ImGui glyph ranges.</summary>
/// <param name="chars">The chars.</param>
/// <returns>A new range builder.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static FluentGlyphRangeBuilder BeginGlyphRange(this string chars) =>
default(FluentGlyphRangeBuilder).With(chars);
/// <summary>Begins building a new array of <see cref="ushort"/> containing ImGui glyph ranges.</summary>
/// <param name="range">The unicode range.</param>
/// <returns>A new range builder.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static FluentGlyphRangeBuilder BeginGlyphRange(this UnicodeRange range) =>
default(FluentGlyphRangeBuilder).With(range);
/// <summary>
/// Compiles given <see cref="char"/>s into an array of <see cref="ushort"/> containing ImGui glyph ranges.
/// </summary>
@ -19,16 +48,12 @@ public static class FontAtlasBuildToolkitUtilities
/// <param name="addFallbackCodepoints">Add fallback codepoints to the range.</param>
/// <param name="addEllipsisCodepoints">Add ellipsis codepoints to the range.</param>
/// <returns>The compiled range.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ushort[] ToGlyphRange(
this IEnumerable<char> enumerable,
bool addFallbackCodepoints = true,
bool addEllipsisCodepoints = true)
{
using var builderScoped = ImGuiHelpers.NewFontGlyphRangeBuilderPtrScoped(out var builder);
foreach (var c in enumerable)
builder.AddChar(c);
return builder.BuildRangesToArray(addFallbackCodepoints, addEllipsisCodepoints);
}
bool addEllipsisCodepoints = true) =>
enumerable.BeginGlyphRange().Build(addFallbackCodepoints, addEllipsisCodepoints);
/// <summary>
/// Compiles given <see cref="char"/>s into an array of <see cref="ushort"/> containing ImGui glyph ranges.
@ -37,16 +62,12 @@ public static class FontAtlasBuildToolkitUtilities
/// <param name="addFallbackCodepoints">Add fallback codepoints to the range.</param>
/// <param name="addEllipsisCodepoints">Add ellipsis codepoints to the range.</param>
/// <returns>The compiled range.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ushort[] ToGlyphRange(
this ReadOnlySpan<char> span,
bool addFallbackCodepoints = true,
bool addEllipsisCodepoints = true)
{
using var builderScoped = ImGuiHelpers.NewFontGlyphRangeBuilderPtrScoped(out var builder);
foreach (var c in span)
builder.AddChar(c);
return builder.BuildRangesToArray(addFallbackCodepoints, addEllipsisCodepoints);
}
bool addEllipsisCodepoints = true) =>
span.BeginGlyphRange().Build(addFallbackCodepoints, addEllipsisCodepoints);
/// <summary>
/// Compiles given string into an array of <see cref="ushort"/> containing ImGui glyph ranges.
@ -55,11 +76,12 @@ public static class FontAtlasBuildToolkitUtilities
/// <param name="addFallbackCodepoints">Add fallback codepoints to the range.</param>
/// <param name="addEllipsisCodepoints">Add ellipsis codepoints to the range.</param>
/// <returns>The compiled range.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ushort[] ToGlyphRange(
this string @string,
bool addFallbackCodepoints = true,
bool addEllipsisCodepoints = true) =>
@string.AsSpan().ToGlyphRange(addFallbackCodepoints, addEllipsisCodepoints);
@string.BeginGlyphRange().Build(addFallbackCodepoints, addEllipsisCodepoints);
/// <summary>
/// Finds the corresponding <see cref="ImFontConfigPtr"/> in

View file

@ -1,4 +1,5 @@
using System.IO;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using Dalamud.Interface.FontIdentifier;
@ -8,6 +9,8 @@ using Dalamud.Utility;
using ImGuiNET;
using TerraFX.Interop.DirectX;
namespace Dalamud.Interface.ManagedFontAtlas;
/// <summary>
@ -216,10 +219,35 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit
/// <returns>The added font.</returns>
ImFontPtr AddGameGlyphs(GameFontStyle gameFontStyle, ushort[]? glyphRanges, ImFontPtr mergeFont);
/// <summary>Adds glyphs from the Windows default font for the given culture info into the provided font.</summary>
/// <param name="cultureInfo">The culture info.</param>
/// <param name="fontConfig">The font config. If <see cref="SafeFontConfig.MergeFont"/> is not set, then
/// <see cref="IFontAtlasBuildToolkit.Font"/> will be used as the target. If that is empty too, then it will do
/// nothing.</param>
/// <param name="weight">The font weight, in range from <c>1</c> to <c>1000</c>. <c>400</c> is regular(normal).
/// </param>
/// <param name="stretch">The font stretch, in range from <c>1</c> to <c>9</c>. <c>5</c> is medium(normal).
/// </param>
/// <param name="style">The font style, in range from <c>0</c> to <c>2</c>. <c>0</c> is normal.</param>
/// <remarks>
/// <para>May do nothing at all if <paramref name="cultureInfo"/> is unsupported by Dalamud font handler.</para>
/// <para>See
/// <a href="https://learn.microsoft.com/en-us/windows/apps/design/globalizing/loc-international-fonts">Microsoft
/// Learn</a> for the fonts.</para>
/// </remarks>
void AttachWindowsDefaultFont(
CultureInfo cultureInfo,
in SafeFontConfig fontConfig,
int weight = (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL,
int stretch = (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL,
int style = (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL);
/// <summary>
/// Adds glyphs of extra languages into the provided font, depending on Dalamud Configuration.<br />
/// <see cref="SafeFontConfig.GlyphRanges"/> will be ignored.
/// </summary>
/// <param name="fontConfig">The font config.</param>
/// <param name="fontConfig">The font config. If <see cref="SafeFontConfig.MergeFont"/> is not set, then
/// <see cref="IFontAtlasBuildToolkit.Font"/> will be used as the target. If that is empty too, then it will do
/// nothing.</param>
void AttachExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig);
}

View file

@ -1,9 +1,9 @@
using System.Buffers;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.Unicode;
using Dalamud.Configuration.Internal;
using Dalamud.Interface.FontIdentifier;
@ -17,6 +17,8 @@ using ImGuiNET;
using SharpDX.DXGI;
using TerraFX.Interop.DirectX;
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
/// <summary>
@ -433,9 +435,163 @@ internal sealed partial class FontAtlasFactory
public ImFontPtr AddGameGlyphs(GameFontStyle gameFontStyle, ushort[]? glyphRanges, ImFontPtr mergeFont) =>
this.gameFontHandleSubstance.AttachGameGlyphs(this, mergeFont, gameFontStyle, glyphRanges);
/// <inheritdoc/>
public void AttachWindowsDefaultFont(
CultureInfo cultureInfo,
in SafeFontConfig fontConfig,
int weight = (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL,
int stretch = (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL,
int style = (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL)
{
var targetFont = fontConfig.MergeFont;
if (targetFont.IsNull())
targetFont = this.Font;
if (targetFont.IsNull())
return;
// https://learn.microsoft.com/en-us/windows/apps/design/globalizing/loc-international-fonts
var splitTag = cultureInfo.IetfLanguageTag.Split("-");
foreach (var test in new[]
{
cultureInfo.IetfLanguageTag,
$"{splitTag[0]}-{splitTag[^1]}",
})
{
var familyName = test switch
{
"af-ZA" => "Segoe UI",
"am-ET" => "Ebrima",
"ar-SA" => "Segoe UI",
"as-IN" => "Nirmala UI",
"az-Latn-AZ" => "Segoe UI",
"be-BY" => "Segoe UI",
"bg-BG" => "Segoe UI",
"bn-BD" => "Nirmala UI",
"bn-IN" => "Nirmala UI",
"bs-Latn-BA" => "Segoe UI",
"ca-ES" => "Segoe UI",
"ca-ES-valencia" => "Segoe UI",
"chr-CHER-US" => "Gadugi",
"cs-CZ" => "Segoe UI",
"cy-GB" => "Segoe UI",
"da-DK" => "Segoe UI",
"de-DE" => "Segoe UI",
"el-GR" => "Segoe UI",
"en-GB" => "Segoe UI",
"es-ES" => "Segoe UI",
"et-EE" => "Segoe UI",
"eu-ES" => "Segoe UI",
"fa-IR" => "Segoe UI",
"fi-FI" => "Segoe UI",
"fil-PH" => "Segoe UI",
"fr-FR" => "Segoe UI",
"ga-IE" => "Segoe UI",
"gd-GB" => "Segoe UI",
"gl-ES" => "Segoe UI",
"gu-IN" => "Nirmala UI",
"ha-Latn-NG" => "Segoe UI",
"he-IL" => "Segoe UI",
"hi-IN" => "Nirmala UI",
"hr-HR" => "Segoe UI",
"hu-HU" => "Segoe UI",
"hy-AM" => "Segoe UI",
"id-ID" => "Segoe UI",
"ig-NG" => "Segoe UI",
"is-IS" => "Segoe UI",
"it-IT" => "Segoe UI",
"ja-JP" => "Yu Gothic UI",
"ka-GE" => "Segoe UI",
"kk-KZ" => "Segoe UI",
"km-KH" => "Leelawadee UI",
"kn-IN" => "Nirmala UI",
"ko-KR" => "Malgun Gothic",
"kok-IN" => "Nirmala UI",
"ku-ARAB-IQ" => "Segoe UI",
"ky-KG" => "Segoe UI",
"lb-LU" => "Segoe UI",
"lt-LT" => "Segoe UI",
"lv-LV" => "Segoe UI",
"mi-NZ" => "Segoe UI",
"mk-MK" => "Segoe UI",
"ml-IN" => "Nirmala UI",
"mn-MN" => "Segoe UI",
"mr-IN" => "Nirmala UI",
"ms-MY" => "Segoe UI",
"mt-MT" => "Segoe UI",
"nb-NO" => "Segoe UI",
"ne-NP" => "Nirmala UI",
"nl-NL" => "Segoe UI",
"nn-NO" => "Segoe UI",
"nso-ZA" => "Segoe UI",
"or-IN" => "Nirmala UI",
"pa-Arab-PK" => "Segoe UI",
"pa-IN" => "Nirmala UI",
"pl-PL" => "Segoe UI",
"prs-AF" => "Segoe UI",
"pt-BR" => "Segoe UI",
"pt-PT" => "Segoe UI",
"qut-GT" => "Segoe UI",
"quz-PE" => "Segoe UI",
"ro-RO" => "Segoe UI",
"ru-RU" => "Segoe UI",
"rw-RW" => "Segoe UI",
"sd-Arab-PK" => "Segoe UI",
"si-LK" => "Nirmala UI",
"sk-SK" => "Segoe UI",
"sl-SI" => "Segoe UI",
"sq-AL" => "Segoe UI",
"sr-Cyrl-BA" => "Segoe UI",
"sr-Cyrl-CS" => "Segoe UI",
"sr-Latn-CS" => "Segoe UI",
"sv-SE" => "Segoe UI",
"sw-KE" => "Segoe UI",
"ta-IN" => "Nirmala UI",
"te-IN" => "Nirmala UI",
"tg-Cyrl-TJ" => "Segoe UI",
"th-TH" => "Leelawadee UI",
"ti-ET" => "Ebrima",
"tk-TM" => "Segoe UI",
"tn-ZA" => "Segoe UI",
"tr-TR" => "Segoe UI",
"tt-RU" => "Segoe UI",
"ug-CN" => "Segoe UI",
"uk-UA" => "Segoe UI",
"ur-PK" => "Segoe UI",
"uz-Latn-UZ" => "Segoe UI",
"vi-VN" => "Segoe UI",
"wo-SN" => "Segoe UI",
"xh-ZA" => "Segoe UI",
"yo-NG" => "Segoe UI",
"zh-CN" => "Microsoft YaHei UI",
"zh-HK" => "Microsoft JhengHei UI",
"zh-TW" => "Microsoft JhengHei UI",
"zh-Hans" => "Microsoft YaHei UI",
"zh-Hant" => "Microsoft YaHei UI",
"zu-ZA" => "Segoe UI",
_ => null,
};
if (familyName is null)
continue;
var family = IFontFamilyId
.ListSystemFonts(false)
.FirstOrDefault(
x => x.EnglishName.Equals(familyName, StringComparison.InvariantCultureIgnoreCase));
if (family?.Fonts[family.FindBestMatch(weight, stretch, style)] is not { } font)
return;
font.AddToBuildToolkit(this, fontConfig with { MergeFont = targetFont });
return;
}
}
/// <inheritdoc/>
public void AttachExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig)
{
var targetFont = fontConfig.MergeFont;
if (targetFont.IsNull())
targetFont = this.Font;
if (targetFont.IsNull())
return;
var dalamudConfiguration = Service<DalamudConfiguration>.Get();
if (dalamudConfiguration.EffectiveLanguage == "ko"
|| Service<DalamudIme>.GetNullable()?.EncounteredHangul is true)
@ -444,41 +600,24 @@ internal sealed partial class FontAtlasFactory
DalamudAsset.NotoSansKrRegular,
fontConfig with
{
GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom(
UnicodeRanges.HangulJamo,
UnicodeRanges.HangulCompatibilityJamo,
UnicodeRanges.HangulSyllables,
UnicodeRanges.HangulJamoExtendedA,
UnicodeRanges.HangulJamoExtendedB),
MergeFont = targetFont,
GlyphRanges = default(FluentGlyphRangeBuilder).WithLanguage("ko-kr").BuildExact(),
});
}
var windowsDir = Environment.GetFolderPath(Environment.SpecialFolder.Windows);
var fontPathChs = Path.Combine(windowsDir, "Fonts", "msyh.ttc");
if (!File.Exists(fontPathChs))
fontPathChs = null;
var fontPathCht = Path.Combine(windowsDir, "Fonts", "msjh.ttc");
if (!File.Exists(fontPathCht))
fontPathCht = null;
if (fontPathCht != null && Service<DalamudConfiguration>.Get().EffectiveLanguage == "tw")
if (Service<DalamudConfiguration>.Get().EffectiveLanguage == "tw")
{
this.AddFontFromFile(fontPathCht, fontConfig with
this.AttachWindowsDefaultFont(CultureInfo.GetCultureInfo("zh-hant"), fontConfig with
{
GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom(
UnicodeRanges.CjkUnifiedIdeographs,
UnicodeRanges.CjkUnifiedIdeographsExtensionA),
GlyphRanges = default(FluentGlyphRangeBuilder).WithLanguage("zh-hant").BuildExact(),
});
}
else if (fontPathChs != null && (Service<DalamudConfiguration>.Get().EffectiveLanguage == "zh"
|| Service<DalamudIme>.GetNullable()?.EncounteredHan is true))
else if (Service<DalamudConfiguration>.Get().EffectiveLanguage == "zh"
|| Service<DalamudIme>.GetNullable()?.EncounteredHan is true)
{
this.AddFontFromFile(fontPathChs, fontConfig with
this.AttachWindowsDefaultFont(CultureInfo.GetCultureInfo("zh-hans"), fontConfig with
{
GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom(
UnicodeRanges.CjkUnifiedIdeographs,
UnicodeRanges.CjkUnifiedIdeographsExtensionA),
GlyphRanges = default(FluentGlyphRangeBuilder).WithLanguage("zh-hans").BuildExact(),
});
}
}

View file

@ -347,6 +347,12 @@ public sealed class UiBuilder : IDisposable
/// </summary>
public IFontAtlas FontAtlas { get; }
/// <summary>
/// Gets a value indicating whether or not to use "reduced motion". This usually means that you should use less
/// intrusive animations, or disable them entirely.
/// </summary>
public bool ShouldUseReducedMotion => Service<DalamudConfiguration>.Get().ReduceMotions ?? false;
/// <summary>
/// Gets or sets a value indicating whether statistics about UI draw time should be collected.
/// </summary>

View file

@ -0,0 +1,194 @@
using System.Collections.Generic;
using System.Linq;
using Dalamud.Game.Command;
using Dalamud.Plugin.Internal.Types;
namespace Dalamud.Plugin.Internal;
/// <summary>
/// Class responsible for validating a dev plugin.
/// </summary>
internal static class PluginValidator
{
private static readonly char[] LineSeparator = new[] { ' ', '\n', '\r' };
/// <summary>
/// Represents the severity of a validation problem.
/// </summary>
public enum ValidationSeverity
{
/// <summary>
/// The problem is informational.
/// </summary>
Information,
/// <summary>
/// The problem is a warning.
/// </summary>
Warning,
/// <summary>
/// The problem is fatal.
/// </summary>
Fatal,
}
/// <summary>
/// Represents a validation problem.
/// </summary>
public interface IValidationProblem
{
/// <summary>
/// Gets the severity of the validation.
/// </summary>
public ValidationSeverity Severity { get; }
/// <summary>
/// Compute the localized description of the problem.
/// </summary>
/// <returns>Localized string to be shown to the developer.</returns>
public string GetLocalizedDescription();
}
/// <summary>
/// Check for problems in a plugin.
/// </summary>
/// <param name="plugin">The plugin to validate.</param>
/// <returns>An list of problems.</returns>
/// <exception cref="InvalidOperationException">Thrown when the plugin is not loaded. A plugin must be loaded to validate it.</exception>
public static IReadOnlyList<IValidationProblem> CheckForProblems(LocalDevPlugin plugin)
{
var problems = new List<IValidationProblem>();
if (!plugin.IsLoaded)
throw new InvalidOperationException("Plugin must be loaded to validate.");
if (!plugin.DalamudInterface!.UiBuilder.HasConfigUi)
problems.Add(new NoConfigUiProblem());
if (!plugin.DalamudInterface.UiBuilder.HasMainUi)
problems.Add(new NoMainUiProblem());
var cmdManager = Service<CommandManager>.Get();
foreach (var cmd in cmdManager.Commands.Where(x => x.Value.LoaderAssemblyName == plugin.InternalName && x.Value.ShowInHelp))
{
if (string.IsNullOrEmpty(cmd.Value.HelpMessage))
problems.Add(new CommandWithoutHelpTextProblem(cmd.Key));
}
if (plugin.Manifest.Tags == null || plugin.Manifest.Tags.Count == 0)
problems.Add(new NoTagsProblem());
if (string.IsNullOrEmpty(plugin.Manifest.Description) || plugin.Manifest.Description.Split(LineSeparator, StringSplitOptions.RemoveEmptyEntries).Length <= 1)
problems.Add(new NoDescriptionProblem());
if (string.IsNullOrEmpty(plugin.Manifest.Punchline))
problems.Add(new NoPunchlineProblem());
if (string.IsNullOrEmpty(plugin.Manifest.Name))
problems.Add(new NoNameProblem());
if (string.IsNullOrEmpty(plugin.Manifest.Author))
problems.Add(new NoAuthorProblem());
return problems;
}
/// <summary>
/// Representing a problem where the plugin does not have a config UI callback.
/// </summary>
public class NoConfigUiProblem : IValidationProblem
{
/// <inheritdoc/>
public ValidationSeverity Severity => ValidationSeverity.Warning;
/// <inheritdoc/>
public string GetLocalizedDescription() => "The plugin does not register a config UI callback. If you have a settings window or section, please consider registering UiBuilder.OpenConfigUi to open it.";
}
/// <summary>
/// Representing a problem where the plugin does not have a main UI callback.
/// </summary>
public class NoMainUiProblem : IValidationProblem
{
/// <inheritdoc/>
public ValidationSeverity Severity => ValidationSeverity.Warning;
/// <inheritdoc/>
public string GetLocalizedDescription() => "The plugin does not register a main UI callback. If your plugin has a window that could be considered the main entrypoint to its features, please consider registering UiBuilder.OpenMainUi to open the plugin's main window.";
}
/// <summary>
/// Representing a problem where a command does not have a help text.
/// </summary>
/// <param name="commandName">Name of the command.</param>
public class CommandWithoutHelpTextProblem(string commandName) : IValidationProblem
{
/// <inheritdoc/>
public ValidationSeverity Severity => ValidationSeverity.Fatal;
/// <inheritdoc/>
public string GetLocalizedDescription() => $"The plugin has a command ({commandName}) without a help message. Please consider adding a help message to the command when registering it.";
}
/// <summary>
/// Representing a problem where a plugin does not have any tags in its manifest.
/// </summary>
public class NoTagsProblem : IValidationProblem
{
/// <inheritdoc/>
public ValidationSeverity Severity => ValidationSeverity.Information;
/// <inheritdoc/>
public string GetLocalizedDescription() => "Your plugin does not have any tags in its manifest. Please consider adding some to make it easier for users to find your plugin in the installer.";
}
/// <summary>
/// Representing a problem where a plugin does not have a description in its manifest.
/// </summary>
public class NoDescriptionProblem : IValidationProblem
{
/// <inheritdoc/>
public ValidationSeverity Severity => ValidationSeverity.Information;
/// <inheritdoc/>
public string GetLocalizedDescription() => "Your plugin does not have a description in its manifest, or it is very terse. Please consider adding one to give users more information about your plugin.";
}
/// <summary>
/// Representing a problem where a plugin has no punchline in its manifest.
/// </summary>
public class NoPunchlineProblem : IValidationProblem
{
/// <inheritdoc/>
public ValidationSeverity Severity => ValidationSeverity.Information;
/// <inheritdoc/>
public string GetLocalizedDescription() => "Your plugin does not have a punchline in its manifest. Please consider adding one to give users a quick overview of what your plugin does.";
}
/// <summary>
/// Representing a problem where a plugin has no name in its manifest.
/// </summary>
public class NoNameProblem : IValidationProblem
{
/// <inheritdoc/>
public ValidationSeverity Severity => ValidationSeverity.Fatal;
/// <inheritdoc/>
public string GetLocalizedDescription() => "Your plugin does not have a name in its manifest.";
}
/// <summary>
/// Representing a problem where a plugin has no author in its manifest.
/// </summary>
public class NoAuthorProblem : IValidationProblem
{
/// <inheritdoc/>
public ValidationSeverity Severity => ValidationSeverity.Fatal;
/// <inheritdoc/>
public string GetLocalizedDescription() => "Your plugin does not have an author in its manifest.";
}
}

View file

@ -18,6 +18,16 @@ namespace Dalamud.Plugin.Internal.Profiles;
[ServiceManager.EarlyLoadedService]
internal class ProfileCommandHandler : IInternalDisposableService
{
#pragma warning disable SA1600
public const string CommandEnable = "/xlenablecollection";
public const string CommandDisable = "/xldisablecollection";
public const string CommandToggle = "/xltogglecollection";
#pragma warning restore SA1600
private static readonly string LegacyCommandEnable = CommandEnable.Replace("collection", "profile");
private static readonly string LegacyCommandDisable = CommandDisable.Replace("collection", "profile");
private static readonly string LegacyCommandToggle = CommandToggle.Replace("collection", "profile");
private readonly CommandManager cmd;
private readonly ProfileManager profileManager;
private readonly ChatGui chat;
@ -40,23 +50,38 @@ internal class ProfileCommandHandler : IInternalDisposableService
this.chat = chat;
this.framework = framework;
this.cmd.AddHandler("/xlenableprofile", new CommandInfo(this.OnEnableProfile)
this.cmd.AddHandler(CommandEnable, new CommandInfo(this.OnEnableProfile)
{
HelpMessage = Loc.Localize("ProfileCommandsEnableHint", "Enable a collection. Usage: /xlenablecollection \"Collection Name\""),
ShowInHelp = true,
});
this.cmd.AddHandler("/xldisableprofile", new CommandInfo(this.OnDisableProfile)
this.cmd.AddHandler(CommandDisable, new CommandInfo(this.OnDisableProfile)
{
HelpMessage = Loc.Localize("ProfileCommandsDisableHint", "Disable a collection. Usage: /xldisablecollection \"Collection Name\""),
ShowInHelp = true,
});
this.cmd.AddHandler("/xltoggleprofile", new CommandInfo(this.OnToggleProfile)
this.cmd.AddHandler(CommandToggle, new CommandInfo(this.OnToggleProfile)
{
HelpMessage = Loc.Localize("ProfileCommandsToggleHint", "Toggle a collection. Usage: /xltogglecollection \"Collection Name\""),
ShowInHelp = true,
});
this.cmd.AddHandler(LegacyCommandEnable, new CommandInfo(this.OnEnableProfile)
{
ShowInHelp = false,
});
this.cmd.AddHandler(LegacyCommandDisable, new CommandInfo(this.OnDisableProfile)
{
ShowInHelp = true,
});
this.cmd.AddHandler(LegacyCommandToggle, new CommandInfo(this.OnToggleProfile)
{
ShowInHelp = true,
});
this.framework.Update += this.FrameworkOnUpdate;
}
@ -71,9 +96,12 @@ internal class ProfileCommandHandler : IInternalDisposableService
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.cmd.RemoveHandler("/xlenablecollection");
this.cmd.RemoveHandler("/xldisablecollection");
this.cmd.RemoveHandler("/xltogglecollection");
this.cmd.RemoveHandler(CommandEnable);
this.cmd.RemoveHandler(CommandDisable);
this.cmd.RemoveHandler(CommandToggle);
this.cmd.RemoveHandler(LegacyCommandEnable);
this.cmd.RemoveHandler(LegacyCommandDisable);
this.cmd.RemoveHandler(LegacyCommandToggle);
this.framework.Update += this.FrameworkOnUpdate;
}

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
@ -99,6 +100,11 @@ internal class LocalDevPlugin : LocalPlugin, IDisposable
/// Gets an ID uniquely identifying this specific instance of a devPlugin.
/// </summary>
public Guid DevImposedWorkingPluginId => this.devSettings.WorkingPluginId;
/// <summary>
/// Gets a list of validation problems that have been dismissed by the user.
/// </summary>
public List<string> DismissedValidationProblems => this.devSettings.DismissedValidationProblems;
/// <inheritdoc/>
public new void Dispose()

@ -1 +1 @@
Subproject commit 4bfbbe1c9eba5e0f70aa7d119ef6816bd9925988
Subproject commit 76f15950d210d5196d942cc8439c2e48bfffd0ee

View file

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<PlatformTarget>x64</PlatformTarget>
<Platforms>x64;AnyCPU</Platforms>
<LangVersion>12.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Dalamud\Dalamud.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CheapLoc" Version="1.1.8" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,10 @@
// See https://aka.ms/new-console-template for more information
using CheapLoc;
Console.WriteLine("=> Starting loc export...");
var dalamud = typeof(Dalamud.Localization).Assembly;
Loc.ExportLocalizableForAssembly(dalamud, true);
Console.WriteLine("=> Finished loc export!");