From c91e24cb5865aa2e6f019d2ed481c34cc7571970 Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 16 Jun 2024 18:21:40 +0200 Subject: [PATCH 01/12] pi: "update all plugins" button must update banned plugins --- .../Windows/PluginInstaller/PluginInstallerWindow.cs | 2 +- Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs | 6 +++--- Dalamud/Plugin/Internal/PluginManager.cs | 3 --- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 1033c9ea4..fe603b35f 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -726,7 +726,7 @@ internal class PluginInstallerWindow : Window, IDisposable this.loadingIndicatorKind = LoadingIndicatorKind.UpdatingAll; var toUpdate = this.pluginListUpdatable - .Where(x => x.InstalledPlugin.IsLoaded) + .Where(x => x.InstalledPlugin.IsWantedByAnyProfile) .ToList(); Task.Run(() => pluginManager.UpdatePluginsAsync(toUpdate, false)) diff --git a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs index 4e2179be8..107dd0979 100644 --- a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs +++ b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs @@ -274,10 +274,10 @@ internal class AutoUpdateManager : IServiceType }); var progress = new Progress(); - progress.ProgressChanged += (_, progress) => + progress.ProgressChanged += (_, updateProgress) => { - notification.Content = Locs.NotificationContentUpdating(progress.CurrentPluginManifest.Name); - notification.Progress = (float)progress.PluginsProcessed / progress.TotalPlugins; + notification.Content = Locs.NotificationContentUpdating(updateProgress.CurrentPluginManifest.Name); + notification.Progress = (float)updateProgress.PluginsProcessed / updateProgress.TotalPlugins; }; var pluginStates = await this.pluginManager.UpdatePluginsAsync(updatablePlugins, this.isDryRun.Value, true, progress); diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 60d2bbe28..64d3d8b93 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1003,9 +1003,6 @@ internal class PluginManager : IInternalDisposableService if (plugin.InstalledPlugin.IsDev) continue; - if (!plugin.InstalledPlugin.IsWantedByAnyProfile) - continue; - if (plugin.InstalledPlugin.Manifest.ScheduledForDeletion) continue; From 5f51ca22e0ae8e884b8324fe7add13dc2323f803 Mon Sep 17 00:00:00 2001 From: goat Date: Mon, 17 Jun 2024 19:23:55 +0200 Subject: [PATCH 02/12] autoupdate: consider notification dismissal time as last check time --- Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs index 107dd0979..09ab22959 100644 --- a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs +++ b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs @@ -228,7 +228,11 @@ internal class AutoUpdateManager : IServiceType throw new InvalidOperationException("Already showing a notification"); this.updateNotification = this.notificationManager.AddNotification(notification); - this.updateNotification.Dismiss += _ => this.updateNotification = null; + this.updateNotification.Dismiss += _ => + { + this.updateNotification = null; + this.lastUpdateCheckTime = DateTime.Now; + }; return this.updateNotification!; } @@ -444,7 +448,7 @@ internal class AutoUpdateManager : IServiceType => string.Format(Loc.Localize("AutoUpdateUpdatesAvailableContent", "There are {0} plugins that can be updated."), numUpdates); public static string NotificationContentUpdatesAvailableMinimized(int numUpdates) - => string.Format(Loc.Localize("AutoUpdateUpdatesAvailableContent", "{0} updates available."), numUpdates); + => string.Format(Loc.Localize("AutoUpdateUpdatesAvailableContent", "{0} plugin updates"), numUpdates); public static string NotificationContentPreparingToUpdate(int numPlugins) => string.Format(Loc.Localize("AutoUpdatePreparingToUpdate", "Preparing to update {0} plugins..."), numPlugins); From 979f78a8b6b01a0de061470e1a44515935e1f776 Mon Sep 17 00:00:00 2001 From: goat Date: Mon, 17 Jun 2024 21:23:31 +0200 Subject: [PATCH 03/12] crashhandler: immediately terminate the game in release mode --- DalamudCrashHandler/DalamudCrashHandler.cpp | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp index d4e9f0a1c..4b1d4a6e5 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.cpp +++ b/DalamudCrashHandler/DalamudCrashHandler.cpp @@ -965,7 +965,7 @@ int main() { const TASKDIALOG_BUTTON buttons[]{ {IdButtonRestart, L"Restart\nRestart the game with the above-selected option."}, {IdButtonSaveTsPack, L"Save Troubleshooting Info\nSave a .tspack file containing information about this crash for analysis."}, - {IdButtonExit, L"Exit\nExit the game."}, + {IdButtonExit, L"Exit\nExit without doing anything."}, }; config.cbSize = sizeof(config); @@ -1060,12 +1060,21 @@ int main() { pProgressDialog->Release(); pProgressDialog = NULL; } + + const auto kill_game = [&] { TerminateProcess(g_hProcess, exinfo.ExceptionRecord.ExceptionCode); }; if (shutup) { - TerminateProcess(g_hProcess, exinfo.ExceptionRecord.ExceptionCode); + kill_game(); return 0; } +#if !_DEBUG + // In release mode, we can't resume the game, so just kill it. It's not safe to keep it running, as we + // don't know what state it's in and it may have crashed off-thread. + // Additionally, if the main thread crashed, Windows will show the ANR dialog, which will block our dialog. + kill_game(); +#endif + int nButtonPressed = 0, nRadioButton = 0; if (FAILED(TaskDialogIndirect(&config, &nButtonPressed, &nRadioButton, nullptr))) { SetEvent(exinfo.hEventHandle); @@ -1073,7 +1082,7 @@ int main() { switch (nButtonPressed) { case IdButtonRestart: { - TerminateProcess(g_hProcess, exinfo.ExceptionRecord.ExceptionCode); + kill_game(); restart_game_using_injector(nRadioButton, *launcherArgs); break; } @@ -1081,7 +1090,7 @@ int main() { if (attemptResume) SetEvent(exinfo.hEventHandle); else - TerminateProcess(g_hProcess, exinfo.ExceptionRecord.ExceptionCode); + kill_game(); } } } From 8553ef5412f66bfc572d527822ec7244710135f0 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 18 Jun 2024 21:37:30 +0200 Subject: [PATCH 04/12] pm: make sure to always destroy scope and interface when unloading a plugin --- Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 44 +++++++++++++------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 36af7e4bc..0d6745845 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -483,6 +483,7 @@ internal class LocalPlugin : IDisposable { case PluginState.Unloaded: throw new InvalidPluginOperationException($"Unable to unload {this.Name}, already unloaded"); + case PluginState.DependencyResolutionFailed: case PluginState.UnloadError: if (!this.IsDev) { @@ -501,31 +502,42 @@ internal class LocalPlugin : IDisposable } this.State = PluginState.Unloading; - Log.Information($"Unloading {this.DllFile.Name}"); + Log.Information("Unloading {PluginName}", this.InternalName); - if (this.manifest.CanUnloadAsync || framework == null) - this.instance?.Dispose(); - else - await framework.RunOnFrameworkThread(() => this.instance?.Dispose()); - - this.instance = null; - this.UnloadAndDisposeState(); - - if (!reloading) + try { - if (waitBeforeLoaderDispose && this.loader != null) - await Task.Delay(configuration.PluginWaitBeforeFree ?? PluginManager.PluginWaitBeforeFreeDefault); - this.loader?.Dispose(); - this.loader = null; + if (this.manifest.CanUnloadAsync || framework == null) + this.instance?.Dispose(); + else + await framework.RunOnFrameworkThread(() => this.instance?.Dispose()); + } + catch (Exception e) + { + this.State = PluginState.UnloadError; + Log.Error(e, "Could not unload {PluginName}, error in plugin dispose", this.InternalName); + return; + } + finally + { + this.instance = null; + this.UnloadAndDisposeState(); + + if (!reloading) + { + if (waitBeforeLoaderDispose && this.loader != null) + await Task.Delay(configuration.PluginWaitBeforeFree ?? PluginManager.PluginWaitBeforeFreeDefault); + this.loader?.Dispose(); + this.loader = null; + } } this.State = PluginState.Unloaded; - Log.Information($"Finished unloading {this.DllFile.Name}"); + Log.Information("Finished unloading {PluginName}", this.InternalName); } catch (Exception ex) { this.State = PluginState.UnloadError; - Log.Error(ex, $"Error while unloading {this.Name}"); + Log.Error(ex, "Error while unloading {PluginName}", this.InternalName); throw; } From 911c109239db773bbaa0530b316ea705c38ec47c Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 18 Jun 2024 21:50:08 +0200 Subject: [PATCH 05/12] pi: make collapsible by default --- .../Internal/Windows/PluginInstaller/PluginInstallerWindow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index fe603b35f..05a352a53 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -141,7 +141,7 @@ internal class PluginInstallerWindow : Window, IDisposable public PluginInstallerWindow(PluginImageCache imageCache, DalamudConfiguration configuration) : base( Locs.WindowTitle + (configuration.DoPluginTest ? Locs.WindowTitleMod_Testing : string.Empty) + "###XlPluginInstaller", - ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollbar) + ImGuiWindowFlags.NoScrollbar) { this.IsOpen = true; this.imageCache = imageCache; From c155be7db61c8dc08306f34336b2af8222b1fb8e Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 18 Jun 2024 22:27:36 +0200 Subject: [PATCH 06/12] autoupdate: handle singular/plural in notification loc --- .../Internal/AutoUpdate/AutoUpdateManager.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs index 09ab22959..229566b3e 100644 --- a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs +++ b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs @@ -445,18 +445,24 @@ internal class AutoUpdateManager : IServiceType public static string NotificationContentUpdatesFailedMinimized => Loc.Localize("AutoUpdateUpdatesFailedContentMinimized", "Plugins failed to update."); public static string NotificationContentUpdatesAvailable(int numUpdates) - => string.Format(Loc.Localize("AutoUpdateUpdatesAvailableContent", "There are {0} plugins that can be updated."), numUpdates); + => numUpdates == 1 ? + Loc.Localize("AutoUpdateUpdatesAvailableContentSingular", "There is a plugin that can be updated.") : + string.Format(Loc.Localize("AutoUpdateUpdatesAvailableContentPlural", "There are {0} plugins that can be updated."), numUpdates); public static string NotificationContentUpdatesAvailableMinimized(int numUpdates) - => string.Format(Loc.Localize("AutoUpdateUpdatesAvailableContent", "{0} plugin updates"), numUpdates); + => numUpdates == 1 ? + Loc.Localize("AutoUpdateUpdatesAvailableContentMinimizedSingular", "1 plugin update available") : + string.Format(Loc.Localize("AutoUpdateUpdatesAvailableContentMinimizedPlural", "{0} plugin updates available"), numUpdates); public static string NotificationContentPreparingToUpdate(int numPlugins) - => string.Format(Loc.Localize("AutoUpdatePreparingToUpdate", "Preparing to update {0} plugins..."), numPlugins); + => numPlugins == 1 ? + Loc.Localize("AutoUpdatePreparingToUpdateSingular", "Preparing to update 1 plugin...") : + string.Format(Loc.Localize("AutoUpdatePreparingToUpdatePlural", "Preparing to update {0} plugins..."), numPlugins); public static string NotificationContentUpdating(string name) => string.Format(Loc.Localize("AutoUpdateUpdating", "Updating {0}..."), name); public static string NotificationContentFailedPlugins(IEnumerable failedPlugins) - => string.Format(Loc.Localize("AutoUpdateFailedPlugins", "Failed plugins: {0}"), string.Join(", ", failedPlugins)); + => string.Format(Loc.Localize("AutoUpdateFailedPlugins", "Failed plugin(s): {0}"), string.Join(", ", failedPlugins)); } } From 3509a0bdca9b09854926cf2d1d8a6745d6d416d4 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 18 Jun 2024 23:04:04 +0200 Subject: [PATCH 07/12] pi: add "updateable plugins" page, open kind, make auto-updates go there by default --- Dalamud/Interface/DalamudWindowOpenKinds.cs | 5 + .../Internal/PluginCategoryManager.cs | 5 +- .../PluginInstaller/PluginInstallerWindow.cs | 128 ++++++++++++------ .../Internal/AutoUpdate/AutoUpdateManager.cs | 23 ++-- 4 files changed, 106 insertions(+), 55 deletions(-) diff --git a/Dalamud/Interface/DalamudWindowOpenKinds.cs b/Dalamud/Interface/DalamudWindowOpenKinds.cs index 1f82cca49..588ff858b 100644 --- a/Dalamud/Interface/DalamudWindowOpenKinds.cs +++ b/Dalamud/Interface/DalamudWindowOpenKinds.cs @@ -14,6 +14,11 @@ public enum PluginInstallerOpenKind /// Open to the "Installed Plugins" page. /// InstalledPlugins, + + /// + /// Open to the "Can be updated" page. + /// + UpdateablePlugins, /// /// Open to the "Changelogs" page. diff --git a/Dalamud/Interface/Internal/PluginCategoryManager.cs b/Dalamud/Interface/Internal/PluginCategoryManager.cs index 3f94b2d2e..a02dfffb9 100644 --- a/Dalamud/Interface/Internal/PluginCategoryManager.cs +++ b/Dalamud/Interface/Internal/PluginCategoryManager.cs @@ -28,6 +28,7 @@ internal class PluginCategoryManager new(12, "special.dalamud", () => Locs.Category_Dalamud), new(13, "special.plugins", () => Locs.Category_Plugins), new(14, "special.profiles", () => Locs.Category_PluginProfiles), + new(15, "special.updateable", () => Locs.Category_UpdateablePlugins), new(FirstTagBasedCategoryId + 0, "other", () => Locs.Category_Other), new(FirstTagBasedCategoryId + 1, "jobs", () => Locs.Category_Jobs), new(FirstTagBasedCategoryId + 2, "ui", () => Locs.Category_UI), @@ -43,7 +44,7 @@ internal class PluginCategoryManager private GroupInfo[] groupList = { new(GroupKind.DevTools, () => Locs.Group_DevTools, 10, 11), - new(GroupKind.Installed, () => Locs.Group_Installed, 0, 1, 14), + new(GroupKind.Installed, () => Locs.Group_Installed, 0, 1, 15, 14), new(GroupKind.Available, () => Locs.Group_Available, 0), new(GroupKind.Changelog, () => Locs.Group_Changelog, 0, 12, 13), @@ -432,6 +433,8 @@ internal class PluginCategoryManager public static string Category_PluginProfiles => Loc.Localize("InstallerCategoryPluginProfiles", "Plugin Collections"); + public static string Category_UpdateablePlugins => Loc.Localize("InstallerCategoryCanBeUpdated", "Can be updated"); + public static string Category_Other => Loc.Localize("InstallerCategoryOther", "Other"); public static string Category_Jobs => Loc.Localize("InstallerCategoryJobs", "Jobs"); diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 05a352a53..046557e87 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -221,6 +221,14 @@ internal class PluginInstallerWindow : Window, IDisposable IsTesting = 1 << 6, } + private enum InstalledPluginListFilter + { + None, + Testing, + Updateable, + Dev, + } + private bool AnyOperationInProgress => this.installStatus == OperationStatus.InProgress || this.updateStatus == OperationStatus.InProgress || this.enableDisableStatus == OperationStatus.InProgress; @@ -430,6 +438,12 @@ internal class PluginInstallerWindow : Window, IDisposable // All category this.categoryManager.CurrentCategoryIdx = 0; break; + case PluginInstallerOpenKind.UpdateablePlugins: + // Installed group + this.categoryManager.CurrentGroupIdx = 1; + // Updateable category + this.categoryManager.CurrentCategoryIdx = 15; + break; case PluginInstallerOpenKind.Changelogs: // Changelog group this.categoryManager.CurrentGroupIdx = 3; @@ -1215,7 +1229,8 @@ internal class PluginInstallerWindow : Window, IDisposable if (proxy.LocalPlugin != null) { - this.DrawInstalledPlugin(proxy.LocalPlugin, i++, proxy.RemoteManifest, true); + var update = this.pluginListUpdatable.FirstOrDefault(up => up.InstalledPlugin == proxy.LocalPlugin); + this.DrawInstalledPlugin(proxy.LocalPlugin, i++, proxy.RemoteManifest, update); } else if (proxy.RemoteManifest != null) { @@ -1225,8 +1240,8 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.PopID(); } } - - private void DrawInstalledPluginList(bool filterTesting) + + private void DrawInstalledPluginList(InstalledPluginListFilter filter) { var pluginList = this.pluginListInstalled; var manager = Service.Get(); @@ -1247,47 +1262,57 @@ internal class PluginInstallerWindow : Window, IDisposable return; } + var drewAny = false; var i = 0; foreach (var plugin in filteredList) { - if (filterTesting && !manager.HasTestingOptIn(plugin.Manifest)) + if (filter == InstalledPluginListFilter.Testing && !manager.HasTestingOptIn(plugin.Manifest)) continue; + + // Find applicable update and manifest, if we have them + AvailablePluginUpdate? update = null; + RemotePluginManifest? remoteManifest = null; - // Find the applicable remote manifest - var remoteManifest = this.pluginListAvailable - .FirstOrDefault(rm => rm.InternalName == plugin.Manifest.InternalName && - rm.RepoUrl == plugin.Manifest.RepoUrl); + if (filter != InstalledPluginListFilter.Dev) + { + update = this.pluginListUpdatable.FirstOrDefault(up => up.InstalledPlugin == plugin); + if (filter == InstalledPluginListFilter.Updateable && update == null) + continue; - this.DrawInstalledPlugin(plugin, i++, remoteManifest); + // Find the applicable remote manifest + remoteManifest = this.pluginListAvailable + .FirstOrDefault(rm => rm.InternalName == plugin.Manifest.InternalName && + rm.RepoUrl == plugin.Manifest.RepoUrl); + } + else if (!plugin.IsDev) + { + continue; + } + + this.DrawInstalledPlugin(plugin, i++, remoteManifest, update); + drewAny = true; } - } - - private void DrawInstalledDevPluginList() - { - var pluginList = this.pluginListInstalled - .Where(plugin => plugin.IsDev) - .ToList(); - - if (pluginList.Count == 0) + + if (!drewAny) { - ImGui.TextColored(ImGuiColors.DalamudGrey, Locs.TabBody_SearchNoInstalled); - return; - } + var text = filter switch + { + InstalledPluginListFilter.None => Locs.TabBody_NoPluginsInstalled, + InstalledPluginListFilter.Testing => Locs.TabBody_NoPluginsTesting, + InstalledPluginListFilter.Updateable => Locs.TabBody_NoPluginsUpdateable, + InstalledPluginListFilter.Dev => Locs.TabBody_NoPluginsDev, + _ => throw new ArgumentException(null, nameof(filter)), + }; + + ImGuiHelpers.ScaledDummy(60); - var filteredList = pluginList - .Where(plugin => !this.IsManifestFiltered(plugin.Manifest)) - .ToList(); - - if (filteredList.Count == 0) - { - ImGui.TextColored(ImGuiColors.DalamudGrey2, Locs.TabBody_SearchNoMatching); - return; - } - - var i = 0; - foreach (var plugin in filteredList) - { - this.DrawInstalledPlugin(plugin, i++, null); + using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey)) + { + foreach (var line in text.Split('\n')) + { + ImGuiHelpers.CenteredText(line); + } + } } } @@ -1462,7 +1487,7 @@ internal class PluginInstallerWindow : Window, IDisposable switch (this.categoryManager.CurrentCategoryIdx) { case 0: - this.DrawInstalledDevPluginList(); + this.DrawInstalledPluginList(InstalledPluginListFilter.Dev); break; case 1: @@ -1470,7 +1495,7 @@ internal class PluginInstallerWindow : Window, IDisposable break; default: - // umm, there's nothing else, keep handled set and just skip drawing... + ImGui.TextUnformatted("You found a mysterious category. Please keep it to yourself."); break; } @@ -1479,16 +1504,24 @@ internal class PluginInstallerWindow : Window, IDisposable switch (this.categoryManager.CurrentCategoryIdx) { case 0: - this.DrawInstalledPluginList(false); + this.DrawInstalledPluginList(InstalledPluginListFilter.None); break; case 1: - this.DrawInstalledPluginList(true); + this.DrawInstalledPluginList(InstalledPluginListFilter.Testing); + break; + + case 2: + this.DrawInstalledPluginList(InstalledPluginListFilter.Updateable); break; - case 2: + case 3: this.profileManagerWidget.Draw(); break; + + default: + ImGui.TextUnformatted("You found a secret category. Please feel a sense of pride and accomplishment."); + break; } break; @@ -2325,7 +2358,7 @@ internal class PluginInstallerWindow : Window, IDisposable } } - private void DrawInstalledPlugin(LocalPlugin plugin, int index, RemotePluginManifest? remoteManifest, bool showInstalled = false) + private void DrawInstalledPlugin(LocalPlugin plugin, int index, RemotePluginManifest? remoteManifest, AvailablePluginUpdate? availablePluginUpdate, bool showInstalled = false) { var configuration = Service.Get(); var commandManager = Service.Get(); @@ -2384,8 +2417,6 @@ internal class PluginInstallerWindow : Window, IDisposable trouble = true; } - var availablePluginUpdate = this.pluginListUpdatable.FirstOrDefault(up => up.InstalledPlugin == plugin); - // Dev plugins can never update if (plugin.IsDev) availablePluginUpdate = null; @@ -3684,7 +3715,16 @@ internal class PluginInstallerWindow : Window, IDisposable public static string TabBody_DownloadFailed => Loc.Localize("InstallerDownloadFailed", "Download failed."); public static string TabBody_SafeMode => Loc.Localize("InstallerSafeMode", "Dalamud is running in Plugin Safe Mode, restart to activate plugins."); - + + public static string TabBody_NoPluginsTesting => Loc.Localize("InstallerNoPluginsTesting", "You aren't testing any plugins at the moment!\nYou can opt in to testing versions in the plugin context menu."); + + public static string TabBody_NoPluginsInstalled => + string.Format(Loc.Localize("InstallerNoPluginsInstalled", "You don't have any plugins installed yet!\nYou can install them from the \"{0}\" tab."), PluginCategoryManager.Locs.Category_All); + + public static string TabBody_NoPluginsUpdateable => Loc.Localize("InstallerNoPluginsUpdate", "No plugins have updates available at the moment."); + + public static string TabBody_NoPluginsDev => Loc.Localize("InstallerNoPluginsDev", "You don't have any dev plugins. Add them some the settings."); + #endregion #region Search text diff --git a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs index 229566b3e..cbcbe637e 100644 --- a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs +++ b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs @@ -128,6 +128,17 @@ internal class AutoUpdateManager : IServiceType _ => throw new ArgumentOutOfRangeException(nameof(behavior), behavior, null), }; } + + private static void DrawOpenInstallerNotificationButton(bool primary, IActiveNotification notification) + { + if (primary ? + DalamudComponents.PrimaryButton(Locs.NotificationButtonOpenPluginInstaller) : + DalamudComponents.SecondaryButton(Locs.NotificationButtonOpenPluginInstaller)) + { + Service.Get().OpenPluginInstallerTo(PluginInstallerOpenKind.UpdateablePlugins); + notification.DismissNow(); + } + } private void OnUpdate(IFramework framework) { @@ -293,11 +304,7 @@ internal class AutoUpdateManager : IServiceType notification.DrawActions += _ => { ImGuiHelpers.ScaledDummy(2); - if (DalamudComponents.PrimaryButton(Locs.NotificationButtonOpenPluginInstaller)) - { - Service.Get().OpenPluginInstaller(); - notification.DismissNow(); - } + DrawOpenInstallerNotificationButton(true, notification); }; // Update the notification to show the final state @@ -356,11 +363,7 @@ internal class AutoUpdateManager : IServiceType } ImGui.SameLine(); - if (DalamudComponents.SecondaryButton(Locs.NotificationButtonOpenPluginInstaller)) - { - Service.Get().OpenPluginInstaller(); - notification.DismissNow(); - } + DrawOpenInstallerNotificationButton(false, notification); }; } From a8025298ea2626e0783b2642a0f9be3a9ad6402b Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 19 Jun 2024 00:26:14 +0200 Subject: [PATCH 08/12] pi: don't use indices to refer to groups and categories Fixes some bugs I bugged into the codebase by adding a new category inbetween two others --- .../Internal/PluginCategoryManager.cs | 246 ++++++++++++------ .../PluginInstaller/PluginInstallerWindow.cs | 85 +++--- 2 files changed, 215 insertions(+), 116 deletions(-) diff --git a/Dalamud/Interface/Internal/PluginCategoryManager.cs b/Dalamud/Interface/Internal/PluginCategoryManager.cs index a02dfffb9..62e7cd97d 100644 --- a/Dalamud/Interface/Internal/PluginCategoryManager.cs +++ b/Dalamud/Interface/Internal/PluginCategoryManager.cs @@ -16,47 +16,49 @@ internal class PluginCategoryManager /// /// First categoryId for tag based categories. /// - public const int FirstTagBasedCategoryId = 100; + private const int FirstTagBasedCategoryId = 100; private readonly CategoryInfo[] categoryList = - { - new(0, "special.all", () => Locs.Category_All), - new(1, "special.isTesting", () => Locs.Category_IsTesting, CategoryInfo.AppearCondition.DoPluginTest), - new(2, "special.availableForTesting", () => Locs.Category_AvailableForTesting, CategoryInfo.AppearCondition.DoPluginTest), - new(10, "special.devInstalled", () => Locs.Category_DevInstalled), - new(11, "special.devIconTester", () => Locs.Category_IconTester), - new(12, "special.dalamud", () => Locs.Category_Dalamud), - new(13, "special.plugins", () => Locs.Category_Plugins), - new(14, "special.profiles", () => Locs.Category_PluginProfiles), - new(15, "special.updateable", () => Locs.Category_UpdateablePlugins), - new(FirstTagBasedCategoryId + 0, "other", () => Locs.Category_Other), - new(FirstTagBasedCategoryId + 1, "jobs", () => Locs.Category_Jobs), - new(FirstTagBasedCategoryId + 2, "ui", () => Locs.Category_UI), - new(FirstTagBasedCategoryId + 3, "minigames", () => Locs.Category_MiniGames), - new(FirstTagBasedCategoryId + 4, "inventory", () => Locs.Category_Inventory), - new(FirstTagBasedCategoryId + 5, "sound", () => Locs.Category_Sound), - new(FirstTagBasedCategoryId + 6, "social", () => Locs.Category_Social), - new(FirstTagBasedCategoryId + 7, "utility", () => Locs.Category_Utility), + [ + new(CategoryKind.All, "special.all", () => Locs.Category_All), + new(CategoryKind.IsTesting, "special.isTesting", () => Locs.Category_IsTesting, CategoryInfo.AppearCondition.DoPluginTest), + new(CategoryKind.AvailableForTesting, "special.availableForTesting", () => Locs.Category_AvailableForTesting, CategoryInfo.AppearCondition.DoPluginTest), + new(CategoryKind.DevInstalled, "special.devInstalled", () => Locs.Category_DevInstalled), + new(CategoryKind.IconTester, "special.devIconTester", () => Locs.Category_IconTester), + new(CategoryKind.DalamudChangelogs, "special.dalamud", () => Locs.Category_Dalamud), + new(CategoryKind.PluginChangelogs, "special.plugins", () => Locs.Category_Plugins), + new(CategoryKind.PluginProfiles, "special.profiles", () => Locs.Category_PluginProfiles), + new(CategoryKind.UpdateablePlugins, "special.updateable", () => Locs.Category_UpdateablePlugins), + + // Tag-driven categories + new(CategoryKind.Other, "other", () => Locs.Category_Other), + new(CategoryKind.Jobs, "jobs", () => Locs.Category_Jobs), + new(CategoryKind.Ui, "ui", () => Locs.Category_UI), + new(CategoryKind.MiniGames, "minigames", () => Locs.Category_MiniGames), + new(CategoryKind.Inventory, "inventory", () => Locs.Category_Inventory), + new(CategoryKind.Sound, "sound", () => Locs.Category_Sound), + new(CategoryKind.Social, "social", () => Locs.Category_Social), + new(CategoryKind.Utility, "utility", () => Locs.Category_Utility) // order doesn't matter, all tag driven categories should have Id >= FirstTagBasedCategoryId - }; + ]; private GroupInfo[] groupList = - { - new(GroupKind.DevTools, () => Locs.Group_DevTools, 10, 11), - new(GroupKind.Installed, () => Locs.Group_Installed, 0, 1, 15, 14), - new(GroupKind.Available, () => Locs.Group_Available, 0), - new(GroupKind.Changelog, () => Locs.Group_Changelog, 0, 12, 13), + [ + new(GroupKind.DevTools, () => Locs.Group_DevTools, CategoryKind.DevInstalled, CategoryKind.IconTester), + new(GroupKind.Installed, () => Locs.Group_Installed, CategoryKind.All, CategoryKind.IsTesting, CategoryKind.UpdateablePlugins, CategoryKind.PluginProfiles), + new(GroupKind.Available, () => Locs.Group_Available, CategoryKind.All), + new(GroupKind.Changelog, () => Locs.Group_Changelog, CategoryKind.All, CategoryKind.DalamudChangelogs, CategoryKind.PluginChangelogs) // order important, used for drawing, keep in sync with defaults for currentGroupIdx - }; + ]; private int currentGroupIdx = 2; - private int currentCategoryIdx = 0; + private CategoryKind currentCategoryKind = CategoryKind.All; private bool isContentDirty; - private Dictionary mapPluginCategories = new(); - private List highlightedCategoryIds = new(); + private Dictionary mapPluginCategories = new(); + private List highlightedCategoryKinds = new(); /// /// Type of category group. @@ -84,6 +86,97 @@ internal class PluginCategoryManager Changelog, } + /// + /// Type of category. + /// + public enum CategoryKind + { + /// + /// All plugins. + /// + All = 0, + + /// + /// Plugins currently being tested. + /// + IsTesting = 1, + + /// + /// Plugins available for testing. + /// + AvailableForTesting = 2, + + /// + /// Installed dev plugins. + /// + DevInstalled = 10, + + /// + /// Icon tester. + /// + IconTester = 11, + + /// + /// Changelogs for Dalamud. + /// + DalamudChangelogs = 12, + + /// + /// Changelogs for plugins. + /// + PluginChangelogs = 13, + + /// + /// Change plugin profiles. + /// + PluginProfiles = 14, + + /// + /// Updateable plugins. + /// + UpdateablePlugins = 15, + + /// + /// Plugins tagged as "other". + /// + Other = FirstTagBasedCategoryId + 0, + + /// + /// Plugins tagged as "jobs". + /// + Jobs = FirstTagBasedCategoryId + 1, + + /// + /// Plugins tagged as "ui". + /// + Ui = FirstTagBasedCategoryId + 2, + + /// + /// Plugins tagged as "minigames". + /// + MiniGames = FirstTagBasedCategoryId + 3, + + /// + /// Plugins tagged as "inventory". + /// + Inventory = FirstTagBasedCategoryId + 4, + + /// + /// Plugins tagged as "sound". + /// + Sound = FirstTagBasedCategoryId + 5, + + /// + /// Plugins tagged as "social". + /// + Social = FirstTagBasedCategoryId + 6, + + /// + /// Plugins tagged as "utility". + /// + Utility = FirstTagBasedCategoryId + 7, + } + /// /// Gets the list of all known categories. /// @@ -93,39 +186,50 @@ internal class PluginCategoryManager /// Gets the list of all known UI groups. /// public GroupInfo[] GroupList => this.groupList; - + /// - /// Gets or sets current group. + /// Gets or sets the current group kind. /// - public int CurrentGroupIdx + public GroupKind CurrentGroupKind { - get => this.currentGroupIdx; + get => this.groupList[this.currentGroupIdx].GroupKind; set { - if (this.currentGroupIdx != value) + var newIdx = Array.FindIndex(this.groupList, x => x.GroupKind == value); + if (newIdx >= 0) { - this.currentGroupIdx = value; - this.currentCategoryIdx = 0; + this.currentGroupIdx = newIdx; + this.currentCategoryKind = this.CurrentGroup.Categories.First(); this.isContentDirty = true; } } } - + /// - /// Gets or sets current category, index in current Group.Categories array. + /// Gets information about currently selected group. /// - public int CurrentCategoryIdx + public GroupInfo CurrentGroup => this.groupList[this.currentGroupIdx]; + + /// + /// Gets or sets the current category kind. + /// + public CategoryKind CurrentCategoryKind { - get => this.currentCategoryIdx; + get => this.currentCategoryKind; set { - if (this.currentCategoryIdx != value) + if (this.currentCategoryKind != value) { - this.currentCategoryIdx = value; + this.currentCategoryKind = value; this.isContentDirty = true; } } } + + /// + /// Gets information about currently selected category. + /// + public CategoryInfo CurrentCategory => this.categoryList.First(x => x.CategoryKind == this.currentCategoryKind); /// /// Gets a value indicating whether current group + category selection changed recently. @@ -134,13 +238,12 @@ internal class PluginCategoryManager public bool IsContentDirty => this.isContentDirty; /// - /// Gets a value indicating whether and are valid. + /// Gets a value indicating whether and are valid. /// public bool IsSelectionValid => (this.currentGroupIdx >= 0) && (this.currentGroupIdx < this.groupList.Length) && - (this.currentCategoryIdx >= 0) && - (this.currentCategoryIdx < this.groupList[this.currentGroupIdx].Categories.Count); + this.groupList[this.currentGroupIdx].Categories.Contains(this.currentCategoryKind); /// /// Rebuild available categories based on currently available plugins. @@ -152,10 +255,10 @@ internal class PluginCategoryManager this.mapPluginCategories.Clear(); var groupAvail = Array.Find(this.groupList, x => x.GroupKind == GroupKind.Available); - var prevCategoryIds = new List(); + var prevCategoryIds = new List(); prevCategoryIds.AddRange(groupAvail.Categories); - var categoryList = new List(); + var categoryList = new List(); var allCategoryIndices = new List(); foreach (var manifest in availablePlugins) @@ -171,10 +274,10 @@ internal class PluginCategoryManager var matchIdx = Array.FindIndex(this.CategoryList, x => x.Tag.Equals(tag, StringComparison.InvariantCultureIgnoreCase)); if (matchIdx >= 0) { - var categoryId = this.CategoryList[matchIdx].CategoryId; - if (categoryId >= FirstTagBasedCategoryId) + var categoryKind = this.CategoryList[matchIdx].CategoryKind; + if ((int)categoryKind >= FirstTagBasedCategoryId) { - categoryList.Add(categoryId); + categoryList.Add(categoryKind); if (!allCategoryIndices.Contains(matchIdx)) { @@ -186,7 +289,7 @@ internal class PluginCategoryManager } if (PluginManager.HasTestingVersion(manifest) || manifest.IsTestingExclusive) - categoryList.Add(2); + categoryList.Add(CategoryKind.AvailableForTesting); // always add, even if empty this.mapPluginCategories.Add(manifest, categoryList.ToArray()); @@ -204,11 +307,11 @@ internal class PluginCategoryManager foreach (var categoryIdx in allCategoryIndices) { - groupAvail.Categories.Add(this.CategoryList[categoryIdx].CategoryId); + groupAvail.Categories.Add(this.CategoryList[categoryIdx].CategoryKind); } // compare with prev state and mark as dirty if needed - var noCategoryChanges = Enumerable.SequenceEqual(prevCategoryIds, groupAvail.Categories); + var noCategoryChanges = prevCategoryIds.SequenceEqual(groupAvail.Categories); if (!noCategoryChanges) { this.isContentDirty = true; @@ -229,20 +332,20 @@ internal class PluginCategoryManager { var groupInfo = this.groupList[this.currentGroupIdx]; - var includeAll = (this.currentCategoryIdx == 0) || (groupInfo.GroupKind != GroupKind.Available); + var includeAll = (this.currentCategoryKind == CategoryKind.All) || (groupInfo.GroupKind != GroupKind.Available); if (includeAll) { result.AddRange(plugins); } else { - var selectedCategoryInfo = Array.Find(this.categoryList, x => x.CategoryId == groupInfo.Categories[this.currentCategoryIdx]); + var selectedCategoryInfo = Array.Find(this.categoryList, x => x.CategoryKind == this.currentCategoryKind); foreach (var plugin in plugins) { if (this.mapPluginCategories.TryGetValue(plugin, out var pluginCategoryIds)) { - var matchIdx = Array.IndexOf(pluginCategoryIds, selectedCategoryInfo.CategoryId); + var matchIdx = Array.IndexOf(pluginCategoryIds, selectedCategoryInfo.CategoryKind); if (matchIdx >= 0) { result.Add(plugin); @@ -270,20 +373,17 @@ internal class PluginCategoryManager /// List of plugins whose categories should be highlighted. public void SetCategoryHighlightsForPlugins(IEnumerable plugins) { - this.highlightedCategoryIds.Clear(); + this.highlightedCategoryKinds.Clear(); - if (plugins != null) + foreach (var entry in plugins) { - foreach (var entry in plugins) + if (this.mapPluginCategories.TryGetValue(entry, out var pluginCategories)) { - if (this.mapPluginCategories.TryGetValue(entry, out var pluginCategories)) + foreach (var categoryKind in pluginCategories) { - foreach (var categoryId in pluginCategories) + if (!this.highlightedCategoryKinds.Contains(categoryKind)) { - if (!this.highlightedCategoryIds.Contains(categoryId)) - { - this.highlightedCategoryIds.Add(categoryId); - } + this.highlightedCategoryKinds.Add(categoryKind); } } } @@ -293,9 +393,9 @@ internal class PluginCategoryManager /// /// Checks if category should be highlighted. /// - /// CategoryId to check. + /// CategoryKind to check. /// true if highlight is needed. - public bool IsCategoryHighlighted(int categoryId) => this.highlightedCategoryIds.Contains(categoryId); + public bool IsCategoryHighlighted(CategoryKind categoryKind) => this.highlightedCategoryKinds.Contains(categoryKind); private IEnumerable GetCategoryTagsForManifest(PluginManifest pluginManifest) { @@ -315,7 +415,7 @@ internal class PluginCategoryManager /// /// Unique Id number of category, tag match based should be greater of equal . /// - public int CategoryId; + public CategoryKind CategoryKind; /// /// Tag from plugin manifest to match. @@ -327,13 +427,13 @@ internal class PluginCategoryManager /// /// Initializes a new instance of the struct. /// - /// Unique id of category. + /// Kind of the category. /// Tag to match. /// Function returning localized name of category. /// Condition to be checked when deciding whether this category should be shown. - public CategoryInfo(int categoryId, string tag, Func nameFunc, AppearCondition condition = AppearCondition.None) + public CategoryInfo(CategoryKind categoryKind, string tag, Func nameFunc, AppearCondition condition = AppearCondition.None) { - this.CategoryId = categoryId; + this.CategoryKind = categoryKind; this.Tag = tag; this.nameFunc = nameFunc; this.Condition = condition; @@ -379,7 +479,7 @@ internal class PluginCategoryManager /// /// List of categories in container. /// - public List Categories; + public List Categories; private Func nameFunc; @@ -389,7 +489,7 @@ internal class PluginCategoryManager /// Type of group. /// Function returning localized name of category. /// List of category Ids to hardcode. - public GroupInfo(GroupKind groupKind, Func nameFunc, params int[] categories) + public GroupInfo(GroupKind groupKind, Func nameFunc, params CategoryKind[] categories) { this.GroupKind = groupKind; this.nameFunc = nameFunc; diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 046557e87..1c64e9baa 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -428,27 +428,27 @@ internal class PluginInstallerWindow : Window, IDisposable { case PluginInstallerOpenKind.AllPlugins: // Plugins group - this.categoryManager.CurrentGroupIdx = 2; + this.categoryManager.CurrentGroupKind = PluginCategoryManager.GroupKind.Available; // All category - this.categoryManager.CurrentCategoryIdx = 0; + this.categoryManager.CurrentCategoryKind = PluginCategoryManager.CategoryKind.All; break; case PluginInstallerOpenKind.InstalledPlugins: // Installed group - this.categoryManager.CurrentGroupIdx = 1; + this.categoryManager.CurrentGroupKind = PluginCategoryManager.GroupKind.Installed; // All category - this.categoryManager.CurrentCategoryIdx = 0; + this.categoryManager.CurrentCategoryKind = PluginCategoryManager.CategoryKind.All; break; case PluginInstallerOpenKind.UpdateablePlugins: // Installed group - this.categoryManager.CurrentGroupIdx = 1; + this.categoryManager.CurrentGroupKind = PluginCategoryManager.GroupKind.Installed; // Updateable category - this.categoryManager.CurrentCategoryIdx = 15; + this.categoryManager.CurrentCategoryKind = PluginCategoryManager.CategoryKind.UpdateablePlugins; break; case PluginInstallerOpenKind.Changelogs: // Changelog group - this.categoryManager.CurrentGroupIdx = 3; + this.categoryManager.CurrentGroupKind = PluginCategoryManager.GroupKind.Changelog; // Plugins category - this.categoryManager.CurrentCategoryIdx = 2; + this.categoryManager.CurrentCategoryKind = PluginCategoryManager.CategoryKind.All; break; default: throw new ArgumentOutOfRangeException(nameof(kind), kind, null); @@ -611,7 +611,8 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.SetCursorPosX(windowSize.X - sortSelectWidth - (style.ItemSpacing.X * 2) - searchInputWidth - searchClearButtonWidth); var isProfileManager = - this.categoryManager.CurrentGroupIdx == 1 && this.categoryManager.CurrentCategoryIdx == 2; + this.categoryManager.CurrentGroupKind == PluginCategoryManager.GroupKind.Installed && + this.categoryManager.CurrentCategoryKind == PluginCategoryManager.CategoryKind.PluginProfiles; // Disable search if profile editor using (ImRaii.Disabled(isProfileManager)) @@ -641,7 +642,7 @@ internal class PluginInstallerWindow : Window, IDisposable } // Disable sort if changelogs or profile editor - using (ImRaii.Disabled(this.categoryManager.CurrentGroupIdx == 3 || isProfileManager)) + using (ImRaii.Disabled(this.categoryManager.CurrentGroupKind == PluginCategoryManager.GroupKind.Changelog || isProfileManager)) { ImGui.SameLine(); ImGui.SetCursorPosY(downShift); @@ -781,9 +782,7 @@ internal class PluginInstallerWindow : Window, IDisposable Service.Get().PrintUpdatedPlugins(this.updatedPlugins, Locs.PluginUpdateHeader_Chatbox); notifications.AddNotification(Locs.Notifications_UpdatesInstalled(this.updatePluginCount), Locs.Notifications_UpdatesInstalledTitle, NotificationType.Success); - var installedGroupIdx = this.categoryManager.GroupList.TakeWhile( - x => x.GroupKind != PluginCategoryManager.GroupKind.Installed).Count(); - this.categoryManager.CurrentGroupIdx = installedGroupIdx; + this.categoryManager.CurrentGroupKind = PluginCategoryManager.GroupKind.Installed; } else if (this.updatePluginCount == 0) { @@ -1373,29 +1372,29 @@ internal class PluginInstallerWindow : Window, IDisposable } } - for (var groupIdx = 0; groupIdx < this.categoryManager.GroupList.Length; groupIdx++) + foreach (var groupInfo in this.categoryManager.GroupList) { - var groupInfo = this.categoryManager.GroupList[groupIdx]; var canShowGroup = (groupInfo.GroupKind != PluginCategoryManager.GroupKind.DevTools) || this.hasDevPlugins; if (!canShowGroup) { continue; } - ImGui.SetNextItemOpen(groupIdx == this.categoryManager.CurrentGroupIdx); - if (ImGui.CollapsingHeader(groupInfo.Name, groupIdx == this.categoryManager.CurrentGroupIdx ? ImGuiTreeNodeFlags.OpenOnDoubleClick : ImGuiTreeNodeFlags.None)) + var isCurrent = groupInfo.GroupKind == this.categoryManager.CurrentGroupKind; + ImGui.SetNextItemOpen(isCurrent); + if (ImGui.CollapsingHeader(groupInfo.Name, isCurrent ? ImGuiTreeNodeFlags.OpenOnDoubleClick : ImGuiTreeNodeFlags.None)) { - if (this.categoryManager.CurrentGroupIdx != groupIdx) + if (!isCurrent) { - this.categoryManager.CurrentGroupIdx = groupIdx; + this.categoryManager.CurrentGroupKind = groupInfo.GroupKind; } ImGui.Indent(); var categoryItemSize = new Vector2(ImGui.GetContentRegionAvail().X - (5 * ImGuiHelpers.GlobalScale), ImGui.GetTextLineHeight()); - for (var categoryIdx = 0; categoryIdx < groupInfo.Categories.Count; categoryIdx++) + foreach (var categoryKind in groupInfo.Categories) { - var categoryInfo = Array.Find(this.categoryManager.CategoryList, x => x.CategoryId == groupInfo.Categories[categoryIdx]); - + var categoryInfo = this.categoryManager.CategoryList.First(x => x.CategoryKind == categoryKind); + switch (categoryInfo.Condition) { case PluginCategoryManager.CategoryInfo.AppearCondition.None: @@ -1409,15 +1408,15 @@ internal class PluginInstallerWindow : Window, IDisposable throw new ArgumentOutOfRangeException(); } - var hasSearchHighlight = this.categoryManager.IsCategoryHighlighted(categoryInfo.CategoryId); + var hasSearchHighlight = this.categoryManager.IsCategoryHighlighted(categoryInfo.CategoryKind); if (hasSearchHighlight) { ImGui.PushStyleColor(ImGuiCol.Text, colorSearchHighlight); } - if (ImGui.Selectable(categoryInfo.Name, this.categoryManager.CurrentCategoryIdx == categoryIdx, ImGuiSelectableFlags.None, categoryItemSize)) + if (ImGui.Selectable(categoryInfo.Name, this.categoryManager.CurrentCategoryKind == categoryKind, ImGuiSelectableFlags.None, categoryItemSize)) { - this.categoryManager.CurrentCategoryIdx = categoryIdx; + this.categoryManager.CurrentCategoryKind = categoryKind; } if (hasSearchHighlight) @@ -1427,11 +1426,7 @@ internal class PluginInstallerWindow : Window, IDisposable } ImGui.Unindent(); - - if (groupIdx != this.categoryManager.GroupList.Length - 1) - { - ImGuiHelpers.ScaledDummy(5); - } + ImGuiHelpers.ScaledDummy(5); } } } @@ -1467,7 +1462,7 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, ImGuiHelpers.ScaledVector2(1, 3)); - var groupInfo = this.categoryManager.GroupList[this.categoryManager.CurrentGroupIdx]; + var groupInfo = this.categoryManager.CurrentGroup; if (this.categoryManager.IsContentDirty) { // reset opened list of collapsibles when switching between categories @@ -1484,13 +1479,13 @@ internal class PluginInstallerWindow : Window, IDisposable { case PluginCategoryManager.GroupKind.DevTools: // this one is never sorted and remains in hardcoded order from group ctor - switch (this.categoryManager.CurrentCategoryIdx) + switch (this.categoryManager.CurrentCategoryKind) { - case 0: + case PluginCategoryManager.CategoryKind.DevInstalled: this.DrawInstalledPluginList(InstalledPluginListFilter.Dev); break; - case 1: + case PluginCategoryManager.CategoryKind.IconTester: this.DrawImageTester(); break; @@ -1501,21 +1496,21 @@ internal class PluginInstallerWindow : Window, IDisposable break; case PluginCategoryManager.GroupKind.Installed: - switch (this.categoryManager.CurrentCategoryIdx) + switch (this.categoryManager.CurrentCategoryKind) { - case 0: + case PluginCategoryManager.CategoryKind.All: this.DrawInstalledPluginList(InstalledPluginListFilter.None); break; - case 1: + case PluginCategoryManager.CategoryKind.IsTesting: this.DrawInstalledPluginList(InstalledPluginListFilter.Testing); break; - case 2: + case PluginCategoryManager.CategoryKind.UpdateablePlugins: this.DrawInstalledPluginList(InstalledPluginListFilter.Updateable); break; - case 3: + case PluginCategoryManager.CategoryKind.PluginProfiles: this.profileManagerWidget.Draw(); break; @@ -1526,19 +1521,23 @@ internal class PluginInstallerWindow : Window, IDisposable break; case PluginCategoryManager.GroupKind.Changelog: - switch (this.categoryManager.CurrentCategoryIdx) + switch (this.categoryManager.CurrentCategoryKind) { - case 0: + case PluginCategoryManager.CategoryKind.All: this.DrawChangelogList(true, true); break; - case 1: + case PluginCategoryManager.CategoryKind.DalamudChangelogs: this.DrawChangelogList(true, false); break; - case 2: + case PluginCategoryManager.CategoryKind.PluginChangelogs: this.DrawChangelogList(false, true); break; + + default: + ImGui.TextUnformatted("You found a quiet category. Please don't wake it up."); + break; } break; From 79392230c462dc4dcbe46be7ea82eb6a06e6bff2 Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 19 Jun 2024 22:48:38 +0200 Subject: [PATCH 09/12] pi: correctly clear highlighted categories when clearing the search bar --- Dalamud/Interface/Internal/PluginCategoryManager.cs | 2 ++ .../Internal/Windows/PluginInstaller/PluginInstallerWindow.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/PluginCategoryManager.cs b/Dalamud/Interface/Internal/PluginCategoryManager.cs index 62e7cd97d..ec2a1c15b 100644 --- a/Dalamud/Interface/Internal/PluginCategoryManager.cs +++ b/Dalamud/Interface/Internal/PluginCategoryManager.cs @@ -373,6 +373,8 @@ internal class PluginCategoryManager /// List of plugins whose categories should be highlighted. public void SetCategoryHighlightsForPlugins(IEnumerable plugins) { + ArgumentNullException.ThrowIfNull(plugins); + this.highlightedCategoryKinds.Clear(); foreach (var entry in plugins) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 1c64e9baa..3ceb19afa 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -3573,7 +3573,7 @@ internal class PluginInstallerWindow : Window, IDisposable { if (string.IsNullOrEmpty(this.searchText)) { - this.categoryManager.SetCategoryHighlightsForPlugins(null); + this.categoryManager.SetCategoryHighlightsForPlugins(Array.Empty()); // Reset here for good measure, as we're returning from a search this.openPluginCollapsibles.Clear(); From d244f7e09e04db1a23f93b1bd3c0e78b1c7d9cd6 Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 20 Jun 2024 01:14:40 +0200 Subject: [PATCH 10/12] autoupdate: wait longer for periodic check if user dismissed updates --- .../Internal/AutoUpdate/AutoUpdateManager.cs | 95 ++++++++++++------- 1 file changed, 60 insertions(+), 35 deletions(-) diff --git a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs index cbcbe637e..07f023006 100644 --- a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs +++ b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs @@ -11,6 +11,7 @@ using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Conditions; using Dalamud.Interface; using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.ImGuiNotification.EventArgs; using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.DesignSystem; @@ -40,7 +41,13 @@ internal class AutoUpdateManager : IServiceType /// /// Time we should wait between scheduled update checks. /// - private static readonly TimeSpan TimeBetweenUpdateChecks = TimeSpan.FromHours(1.5); + private static readonly TimeSpan TimeBetweenUpdateChecks = TimeSpan.FromHours(2); + + /// + /// Time we should wait between scheduled update checks if the user has dismissed the notification, + /// instead of updating. We don't want to spam the user with notifications. + /// + private static readonly TimeSpan TimeBetweenUpdateChecksIfDismissed = TimeSpan.FromHours(12); /// /// Time we should wait after unblocking to nag the user. @@ -63,12 +70,13 @@ internal class AutoUpdateManager : IServiceType private readonly IConsoleVariable isDryRun; private DateTime? loginTime; - private DateTime? lastUpdateCheckTime; + private DateTime? nextUpdateCheckTime; private DateTime? unblockedSince; private bool hasStartedInitialUpdateThisSession; private IActiveNotification? updateNotification; + private bool notificationHasStartedUpdate; // Used to track if the user has started an update from the notification. private Task? autoUpdateTask; @@ -96,7 +104,7 @@ internal class AutoUpdateManager : IServiceType }); console.AddCommand("dalamud.autoupdate.force_check", "Force a check for updates", () => { - this.lastUpdateCheckTime = DateTime.Now - TimeBetweenUpdateChecks; + this.nextUpdateCheckTime = DateTime.Now + TimeSpan.FromSeconds(5); return true; }); } @@ -129,13 +137,13 @@ internal class AutoUpdateManager : IServiceType }; } - private static void DrawOpenInstallerNotificationButton(bool primary, IActiveNotification notification) + private static void DrawOpenInstallerNotificationButton(bool primary, PluginInstallerOpenKind kind, IActiveNotification notification) { if (primary ? DalamudComponents.PrimaryButton(Locs.NotificationButtonOpenPluginInstaller) : DalamudComponents.SecondaryButton(Locs.NotificationButtonOpenPluginInstaller)) { - Service.Get().OpenPluginInstallerTo(PluginInstallerOpenKind.UpdateablePlugins); + Service.Get().OpenPluginInstallerTo(kind); notification.DismissNow(); } } @@ -182,11 +190,9 @@ internal class AutoUpdateManager : IServiceType // the only time we actually install updates automatically. if (!this.hasStartedInitialUpdateThisSession && DateTime.Now > this.loginTime.Value.Add(UpdateTimeAfterLogin)) { - this.lastUpdateCheckTime = DateTime.Now; this.hasStartedInitialUpdateThisSession = true; var currentlyUpdatablePlugins = this.GetAvailablePluginUpdates(DecideUpdateListingRestriction(behavior)); - if (currentlyUpdatablePlugins.Count == 0) { this.IsAutoUpdateComplete = true; @@ -198,12 +204,13 @@ internal class AutoUpdateManager : IServiceType if (behavior == AutoUpdateBehavior.OnlyNotify) { // List all plugins in the notification - Log.Verbose("Ran initial update, notifying for {Num} plugins", currentlyUpdatablePlugins.Count); + Log.Verbose("Running initial auto-update, notifying for {Num} plugins", currentlyUpdatablePlugins.Count); this.NotifyUpdatesAreAvailable(currentlyUpdatablePlugins); return; } - Log.Verbose("Ran initial update, updating {Num} plugins", currentlyUpdatablePlugins.Count); + Log.Verbose("Running initial auto-update, updating {Num} plugins", currentlyUpdatablePlugins.Count); + this.notificationHasStartedUpdate = true; this.KickOffAutoUpdates(currentlyUpdatablePlugins); return; } @@ -211,10 +218,13 @@ internal class AutoUpdateManager : IServiceType // 2. Continuously check for updates while the game is running. We run these every once in a while and // will only show a notification here that lets people start the update or open the installer. if (this.config.CheckPeriodicallyForUpdates && - this.lastUpdateCheckTime != null && - DateTime.Now - this.lastUpdateCheckTime > TimeBetweenUpdateChecks && + this.nextUpdateCheckTime != null && + DateTime.Now > this.nextUpdateCheckTime && this.updateNotification == null) { + this.nextUpdateCheckTime = null; + + Log.Verbose("Starting periodic update check"); this.pluginManager.ReloadPluginMastersAsync() .ContinueWith( t => @@ -228,8 +238,6 @@ internal class AutoUpdateManager : IServiceType this.GetAvailablePluginUpdates( DecideUpdateListingRestriction(behavior))); }); - - this.lastUpdateCheckTime = DateTime.Now; } } @@ -238,20 +246,34 @@ internal class AutoUpdateManager : IServiceType if (this.updateNotification != null) throw new InvalidOperationException("Already showing a notification"); + if (this.notificationHasStartedUpdate) + throw new InvalidOperationException("Lost track of notification state"); + this.updateNotification = this.notificationManager.AddNotification(notification); this.updateNotification.Dismiss += _ => { this.updateNotification = null; - this.lastUpdateCheckTime = DateTime.Now; + + // If the user just clicked off the notification, we don't want to bother them again for quite a while. + if (this.notificationHasStartedUpdate) + { + this.nextUpdateCheckTime = DateTime.Now + TimeBetweenUpdateChecks; + Log.Verbose("User started update, next check at {Time}", this.nextUpdateCheckTime); + } + else + { + this.nextUpdateCheckTime = DateTime.Now + TimeBetweenUpdateChecksIfDismissed; + Log.Verbose("User dismissed update notification, next check at {Time}", this.nextUpdateCheckTime); + } }; return this.updateNotification!; } - private void KickOffAutoUpdates(ICollection updatablePlugins) + private void KickOffAutoUpdates(ICollection updatablePlugins, IActiveNotification? notification = null) { this.autoUpdateTask = - Task.Run(() => this.RunAutoUpdates(updatablePlugins)) + Task.Run(() => this.RunAutoUpdates(updatablePlugins, notification)) .ContinueWith(t => { if (t.IsFaulted) @@ -268,25 +290,23 @@ internal class AutoUpdateManager : IServiceType }); } - private async Task RunAutoUpdates(ICollection updatablePlugins) + private async Task RunAutoUpdates(ICollection updatablePlugins, IActiveNotification? notification = null) { Log.Information("Found {UpdatablePluginsCount} plugins to update", updatablePlugins.Count); if (updatablePlugins.Count == 0) return; - var notification = this.GetBaseNotification(new Notification - { - Title = Locs.NotificationTitleUpdatingPlugins, - Content = Locs.NotificationContentPreparingToUpdate(updatablePlugins.Count), - Type = NotificationType.Info, - InitialDuration = TimeSpan.MaxValue, - ShowIndeterminateIfNoExpiry = false, - UserDismissable = false, - Progress = 0, - Icon = INotificationIcon.From(FontAwesomeIcon.Download), - Minimized = false, - }); + notification ??= this.GetBaseNotification(new Notification()); + notification.Title = Locs.NotificationTitleUpdatingPlugins; + notification.Content = Locs.NotificationContentPreparingToUpdate(updatablePlugins.Count); + notification.Type = NotificationType.Info; + notification.InitialDuration = TimeSpan.MaxValue; + notification.ShowIndeterminateIfNoExpiry = false; + notification.UserDismissable = false; + notification.Progress = 0; + notification.Icon = INotificationIcon.From(FontAwesomeIcon.Download); + notification.Minimized = false; var progress = new Progress(); progress.ProgressChanged += (_, updateProgress) => @@ -304,7 +324,7 @@ internal class AutoUpdateManager : IServiceType notification.DrawActions += _ => { ImGuiHelpers.ScaledDummy(2); - DrawOpenInstallerNotificationButton(true, notification); + DrawOpenInstallerNotificationButton(true, PluginInstallerOpenKind.InstalledPlugins, notification); }; // Update the notification to show the final state @@ -340,6 +360,8 @@ internal class AutoUpdateManager : IServiceType { if (updatablePlugins.Count == 0) return; + + this.notificationHasStartedUpdate = false; var notification = this.GetBaseNotification(new Notification { @@ -352,19 +374,22 @@ internal class AutoUpdateManager : IServiceType Icon = INotificationIcon.From(FontAwesomeIcon.Download), }); - notification.DrawActions += _ => + void DrawNotificationContent(INotificationDrawArgs args) { ImGuiHelpers.ScaledDummy(2); if (DalamudComponents.PrimaryButton(Locs.NotificationButtonUpdate)) { - this.KickOffAutoUpdates(updatablePlugins); - notification.DismissNow(); + notification.DrawActions -= DrawNotificationContent; + this.KickOffAutoUpdates(updatablePlugins, notification); + this.notificationHasStartedUpdate = true; } ImGui.SameLine(); - DrawOpenInstallerNotificationButton(false, notification); - }; + DrawOpenInstallerNotificationButton(false, PluginInstallerOpenKind.UpdateablePlugins, notification); + } + + notification.DrawActions += DrawNotificationContent; } private List GetAvailablePluginUpdates(UpdateListingRestriction restriction) From 5d2942786f93659b15f73b3cd06fba6af7491fc0 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Fri, 21 Jun 2024 01:00:13 -0700 Subject: [PATCH 11/12] Add Attestations to Dalamud CI (#1848) --- .github/workflows/main.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0a6b44eeb..d78c87d68 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,6 +7,10 @@ concurrency: jobs: build: name: Build on Windows + permissions: + id-token: write + contents: read + attestations: write runs-on: windows-2022 steps: - name: Checkout Dalamud @@ -40,6 +44,17 @@ jobs: run: .\sign.ps1 .\bin\Release - name: Create hashlist run: .\CreateHashList.ps1 .\bin\Release + - name: Attest Build + if: ${{ github.repository_owner == 'goatcorp' && github.event_name == 'push' }} + uses: actions/attest-build-provenance@v1 + with: + subject-path: | + bin/Release/hashes.json + bin/Release/Dalamud.dll + bin/Release/DalamudCrashHandler.exe + bin/Release/Dalamud.*.dll + bin/Release/Dalamud.*.exe + bin/Release/FFXIVClientStructs.dll - name: Upload artifact uses: actions/upload-artifact@v2 with: From 50dc3d44c2a7cf5dfd8972e3fcf0bd92c9323bad Mon Sep 17 00:00:00 2001 From: Anna Date: Fri, 21 Jun 2024 22:36:18 +0000 Subject: [PATCH 12/12] Add /xlcopylog command (#1850) * feat: add /xlcopylog command Add a command to copy the dalamud.log file to the user's clipboard, behaving the same as copying the file in an Explorer window. * chore: fix clientstructs submodule * fix: dispose hGlobal if it's unused * refactor: move file copier to util class --- Dalamud/Interface/Internal/DalamudCommands.cs | 21 +++++ Dalamud/NativeMethods.txt | 10 ++ Dalamud/Utility/Util.cs | 92 ++++++++++++++++--- 3 files changed, 111 insertions(+), 12 deletions(-) diff --git a/Dalamud/Interface/Internal/DalamudCommands.cs b/Dalamud/Interface/Internal/DalamudCommands.cs index 18936687a..cc6f1b64c 100644 --- a/Dalamud/Interface/Internal/DalamudCommands.cs +++ b/Dalamud/Interface/Internal/DalamudCommands.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using CheapLoc; @@ -143,6 +144,13 @@ internal class DalamudCommands : IServiceType HelpMessage = "ImGui DEBUG", ShowInHelp = false, }); + + commandManager.AddHandler("/xlcopylog", new CommandInfo(this.OnCopyLogCommand) + { + HelpMessage = Loc.Localize( + "DalamudCopyLogHelp", + "Copy the dalamud.log file to your clipboard."), + }); } private void OnUnloadCommand(string command, string arguments) @@ -406,4 +414,17 @@ internal class DalamudCommands : IServiceType { Service.Get().ToggleProfilerWindow(); } + + private void OnCopyLogCommand(string command, string arguments) + { + var chatGui = Service.Get(); + var logPath = Path.Join( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "XIVLauncher", + "dalamud.log"); + var message = Util.CopyFilesToClipboard([logPath]) + ? Loc.Localize("DalamudLogCopySuccess", "Log file copied to clipboard.") + : Loc.Localize("DalamudLogCopyFailure", "Could not copy log file to clipboard."); + chatGui.Print(message); + } } diff --git a/Dalamud/NativeMethods.txt b/Dalamud/NativeMethods.txt index c42e76c1c..d9d05e472 100644 --- a/Dalamud/NativeMethods.txt +++ b/Dalamud/NativeMethods.txt @@ -11,3 +11,13 @@ SetActiveWindow HWND_TOPMOST HWND_NOTOPMOST SET_WINDOW_POS_FLAGS + +OpenClipboard +SetClipboardData +CloseClipboard +DROPFILES +CLIPBOARD_FORMAT +GlobalAlloc +GlobalLock +GlobalUnlock +GLOBAL_ALLOC_FLAGS diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 3c3efada4..85769d2da 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -25,6 +25,11 @@ using Lumina.Excel.GeneratedSheets; using Serilog; using TerraFX.Interop.Windows; using Windows.Win32.Storage.FileSystem; +using Windows.Win32.System.Memory; +using Windows.Win32.System.Ole; + +using HWND = Windows.Win32.Foundation.HWND; +using Win32_PInvoke = Windows.Win32.PInvoke; namespace Dalamud.Utility; @@ -476,12 +481,12 @@ public static class Util case "MacOS": return OSPlatform.OSX; case "Linux": return OSPlatform.Linux; } - + // n.b. we had some fancy code here to check if the Wine host version returned "Darwin" but apparently // *all* our Wines report Darwin if exports aren't hidden. As such, it is effectively impossible (without some // (very cursed and inaccurate heuristics) to determine if we're on macOS or Linux unless we're explicitly told // by our launcher. See commit a7aacb15e4603a367e2f980578271a9a639d8852 for the old check. - + return IsWine() ? OSPlatform.Linux : OSPlatform.Windows; } @@ -544,7 +549,7 @@ public static class Util } } } - } + } finally { foreach (var enumerator in enumerators) @@ -585,7 +590,7 @@ public static class Util { WriteAllTextSafe(path, text, Encoding.UTF8); } - + /// /// Overwrite text in a file by first writing it to a temporary file, and then /// moving that file to the path specified. @@ -597,7 +602,7 @@ public static class Util { WriteAllBytesSafe(path, encoding.GetBytes(text)); } - + /// /// Overwrite data in a file by first writing it to a temporary file, and then /// moving that file to the path specified. @@ -607,13 +612,13 @@ public static class Util public static unsafe void WriteAllBytesSafe(string path, byte[] bytes) { ArgumentException.ThrowIfNullOrEmpty(path); - + // Open the temp file var tempPath = path + ".tmp"; using var tempFile = Windows.Win32.PInvoke.CreateFile( - tempPath, - (uint)(FILE_ACCESS_RIGHTS.FILE_GENERIC_READ | FILE_ACCESS_RIGHTS.FILE_GENERIC_WRITE), + tempPath, + (uint)(FILE_ACCESS_RIGHTS.FILE_GENERIC_READ | FILE_ACCESS_RIGHTS.FILE_GENERIC_WRITE), FILE_SHARE_MODE.FILE_SHARE_NONE, null, FILE_CREATION_DISPOSITION.CREATE_ALWAYS, @@ -622,7 +627,7 @@ public static class Util if (tempFile.IsInvalid) throw new Win32Exception(); - + // Write the data uint bytesWritten = 0; if (!Windows.Win32.PInvoke.WriteFile(tempFile, new ReadOnlySpan(bytes), &bytesWritten, null)) @@ -633,7 +638,7 @@ public static class Util if (!Windows.Win32.PInvoke.FlushFileBuffers(tempFile)) throw new Win32Exception(); - + tempFile.Close(); if (!Windows.Win32.PInvoke.MoveFileEx(tempPath, path, MOVE_FILE_FLAGS.MOVEFILE_REPLACE_EXISTING | MOVE_FILE_FLAGS.MOVEFILE_WRITE_THROUGH)) @@ -736,6 +741,69 @@ public static class Util } } + /// + /// Copy files to the clipboard as if they were copied in Explorer. + /// + /// Full paths to files to be copied. + /// Returns true on success. + internal static unsafe bool CopyFilesToClipboard(IEnumerable paths) + { + var pathBytes = paths + .Select(Encoding.Unicode.GetBytes) + .ToArray(); + var pathBytesSize = pathBytes + .Select(bytes => bytes.Length) + .Sum(); + var sizeWithTerminators = pathBytesSize + (pathBytes.Length * 2); + + var dropFilesSize = sizeof(DROPFILES); + var hGlobal = Win32_PInvoke.GlobalAlloc_SafeHandle( + GLOBAL_ALLOC_FLAGS.GHND, + // struct size + size of encoded strings + null terminator for each + // string + two null terminators for end of list + (uint)(dropFilesSize + sizeWithTerminators + 4)); + var dropFiles = (DROPFILES*)Win32_PInvoke.GlobalLock(hGlobal); + + *dropFiles = default; + dropFiles->fWide = true; + dropFiles->pFiles = (uint)dropFilesSize; + + var pathLoc = (byte*)((nint)dropFiles + dropFilesSize); + foreach (var bytes in pathBytes) + { + // copy the encoded strings + for (var i = 0; i < bytes.Length; i++) + { + pathLoc![i] = bytes[i]; + } + + // null terminate + pathLoc![bytes.Length] = 0; + pathLoc[bytes.Length + 1] = 0; + pathLoc += bytes.Length + 2; + } + + // double null terminator for end of list + for (var i = 0; i < 4; i++) + { + pathLoc![i] = 0; + } + + Win32_PInvoke.GlobalUnlock(hGlobal); + + if (Win32_PInvoke.OpenClipboard(HWND.Null)) + { + Win32_PInvoke.SetClipboardData( + (uint)CLIPBOARD_FORMAT.CF_HDROP, + hGlobal); + Win32_PInvoke.CloseClipboard(); + return true; + } + + hGlobal.Dispose(); + return false; + } + private static void ShowSpanProperty(ulong addr, IList path, PropertyInfo p, object obj) { var objType = obj.GetType(); @@ -750,7 +818,7 @@ public static class Util "-", MethodAttributes.Public | MethodAttributes.Static, CallingConventions.Standard, - null, + null, new[] { typeof(object), typeof(IList), typeof(ulong) }, obj.GetType(), true); @@ -907,7 +975,7 @@ public static class Util } } } - + /// /// Show a structure in an ImGui context. ///