diff --git a/Dalamud.sln b/Dalamud.sln index 93089b9a6..2d22932dd 100644 --- a/Dalamud.sln +++ b/Dalamud.sln @@ -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 diff --git a/Dalamud/Configuration/Internal/DevPluginSettings.cs b/Dalamud/Configuration/Internal/DevPluginSettings.cs index cfe8ba411..63d56fdb6 100644 --- a/Dalamud/Configuration/Internal/DevPluginSettings.cs +++ b/Dalamud/Configuration/Internal/DevPluginSettings.cs @@ -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. /// public Guid WorkingPluginId { get; set; } = Guid.Empty; + + /// + /// Gets or sets a list of validation problems that have been dismissed by the user. + /// + public List DismissedValidationProblems { get; set; } = new(); } diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 6b0ca1c0c..f80962a9d 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.1.0.2 + 9.1.0.5 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) diff --git a/Dalamud/Interface/Fools24.cs b/Dalamud/Interface/Fools24.cs new file mode 100644 index 000000000..9fa3eb7b9 --- /dev/null +++ b/Dalamud/Interface/Fools24.cs @@ -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.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; + } + } +} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs index 18bb57118..7811c1aaa 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs @@ -56,7 +56,7 @@ internal static class NotificationConstants public const float ProgressWaveLoopMaxColorTimeRatio = 0.7f; /// Default duration of the notification. - public static readonly TimeSpan DefaultDuration = TimeSpan.FromSeconds(3); + public static readonly TimeSpan DefaultDuration = TimeSpan.FromSeconds(7); /// Duration of show animation. public static readonly TimeSpan ShowAnimationDuration = TimeSpan.FromMilliseconds(300); diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index 64040011e..2b0da5aca 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -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 /// Undo range for modifying the buffer while composition is in progress. 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; } } diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index ec18fbb69..834bd9865 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -943,7 +943,7 @@ internal class DalamudInterface : IInternalDisposableService if (ImGui.MenuItem("Export localizable")) { - localization.ExportLocalizable(); + localization.ExportLocalizable(true); } if (ImGui.BeginMenu("Load language...")) diff --git a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs index 634999143..9e0ef4df7 100644 --- a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs +++ b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs @@ -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.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.Get(); + if (Fools24.IsDayApplicable()) + { + var iconLink = fools.GetHorseIconLink(manifest.InternalName); + if (iconLink != null) + return iconLink; + } return MainRepoDip17ImageUrl.Format(manifest.Dip17Channel!, manifest.InternalName, "icon.png"); } diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index ea49ef3ba..88d66eb84 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -268,6 +268,8 @@ internal class PluginInstallerWindow : Window, IDisposable /// public override void OnOpen() { + Service.Get().NotifyInstallerWindowOpened(); + var pluginManager = Service.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.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/"); diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index 9f3196928..6bee26755 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -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."); diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs index 47ba2c65f..4c67afce4 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs @@ -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")); } } diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs index c7fcdc58d..1c446f26b 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs @@ -133,7 +133,7 @@ public class SettingsTabLook : SettingsTab new SettingsEntry( 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), }; diff --git a/Dalamud/Interface/ManagedFontAtlas/FluentGlyphRangeBuilder.cs b/Dalamud/Interface/ManagedFontAtlas/FluentGlyphRangeBuilder.cs new file mode 100644 index 000000000..ec395a61f --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/FluentGlyphRangeBuilder.cs @@ -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; + +/// A fluent ImGui glyph range builder. +[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; + + /// Clears the builder. + /// this for method chaining. + /// A builder is in cleared state on first use. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public FluentGlyphRangeBuilder Clear() + { + this.characters?.SetAll(false); + return this; + } + + /// Adds a single codepoint to the builder. + /// The codepoint to add. + /// this for method chaining. + /// Unsupported codepoints will be ignored. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public FluentGlyphRangeBuilder With(char codepoint) => this.With((int)codepoint); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public FluentGlyphRangeBuilder With(uint codepoint) => + codepoint <= char.MaxValue ? this.With((int)codepoint) : this; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public FluentGlyphRangeBuilder With(int codepoint) + { + if (codepoint <= ImUnicodeCodepointMax) + this.EnsureCharacters().Set(codepoint, true); + return this; + } + + /// Adds a unicode range to the builder. + /// The unicode range to add. + /// this for method chaining. + /// Unsupported codepoints will be ignored. + public FluentGlyphRangeBuilder With(UnicodeRange range) => + this.With(range.FirstCodePoint, (range.FirstCodePoint + range.Length) - 1); + + /// Adds unicode ranges to the builder. + /// The 1st unicode range to add. + /// The 2st unicode range to add. + /// this for method chaining. + /// Unsupported codepoints will be ignored. + public FluentGlyphRangeBuilder With(UnicodeRange range1, UnicodeRange range2) => + this.With(range1.FirstCodePoint, (range1.FirstCodePoint + range1.Length) - 1) + .With(range2.FirstCodePoint, (range2.FirstCodePoint + range2.Length) - 1); + + /// Adds unicode ranges to the builder. + /// The 1st unicode range to add. + /// The 2st unicode range to add. + /// The 3rd unicode range to add. + /// this for method chaining. + /// Unsupported codepoints will be ignored. + 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); + + /// Adds unicode ranges to the builder. + /// The 1st unicode range to add. + /// The 2st unicode range to add. + /// The 3rd unicode range to add. + /// The 4th unicode range to add. + /// Even more unicode ranges to add. + /// this for method chaining. + /// Unsupported codepoints will be ignored. + 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); + + /// Adds unicode ranges to the builder. + /// Unicode ranges to add. + /// this for method chaining. + /// Unsupported codepoints will be ignored. + public FluentGlyphRangeBuilder With(IEnumerable ranges) + { + foreach (var range in ranges) + this.With(range); + return this; + } + + /// Adds a range of characters to the builder. + /// The first codepoint, inclusive. + /// The last codepoint, inclusive. + /// this for method chaining. + /// + /// Unsupported codepoints will be ignored. + /// If is more than , then they will be swapped. + /// + public FluentGlyphRangeBuilder With(char from, char to) => + this.With(Math.Clamp(from, int.MinValue, int.MaxValue), Math.Clamp(to, int.MinValue, int.MaxValue)); + + /// + public FluentGlyphRangeBuilder With(uint from, uint to) => + this.With((int)Math.Min(from, int.MaxValue), (int)Math.Min(to, int.MaxValue)); + + /// + 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; + } + + /// Adds characters from a UTF-8 character sequence. + /// The sequence. + /// this for method chaining. + /// Unsupported codepoints will be ignored. + public FluentGlyphRangeBuilder With(ReadOnlySpan 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; + } + + /// Adds characters from a UTF-8 character sequence. + /// The sequence. + /// this for method chaining. + /// Unsupported codepoints will be ignored. + public FluentGlyphRangeBuilder With(IEnumerable utf8Sequence) + { + Span 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; + } + + /// Adds characters from a UTF-16 character sequence. + /// The sequence. + /// this for method chaining. + /// Unsupported codepoints will be ignored. + public FluentGlyphRangeBuilder With(ReadOnlySpan 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; + } + + /// Adds characters from a UTF-16 character sequence. + /// The sequence. + /// this for method chaining. + /// Unsupported codepoints will be ignored. + public FluentGlyphRangeBuilder With(IEnumerable utf16Sequence) + { + var bits = this.EnsureCharacters(); + foreach (var c in utf16Sequence) + { + if (!char.IsSurrogate(c)) + bits.Set(c, true); + } + + return this; + } + + /// Adds characters from a string. + /// The string. + /// this for method chaining. + /// Unsupported codepoints will be ignored. + public FluentGlyphRangeBuilder With(string @string) => this.With(@string.AsSpan()); + + /// Adds glyphs that are likely to be used in the given culture to the builder. + /// A culture info. + /// this for method chaining. + /// Unsupported codepoints will be ignored. Unsupported culture will do nothing. + /// Do make a PR if you need more. + 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; + } + } + + /// Adds glyphs that are likely to be used in the given culture to the builder. + /// A language tag that will be used to locate the culture info. + /// this for method chaining. + /// See documentation for supported language tags. + /// + public FluentGlyphRangeBuilder WithLanguage(string languageTag) => + this.WithLanguage(CultureInfo.GetCultureInfo(languageTag)); + + /// Builds the accumulated data into an ImGui glyph range. + /// Whether to add the default fallback codepoints to the range. + /// Whether to add the default ellipsis codepoints to the range. + /// The built ImGui glyph ranges. + 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(); + } + + /// Builds the accumulated data into an ImGui glyph range, exactly as specified. + /// The built ImGui glyph ranges. + 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((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; + } + + /// Ensures that is not null, by creating one as necessary. + /// An instance of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private BitArray EnsureCharacters() => this.characters ??= new(ImUnicodeCodepointMax + 1); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs index 4c3e9023a..3b8bfd965 100644 --- a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs +++ b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs @@ -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; /// public static class FontAtlasBuildToolkitUtilities { + /// Begins building a new array of containing ImGui glyph ranges. + /// The chars. + /// A new range builder. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static FluentGlyphRangeBuilder BeginGlyphRange(this IEnumerable chars) => + default(FluentGlyphRangeBuilder).With(chars); + + /// Begins building a new array of containing ImGui glyph ranges. + /// The chars. + /// A new range builder. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static FluentGlyphRangeBuilder BeginGlyphRange(this ReadOnlySpan chars) => + default(FluentGlyphRangeBuilder).With(chars); + + /// Begins building a new array of containing ImGui glyph ranges. + /// The chars. + /// A new range builder. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static FluentGlyphRangeBuilder BeginGlyphRange(this string chars) => + default(FluentGlyphRangeBuilder).With(chars); + + /// Begins building a new array of containing ImGui glyph ranges. + /// The unicode range. + /// A new range builder. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static FluentGlyphRangeBuilder BeginGlyphRange(this UnicodeRange range) => + default(FluentGlyphRangeBuilder).With(range); + /// /// Compiles given s into an array of containing ImGui glyph ranges. /// @@ -19,16 +48,12 @@ public static class FontAtlasBuildToolkitUtilities /// Add fallback codepoints to the range. /// Add ellipsis codepoints to the range. /// The compiled range. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ushort[] ToGlyphRange( this IEnumerable 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); /// /// Compiles given s into an array of containing ImGui glyph ranges. @@ -37,16 +62,12 @@ public static class FontAtlasBuildToolkitUtilities /// Add fallback codepoints to the range. /// Add ellipsis codepoints to the range. /// The compiled range. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ushort[] ToGlyphRange( this ReadOnlySpan 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); /// /// Compiles given string into an array of containing ImGui glyph ranges. @@ -55,11 +76,12 @@ public static class FontAtlasBuildToolkitUtilities /// Add fallback codepoints to the range. /// Add ellipsis codepoints to the range. /// The compiled range. + [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); /// /// Finds the corresponding in diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs index 9b80d27ff..b32e3db18 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs @@ -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; /// @@ -216,10 +219,35 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit /// The added font. ImFontPtr AddGameGlyphs(GameFontStyle gameFontStyle, ushort[]? glyphRanges, ImFontPtr mergeFont); + /// Adds glyphs from the Windows default font for the given culture info into the provided font. + /// The culture info. + /// The font config. If is not set, then + /// will be used as the target. If that is empty too, then it will do + /// nothing. + /// The font weight, in range from 1 to 1000. 400 is regular(normal). + /// + /// The font stretch, in range from 1 to 9. 5 is medium(normal). + /// + /// The font style, in range from 0 to 2. 0 is normal. + /// + /// May do nothing at all if is unsupported by Dalamud font handler. + /// See + /// Microsoft + /// Learn for the fonts. + /// + 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); + /// /// Adds glyphs of extra languages into the provided font, depending on Dalamud Configuration.
/// will be ignored. ///
- /// The font config. + /// The font config. If is not set, then + /// will be used as the target. If that is empty too, then it will do + /// nothing. void AttachExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig); } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index 55af20329..34d28ccbd 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -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; /// @@ -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); + /// + 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; + } + } + /// public void AttachExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig) { + var targetFont = fontConfig.MergeFont; + if (targetFont.IsNull()) + targetFont = this.Font; + if (targetFont.IsNull()) + return; + var dalamudConfiguration = Service.Get(); if (dalamudConfiguration.EffectiveLanguage == "ko" || Service.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.Get().EffectiveLanguage == "tw") + if (Service.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.Get().EffectiveLanguage == "zh" - || Service.GetNullable()?.EncounteredHan is true)) + else if (Service.Get().EffectiveLanguage == "zh" + || Service.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(), }); } } diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 2c2ca9725..b80fe0b82 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -347,6 +347,12 @@ public sealed class UiBuilder : IDisposable /// public IFontAtlas FontAtlas { get; } + /// + /// 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. + /// + public bool ShouldUseReducedMotion => Service.Get().ReduceMotions ?? false; + /// /// Gets or sets a value indicating whether statistics about UI draw time should be collected. /// diff --git a/Dalamud/Plugin/Internal/PluginValidator.cs b/Dalamud/Plugin/Internal/PluginValidator.cs new file mode 100644 index 000000000..134bb2a4c --- /dev/null +++ b/Dalamud/Plugin/Internal/PluginValidator.cs @@ -0,0 +1,194 @@ +using System.Collections.Generic; +using System.Linq; + +using Dalamud.Game.Command; +using Dalamud.Plugin.Internal.Types; + +namespace Dalamud.Plugin.Internal; + +/// +/// Class responsible for validating a dev plugin. +/// +internal static class PluginValidator +{ + private static readonly char[] LineSeparator = new[] { ' ', '\n', '\r' }; + + /// + /// Represents the severity of a validation problem. + /// + public enum ValidationSeverity + { + /// + /// The problem is informational. + /// + Information, + + /// + /// The problem is a warning. + /// + Warning, + + /// + /// The problem is fatal. + /// + Fatal, + } + + /// + /// Represents a validation problem. + /// + public interface IValidationProblem + { + /// + /// Gets the severity of the validation. + /// + public ValidationSeverity Severity { get; } + + /// + /// Compute the localized description of the problem. + /// + /// Localized string to be shown to the developer. + public string GetLocalizedDescription(); + } + + /// + /// Check for problems in a plugin. + /// + /// The plugin to validate. + /// An list of problems. + /// Thrown when the plugin is not loaded. A plugin must be loaded to validate it. + public static IReadOnlyList CheckForProblems(LocalDevPlugin plugin) + { + var problems = new List(); + + 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.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; + } + + /// + /// Representing a problem where the plugin does not have a config UI callback. + /// + public class NoConfigUiProblem : IValidationProblem + { + /// + public ValidationSeverity Severity => ValidationSeverity.Warning; + + /// + 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."; + } + + /// + /// Representing a problem where the plugin does not have a main UI callback. + /// + public class NoMainUiProblem : IValidationProblem + { + /// + public ValidationSeverity Severity => ValidationSeverity.Warning; + + /// + 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."; + } + + /// + /// Representing a problem where a command does not have a help text. + /// + /// Name of the command. + public class CommandWithoutHelpTextProblem(string commandName) : IValidationProblem + { + /// + public ValidationSeverity Severity => ValidationSeverity.Fatal; + + /// + 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."; + } + + /// + /// Representing a problem where a plugin does not have any tags in its manifest. + /// + public class NoTagsProblem : IValidationProblem + { + /// + public ValidationSeverity Severity => ValidationSeverity.Information; + + /// + 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."; + } + + /// + /// Representing a problem where a plugin does not have a description in its manifest. + /// + public class NoDescriptionProblem : IValidationProblem + { + /// + public ValidationSeverity Severity => ValidationSeverity.Information; + + /// + 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."; + } + + /// + /// Representing a problem where a plugin has no punchline in its manifest. + /// + public class NoPunchlineProblem : IValidationProblem + { + /// + public ValidationSeverity Severity => ValidationSeverity.Information; + + /// + 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."; + } + + /// + /// Representing a problem where a plugin has no name in its manifest. + /// + public class NoNameProblem : IValidationProblem + { + /// + public ValidationSeverity Severity => ValidationSeverity.Fatal; + + /// + public string GetLocalizedDescription() => "Your plugin does not have a name in its manifest."; + } + + /// + /// Representing a problem where a plugin has no author in its manifest. + /// + public class NoAuthorProblem : IValidationProblem + { + /// + public ValidationSeverity Severity => ValidationSeverity.Fatal; + + /// + public string GetLocalizedDescription() => "Your plugin does not have an author in its manifest."; + } +} diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs b/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs index eebb87aaa..7b7b4cfd0 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs @@ -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 /// 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; } diff --git a/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs index 1f9f503e0..9f7c761de 100644 --- a/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs @@ -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. ///
public Guid DevImposedWorkingPluginId => this.devSettings.WorkingPluginId; + + /// + /// Gets a list of validation problems that have been dismissed by the user. + /// + public List DismissedValidationProblems => this.devSettings.DismissedValidationProblems; /// public new void Dispose() diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 4bfbbe1c9..76f15950d 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 4bfbbe1c9eba5e0f70aa7d119ef6816bd9925988 +Subproject commit 76f15950d210d5196d942cc8439c2e48bfffd0ee diff --git a/tools/Dalamud.LocExporter/Dalamud.LocExporter.csproj b/tools/Dalamud.LocExporter/Dalamud.LocExporter.csproj new file mode 100644 index 000000000..5701e706f --- /dev/null +++ b/tools/Dalamud.LocExporter/Dalamud.LocExporter.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0-windows + x64 + x64;AnyCPU + 12.0 + enable + enable + + + + + + + + + + + diff --git a/tools/Dalamud.LocExporter/Program.cs b/tools/Dalamud.LocExporter/Program.cs new file mode 100644 index 000000000..f9fe8f6aa --- /dev/null +++ b/tools/Dalamud.LocExporter/Program.cs @@ -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!");