From 9bdfa58debf41e1ecc0fffa729aa2adae2f701ae Mon Sep 17 00:00:00 2001 From: Infi Date: Sun, 21 Apr 2024 23:16:49 +0200 Subject: [PATCH] Implement some todos from #27 --- ChatTwo/LegacyMessageImporter.cs | 73 +++++-- ChatTwo/Plugin.cs | 8 +- ChatTwo/Ui/LegacyMesasgeImporterWindow.cs | 201 ------------------- ChatTwo/Ui/LegacyMessageImporterWindow.cs | 225 ++++++++++++++++++++++ ChatTwo/Ui/SettingsTabs/Database.cs | 4 +- ChatTwo/Util/ImGuiUtil.cs | 38 +++- 6 files changed, 314 insertions(+), 235 deletions(-) delete mode 100644 ChatTwo/Ui/LegacyMesasgeImporterWindow.cs create mode 100644 ChatTwo/Ui/LegacyMessageImporterWindow.cs diff --git a/ChatTwo/LegacyMessageImporter.cs b/ChatTwo/LegacyMessageImporter.cs index b45aaf8..c06c3a0 100644 --- a/ChatTwo/LegacyMessageImporter.cs +++ b/ChatTwo/LegacyMessageImporter.cs @@ -84,27 +84,41 @@ internal class LegacyMessageImporterEligibility } } - internal LegacyMessageImporter StartImport(MessageStore targetStore, bool noLog = false) + internal LegacyMessageImporter StartImport(MessageStore targetStore, bool noLog = false, Plugin? plugin = null) { if (Status != LegacyMessageImporterEligibilityStatus.Eligible) throw new InvalidOperationException($"Migration not eligible: status is {Status}"); - return new LegacyMessageImporter(targetStore, originalDbPath: OriginalDbPath, migrationDbPath: MigrationDbPath, noLog: noLog); + return new LegacyMessageImporter(targetStore, originalDbPath: OriginalDbPath, migrationDbPath: MigrationDbPath, noLog: noLog, plugin); } /// /// Makes the migration ineligible so the user won't be asked again. /// - internal void RenameOldDatabase() + internal bool RenameOldDatabase() { - File.Move(OriginalDbPath, MigrationDbPath); - Status = LegacyMessageImporterEligibilityStatus.IneligibleMigrationDbExists; - AdditionalIneligibilityInfo = "User chose to rename the old database file"; + try + { + File.Move(OriginalDbPath, MigrationDbPath); + Status = LegacyMessageImporterEligibilityStatus.IneligibleMigrationDbExists; + AdditionalIneligibilityInfo = "User chose to rename the old database file"; + return true; + } + catch (Exception ex) + { + Plugin.Log.Error(ex, "Unable to move the old database"); + return false; + } } } -internal class LegacyMessageImporter : IDisposable +internal class LegacyMessageImporter : IAsyncDisposable { + private readonly Plugin? Plugin; + + private readonly CancellationTokenSource CancellationToken = new(); + private Thread? WorkingThread = null; + internal const string MessagesCollection = "messages"; private const int MaxFailedMessageLogs = 10; @@ -130,16 +144,17 @@ internal class LegacyMessageImporter : IDisposable // This can be set by the user to limit the rate at which messages are // imported. If the rate exceeds this value, the importer will sleep for the // remainder of the second. - internal int MaxMessageRate { get; set; } = 250; // start low + internal int MaxMessageRate = 250; // start low // Do not call this directly, use // LegacyMessageImporterEligibility.StartImport instead. - internal LegacyMessageImporter(MessageStore targetStore, string? originalDbPath = null, string? migrationDbPath = null, bool noLog = false) + internal LegacyMessageImporter(MessageStore targetStore, string? originalDbPath = null, string? migrationDbPath = null, bool noLog = false, Plugin? plugin = null) { _targetStore = targetStore; originalDbPath ??= Path.Join(Plugin.Interface.ConfigDirectory.FullName, "chat.db"); migrationDbPath ??= migrationDbPath ?? Path.Join(Plugin.Interface.ConfigDirectory.FullName, "chat-litedb.db"); _log = noLog ? null : Plugin.Log; + Plugin = plugin; _log?.Info($"[Migration] Moving '{originalDbPath}' to '{migrationDbPath}'"); File.Move(originalDbPath, migrationDbPath); @@ -147,12 +162,30 @@ internal class LegacyMessageImporter : IDisposable _database = Connect(migrationDbPath); ImportStart = Environment.TickCount64; - new Thread(DoImport).Start(); + WorkingThread = new Thread(() => DoImport(CancellationToken.Token)); + WorkingThread.Start(); } public void Dispose() { - // TODO: cancel thread and wait for it to close + _database?.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await CancellationToken.CancelAsync(); + + var timeout = 10_000; // 10s + while (WorkingThread != null && timeout > 0) + { + if (!WorkingThread.IsAlive) + break; + + timeout -= 100; + await Task.Delay(100); + Plugin.Log.Information("Sleeping because thread still alive"); + } + _database?.Dispose(); } @@ -248,7 +281,7 @@ internal class LegacyMessageImporter : IDisposable return conn; } - private void DoImport() + private void DoImport(CancellationToken token) { var importRateTimer = Stopwatch.StartNew(); var messagesInLastSecond = 0; @@ -261,6 +294,9 @@ internal class LegacyMessageImporter : IDisposable var messages = messagesCollection.Query().OrderBy(msg => msg.Date).ToDocuments(); foreach (var messageDoc in messages) { + if (token.IsCancellationRequested) + return; + try { var message = BsonDocumentToMessage(messageDoc); @@ -271,8 +307,7 @@ internal class LegacyMessageImporter : IDisposable { FailedMessages++; if (FailedMessages <= MaxFailedMessageLogs) - _log?.Error( - $"[Migration] Failed to import message '{messageDoc["_id"].AsObjectId}' (usually due to corruption): {e}"); + _log?.Error($"[Migration] Failed to import message '{messageDoc["_id"].AsObjectId}' (usually due to corruption): {e}"); if (FailedMessages == MaxFailedMessageLogs) _log?.Error("[Migration] Further failed message logs will be suppressed"); } @@ -293,19 +328,19 @@ internal class LegacyMessageImporter : IDisposable // Log every 1,000 messages if ((SuccessfulMessages + FailedMessages) % 1000 == 0) - _log?.Information( - $"[Migration] Progress: successfully imported {SuccessfulMessages}/{totalMessages} messages ({FailedMessages} failures)"); + _log?.Information($"[Migration] Progress: successfully imported {SuccessfulMessages}/{totalMessages} messages ({FailedMessages} failures)"); } _log?.Information($"[Migration] Imported {SuccessfulMessages}/{FailedMessages} messages, {FailedMessages} failed"); - if (ProcessedMessages != totalMessages) - _log?.Warning( - $"[Migration] Total message count mismatch: expected {totalMessages}, got {SuccessfulMessages + FailedMessages}"); + _log?.Warning($"[Migration] Total message count mismatch: expected {totalMessages}, got {SuccessfulMessages + FailedMessages}"); ImportComplete = Environment.TickCount64; _database.Dispose(); _database = null; + + if (Plugin != null) + Plugin.Framework.Run(() => Plugin.MessageManager.FilterAllTabs(false), token); } private static Message BsonDocumentToMessage(BsonDocument doc) diff --git a/ChatTwo/Plugin.cs b/ChatTwo/Plugin.cs index cec74f4..6ee1b60 100755 --- a/ChatTwo/Plugin.cs +++ b/ChatTwo/Plugin.cs @@ -49,7 +49,7 @@ public sealed class Plugin : IDalamudPlugin public ChatLogWindow ChatLogWindow { get; } public CommandHelpWindow CommandHelpWindow { get; } public SeStringDebugger SeStringDebugger { get; } - internal LegacyMesasgeImporterWindow LegacyMesasgeImporterWindow { get; } + internal LegacyMessageImporterWindow LegacyMessageImporterWindow { get; } internal Configuration Config { get; } internal Commands Commands { get; } @@ -106,8 +106,8 @@ public sealed class Plugin : IDalamudPlugin MessageManager = new MessageManager(this); // requires Ui // Requires MessageManager - LegacyMesasgeImporterWindow = new LegacyMesasgeImporterWindow(MessageManager.Store); - WindowSystem.AddWindow(LegacyMesasgeImporterWindow); + LegacyMessageImporterWindow = new LegacyMessageImporterWindow(this); + WindowSystem.AddWindow(LegacyMessageImporterWindow); // let all the other components register, then initialise commands Commands.Initialise(); @@ -143,7 +143,7 @@ public sealed class Plugin : IDalamudPlugin ChatLogWindow?.Dispose(); SettingsWindow?.Dispose(); SeStringDebugger?.Dispose(); - LegacyMesasgeImporterWindow?.Dispose(); + LegacyMessageImporterWindow?.Dispose(); ExtraChat?.Dispose(); Ipc?.Dispose(); diff --git a/ChatTwo/Ui/LegacyMesasgeImporterWindow.cs b/ChatTwo/Ui/LegacyMesasgeImporterWindow.cs deleted file mode 100644 index d7bb412..0000000 --- a/ChatTwo/Ui/LegacyMesasgeImporterWindow.cs +++ /dev/null @@ -1,201 +0,0 @@ -using System.Numerics; -using ChatTwo.Resources; -using ChatTwo.Util; -using Dalamud.Interface.ImGuiNotification; -using Dalamud.Interface.Internal.Notifications; -using Dalamud.Interface.Windowing; -using ImGuiNET; - -namespace ChatTwo.Ui; - -internal class LegacyMesasgeImporterWindow : Window -{ - private readonly MessageStore _store; - - private LegacyMessageImporterEligibility Eligibility { get; set; } - private LegacyMessageImporter? Importer { get; set; } - - internal LegacyMesasgeImporterWindow(MessageStore store) : base("Chat 2 Legacy Importer###chat2-legacy-importer") - { - _store = store; - Eligibility = LegacyMessageImporterEligibility.CheckEligibility(); - LogAndNotify(); - } - - public void Dispose() - { - Importer?.Dispose(); - } - - private void LogAndNotify() - { - Plugin.Log.Info( - $"[Migration] Checked migration eligibility: {Eligibility.Status} - '{Eligibility.AdditionalIneligibilityInfo}'"); - - switch (Eligibility.Status) - { - case LegacyMessageImporterEligibilityStatus.Eligible: - { - var notification = Plugin.Notification.AddNotification(new Notification - { - Type = NotificationType.Info, - // The user needs to dismiss this for it to go away. - InitialDuration = TimeSpan.FromHours(6), - Title = "Chat 2 Migration", - Content = "Import messages from old database into new database? Click for more information.", - }); - // TODO: clicking does not dismiss - notification.Click += _ => IsOpen = true; - break; - } - - case LegacyMessageImporterEligibilityStatus.IneligibleLiteDbFailed: - { - var notification = Plugin.Notification.AddNotification(new Notification - { - Type = NotificationType.Warning, - InitialDuration = TimeSpan.FromMinutes(1), - Title = "Chat Two Migration", - Content = - "Migration is not possible because the old database could not be opened. Click for more information." - }); - // TODO: clicking does not dismiss - notification.Click += _ => IsOpen = true; - break; - } - } - } - - public override void Draw() - { - if (Importer != null) - { - DrawImportStatus(); - return; - } - - if (Eligibility.Status == LegacyMessageImporterEligibilityStatus.Eligible) - DrawEligible(); - else - DrawIneligible(); - } - - private void DrawEligible() - { - // TODO: pretty - ImGui.Text("Import database messages from legacy LiteDB database to Sqlite database?"); - ImGui.Text($"Message count: {Eligibility.MessageCount}"); - ImGui.Text($"Database size: {Eligibility.DatabaseSizeBytes}"); - - if (ImGui.Button("Yes, import messages")) - { - // Next draw call will run DrawImportStatus(). - Importer = Eligibility.StartImport(_store); - return; - } - - ImGui.SameLine(); - - if (ImGuiUtil.CtrlShiftButton("No, do not import messages", - "Ctrl+Shift: renames old database to avoid prompting again")) - { - Eligibility.RenameOldDatabase(); - IsOpen = false; - } - } - - private void DrawIneligible() - { - // TODO: pretty - ImGui.Text("Your legacy LiteDB database is not eligible for import:"); - switch (Eligibility.Status) - { - case LegacyMessageImporterEligibilityStatus.IneligibleOriginalDbNotExists: - ImGui.Text("The old database could not be found."); - break; - case LegacyMessageImporterEligibilityStatus.IneligibleMigrationDbExists: - ImGui.Text("The migration process was already started."); - break; - case LegacyMessageImporterEligibilityStatus.IneligibleLiteDbFailed: - ImGui.Text("The old database could not be opened."); - break; - case LegacyMessageImporterEligibilityStatus.IneligibleNoMessages: - ImGui.Text("The old database contains no messages."); - break; - case LegacyMessageImporterEligibilityStatus.Eligible: - default: - throw new ArgumentOutOfRangeException(); - } - if (!string.IsNullOrWhiteSpace(Eligibility.AdditionalIneligibilityInfo)) - ImGui.Text(Eligibility.AdditionalIneligibilityInfo); - - // LiteDB failures notify the user, so give them a chance to rename the - // database to avoid prompting again. - if (Eligibility.Status == LegacyMessageImporterEligibilityStatus.IneligibleLiteDbFailed) - { - if (ImGuiUtil.CtrlShiftButton("Rename old database", - "Ctrl+Shift: rename old database to avoid import prompt in the future")) - { - Eligibility.RenameOldDatabase(); - // TODO: notify success as this changes the status - } - } - } - - private void DrawImportStatus() - { - // TODO: pretty - if (Importer == null) - return; - - var importStart = Importer.ImportStart; - var importEnd = Importer.ImportComplete; - var total = Importer.ImportCount; - var successful = Importer!.SuccessfulMessages; - var failed = Importer.FailedMessages; - var remaining = Importer.RemainingMessages; - - if (importEnd != null) - { - ImGui.Text($"Completed migration in {Duration(importStart, importEnd.Value)}"); - ImGui.Text($"Successfully imported: {successful} messages"); - ImGui.Text($"Failed to import: {failed} messages"); - ImGui.Text($"Unaccounted for: {remaining}"); - ImGui.Text("See logs for more details: /xllog"); - return; - } - - // TODO: implement Importer.MaxMessageRate slider in UI, values 0 (infinity) => 10000 - - ImGui.Text($"Importing messages... {Importer.Progress:P}%"); - ImGui.Text($"Duration: {Duration(importStart, Environment.TickCount64)}"); - ImGui.Text($"Successfully imported: {successful} messages"); - ImGui.Text($"Failed to import: {failed} messages"); - ImGui.Text($"Progress: {Importer.ProcessedMessages}/{total} messages"); - ImGui.Text($"Remaining: {remaining} messages"); - ImGui.Text($"Messages per second: {Importer.CurrentMessageRate}"); - ImGui.Text($"Estimated time remaining: {Importer.EstimatedTimeRemaining}"); - ImGui.Text("See logs for more details: /xllog"); - - // TODO: this doesn't render properly - ImGui.ProgressBar(Importer.Progress, new Vector2(0.0f, 0.0f), $"{Importer.Progress:P}%"); - - if (ImGuiUtil.CtrlShiftButton("Cancel import", "Ctrl+Shift: cancel import and close window")) - { - // TODO: This currently crashes the whole game because we don't ask - // the importer thread to stop and wait for it to stop before - // disposing it. - // See LegacyMessageImporter.Dispose() for more details. - /* - Importer.Dispose(); - Importer = null; - Eligibility = LegacyMessageImporterEligibility.CheckEligibility(); - */ - } - } - - private static TimeSpan Duration(long startTicks, long endTicks) - { - return endTicks < startTicks ? TimeSpan.Zero : TimeSpan.FromTicks(endTicks - startTicks); - } -} diff --git a/ChatTwo/Ui/LegacyMessageImporterWindow.cs b/ChatTwo/Ui/LegacyMessageImporterWindow.cs new file mode 100644 index 0000000..51751b8 --- /dev/null +++ b/ChatTwo/Ui/LegacyMessageImporterWindow.cs @@ -0,0 +1,225 @@ +using System.Numerics; +using ChatTwo.Util; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Components; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.ImGuiNotification.EventArgs; +using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Interface.Windowing; +using ImGuiNET; + +namespace ChatTwo.Ui; + +internal class LegacyMessageImporterWindow : Window +{ + private readonly Plugin Plugin; + private readonly MessageStore _store; + + private LegacyMessageImporterEligibility Eligibility { get; set; } + private LegacyMessageImporter? Importer { get; set; } + + internal LegacyMessageImporterWindow(Plugin plugin) : base("Chat 2 Legacy Importer###chat2-legacy-importer") + { + Plugin = plugin; + + Flags = ImGuiWindowFlags.NoResize; + Size = new Vector2(500, 400); + + _store = plugin.MessageManager.Store; + Eligibility = LegacyMessageImporterEligibility.CheckEligibility(); + LogAndNotify(); + } + + public void Dispose() + { + Importer?.Dispose(); + } + + private void NotificationClicked(INotificationClickArgs args) + { + IsOpen = true; + args.Notification.DismissNow(); + } + + private void LogAndNotify() + { + Plugin.Log.Info($"[Migration] Checked migration eligibility: {Eligibility.Status} - '{Eligibility.AdditionalIneligibilityInfo}'"); + + switch (Eligibility.Status) + { + case LegacyMessageImporterEligibilityStatus.Eligible: + { + var notification = Plugin.Notification.AddNotification(new Notification + { + // The user needs to dismiss this for it to go away. + Type = NotificationType.Info, + InitialDuration = TimeSpan.FromHours(24), + Title = "Chat2 Migration", + Content = "Import messages from old database into new database?\nClick for more information.", + Minimized = false, + }); + + notification.Click += NotificationClicked; + break; + } + + case LegacyMessageImporterEligibilityStatus.IneligibleLiteDbFailed: + { + var notification = Plugin.Notification.AddNotification(new Notification + { + Type = NotificationType.Warning, + InitialDuration = TimeSpan.FromMinutes(1), + Title = "Chat2 Migration", + Content = "Migration is not possible because the old database could not be opened.\nClick for more information.", + Minimized = false, + }); + + notification.Click += NotificationClicked; + break; + } + } + } + + public override void Draw() + { + if (Importer != null) + { + DrawImportStatus(); + return; + } + + if (Eligibility.Status == LegacyMessageImporterEligibilityStatus.Eligible) + DrawEligible(); + else + DrawIneligible(); + } + + private void DrawEligible() + { + ImGui.TextWrapped("Import database messages from legacy LiteDB database to SQLite database?"); + ImGui.Text($"Message count: {Eligibility.MessageCount:N0}"); + ImGui.Text($"Database size: {StringUtil.BytesToString(Eligibility.DatabaseSizeBytes)}"); + + ImGui.Spacing(); + + var colorNormal = new Vector4(0.0f, 0.70f, 0.0f, 1.0f); + var colorHovered = new Vector4(0.059f, 0.49f, 0.0f, 1.0f); + using (ImRaii.PushColor(ImGuiCol.Button, colorNormal)) + using (ImRaii.PushColor(ImGuiCol.ButtonHovered, colorHovered)) + { + if (ImGui.Button("Yes, import messages")) + { + // Next draw call will run DrawImportStatus(). + Importer = Eligibility.StartImport(_store, plugin: Plugin); + return; + } + } + + ImGui.SameLine(); + + if (ImGuiUtil.CtrlShiftButtonColored("No, do not import messages", "Ctrl+Shift: renames old database to avoid prompting again")) + { + Eligibility.RenameOldDatabase(); + IsOpen = false; + } + } + + private void DrawIneligible() + { + ImGui.Text("Your legacy LiteDB database is not eligible for import:"); + switch (Eligibility.Status) + { + case LegacyMessageImporterEligibilityStatus.IneligibleOriginalDbNotExists: + ImGui.Text("The old database could not be found."); + break; + case LegacyMessageImporterEligibilityStatus.IneligibleMigrationDbExists: + ImGui.Text("The migration process was already started."); + break; + case LegacyMessageImporterEligibilityStatus.IneligibleLiteDbFailed: + ImGui.Text("The old database could not be opened."); + break; + case LegacyMessageImporterEligibilityStatus.IneligibleNoMessages: + ImGui.Text("The old database contains no messages."); + break; + case LegacyMessageImporterEligibilityStatus.Eligible: + default: + throw new ArgumentOutOfRangeException(); + } + + if (!string.IsNullOrWhiteSpace(Eligibility.AdditionalIneligibilityInfo)) + ImGui.Text(Eligibility.AdditionalIneligibilityInfo); + + // LiteDB failures notify the user, so give them a chance to rename the + // database to avoid prompting again. + if (Eligibility.Status == LegacyMessageImporterEligibilityStatus.IneligibleLiteDbFailed) + { + if (ImGuiUtil.CtrlShiftButton("Rename old database", "Ctrl+Shift: rename old database to avoid import prompt in the future")) + { + if (Eligibility.RenameOldDatabase()) + WrapperUtil.AddNotification("Successfully renamed the old database.", NotificationType.Success); + else + WrapperUtil.AddNotification("Rename failed, please check /xllog for more information.", NotificationType.Error); + } + } + } + + private void DrawImportStatus() + { + if (Importer == null) + return; + + if (Importer.ImportComplete != null) + { + ImGui.TextUnformatted($"Completed migration in {Duration(Importer.ImportStart, Importer.ImportComplete.Value):g}"); + ImGui.TextUnformatted($"Successfully imported: {Importer.SuccessfulMessages} messages"); + ImGui.TextUnformatted($"Failed to import: {Importer.FailedMessages} messages"); + ImGui.TextUnformatted($"Unaccounted for: {Importer.RemainingMessages}"); + ImGui.TextUnformatted("See logs for more details: /xllog"); + + ImGui.Spacing(); + + if (ImGui.Button("Finish")) + IsOpen = false; + + return; + } + + ImGui.TextUnformatted($"Importing messages ... {Importer.Progress:P}"); + ImGuiHelpers.ScaledDummy(10.0f); + + ImGui.TextUnformatted($"Duration: {Duration(Importer.ImportStart, Environment.TickCount64):g}"); + ImGui.TextUnformatted($"Progress: {Importer.ProcessedMessages}/{Importer.ImportCount} messages ({Importer.FailedMessages} failed)"); + ImGuiHelpers.ScaledDummy(10.0f); + + var width = ImGui.GetContentRegionAvail().X / 2; + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Import speed:"); + ImGui.SameLine(); + ImGui.SetNextItemWidth(width); + ImGui.SliderInt("##speedSlider", ref Importer.MaxMessageRate, 1, 10000, "%d m/sec", ImGuiSliderFlags.AlwaysClamp); + ImGui.TextUnformatted($"Current speed: {Importer.CurrentMessageRate:N0} m/sec"); + ImGui.TextUnformatted($"Estimated time remaining: {Importer.EstimatedTimeRemaining:g}"); + ImGui.TextUnformatted("See logs for more details: /xllog"); + ImGuiHelpers.ScaledDummy(10.0f); + + ImGui.ProgressBar(Importer.Progress, new Vector2(-1, 0), $"{Importer.Progress:P}"); + ImGui.Spacing(); + + if (ImGuiUtil.CtrlShiftButton("Cancel import", "Ctrl+Shift: cancel import and close window")) + { + Task.Run(async () => + { + await Importer.DisposeAsync(); + Importer = null; + Eligibility = LegacyMessageImporterEligibility.CheckEligibility(); + }); + } + } + + private static TimeSpan Duration(long startTicks, long endTicks) + { + return endTicks < startTicks ? TimeSpan.Zero : TimeSpan.FromMilliseconds(endTicks - startTicks); + } +} diff --git a/ChatTwo/Ui/SettingsTabs/Database.cs b/ChatTwo/Ui/SettingsTabs/Database.cs index aa75cb4..67c4e63 100755 --- a/ChatTwo/Ui/SettingsTabs/Database.cs +++ b/ChatTwo/Ui/SettingsTabs/Database.cs @@ -108,11 +108,11 @@ internal sealed class Database : ISettingsTab ImGuiUtil.HelpText(string.Format(Language.Options_Database_Metadata_Size, StringUtil.BytesToString(DatabaseSize))); if (ImGui.IsItemHovered()) - ImGui.SetTooltip(DatabaseSize.ToString("N0") + "B"); + ImGui.SetTooltip(StringUtil.BytesToString(DatabaseSize)); ImGuiUtil.HelpText(string.Format(Language.Options_Database_Metadata_LogSize, StringUtil.BytesToString(DatabaseLogSize))); if (ImGui.IsItemHovered()) - ImGui.SetTooltip(DatabaseLogSize.ToString("N0") + "B"); + ImGui.SetTooltip(StringUtil.BytesToString(DatabaseLogSize)); ImGuiUtil.HelpText(string.Format(Language.Options_Database_Metadata_MessageCount, DatabaseMessageCount)); diff --git a/ChatTwo/Util/ImGuiUtil.cs b/ChatTwo/Util/ImGuiUtil.cs index 4842750..e96d8fc 100755 --- a/ChatTwo/Util/ImGuiUtil.cs +++ b/ChatTwo/Util/ImGuiUtil.cs @@ -5,6 +5,7 @@ using Dalamud.Game.Text.SeStringHandling; using Dalamud.Interface; using Dalamud.Interface.Style; using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; using ImGuiNET; namespace ChatTwo.Util; @@ -262,20 +263,39 @@ internal static class ImGuiUtil internal static bool CtrlShiftButton(string label, string tooltip = "") { - var io = ImGui.GetIO(); - var ctrlShiftHeld = io.KeyCtrl && io.KeyShift; - if (!ctrlShiftHeld) ImGui.BeginDisabled(); + var ctrlShiftHeld = ImGui.GetIO() is { KeyCtrl: true, KeyShift: true }; + if (!ctrlShiftHeld) + ImGui.BeginDisabled(); + var ret = ImGui.Button(label) && ctrlShiftHeld; - if (!ctrlShiftHeld) ImGui.EndDisabled(); - if (!string.IsNullOrEmpty(tooltip) && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) { - ImGui.BeginTooltip(); - ImGui.TextUnformatted(tooltip); - ImGui.EndTooltip(); - } + + if (!ctrlShiftHeld) + ImGui.EndDisabled(); + + if (!string.IsNullOrEmpty(tooltip) && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + ImGui.SetTooltip(tooltip); return ret; } + internal static bool CtrlShiftButtonColored(string label, string tooltip = "") + { + var ctrlShiftHeld = ImGui.GetIO() is { KeyCtrl: true, KeyShift: true }; + + var colorNormal = new Vector4(0.780f, 0.245f, 0.245f, 1.0f); + var colorHovered = new Vector4(0.7f, 0.0f, 0.0f, 1.0f); + using (ImRaii.PushColor(ImGuiCol.Button, colorNormal)) + using (ImRaii.PushColor(ImGuiCol.ButtonHovered, colorHovered)) + { + var ret = ImGui.Button(label) && ctrlShiftHeld; + + if (!string.IsNullOrEmpty(tooltip) && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + ImGui.SetTooltip(tooltip); + + return ret; + } + } + internal static bool TryToImGui(this VirtualKey key, out ImGuiKey result) { result = key switch {