diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs index ba4a298665..4917dc5f0e 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs @@ -8,6 +8,7 @@ using Microsoft.CmdPal.Common.Services; using Microsoft.CmdPal.Ext.Apps; using Microsoft.CmdPal.Ext.Bookmarks; using Microsoft.CmdPal.Ext.Calc; +using Microsoft.CmdPal.Ext.ClipboardHistory; using Microsoft.CmdPal.Ext.Indexer; using Microsoft.CmdPal.Ext.Registry; using Microsoft.CmdPal.Ext.Shell; @@ -100,8 +101,7 @@ public partial class App : Application services.AddSingleton(); services.AddSingleton(); - // TODO GH #527 re-enable the clipboard commands - // services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/ClipboardHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/ClipboardHelper.cs index bbfec3c491..100d23e9f0 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/ClipboardHelper.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/ClipboardHelper.cs @@ -30,6 +30,8 @@ internal static class ClipboardHelper (StandardDataFormats.Bitmap, ClipboardFormat.Image), ]; + private static readonly ClipboardThreadQueue ClipboardThreadQueue = new ClipboardThreadQueue(); + internal static async Task GetAvailableClipboardFormatsAsync(DataPackageView clipboardData) { var availableClipboardFormats = DataFormats.Aggregate( @@ -58,9 +60,12 @@ internal static class ClipboardHelper try { // Clipboard.SetContentWithOptions(output, null); - Clipboard.SetContent(output); - Flush(); - ExtensionHost.LogMessage(new LogMessage() { Message = "Copied text to clipboard" }); + ClipboardThreadQueue.EnqueueTask(() => + { + Clipboard.SetContent(output); + Flush(); + ExtensionHost.LogMessage(new LogMessage() { Message = "Copied text to clipboard" }); + }); } catch (COMException ex) { @@ -74,27 +79,32 @@ internal static class ClipboardHelper // TODO(stefan): For some reason Flush() fails from time to time when directly activated via hotkey. // Calling inside a loop makes it work. // Exception is: The operation is not permitted because the calling application is not the owner of the data on the clipboard. - const int maxAttempts = 5; - for (var i = 1; i <= maxAttempts; i++) + ClipboardThreadQueue.EnqueueTask(() => { - try + const int maxAttempts = 5; + + for (var i = 1; i <= maxAttempts; i++) { - Task.Run(Clipboard.Flush).Wait(); - return true; - } - catch (Exception ex) - { - if (i == maxAttempts) + try { - ExtensionHost.LogMessage(new LogMessage() + Task.Run(Clipboard.Flush).Wait(); + return; + } + catch (Exception ex) + { + if (i == maxAttempts) { - Message = $"{nameof(Clipboard)}.{nameof(Flush)}() failed: {ex}", - }); + ExtensionHost.LogMessage(new LogMessage() + { + Message = $"{nameof(Clipboard)}.{nameof(Flush)}() failed: {ex}", + }); + } } } - } + }); - return false; + // We cannot get the real result of the Flush() call here, as it is executed in a different thread. + return true; } private static async Task FlushAsync() => await Task.Run(Flush); @@ -105,7 +115,7 @@ internal static class ClipboardHelper DataPackage output = new(); output.SetStorageItems([storageFile]); - Clipboard.SetContent(output); + ClipboardThreadQueue.EnqueueTask(() => Clipboard.SetContent(output)); await FlushAsync(); } @@ -118,7 +128,7 @@ internal static class ClipboardHelper { DataPackage output = new(); output.SetBitmap(image); - Clipboard.SetContentWithOptions(output, null); + ClipboardThreadQueue.EnqueueTask(() => Clipboard.SetContentWithOptions(output, null)); Flush(); } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/ClipboardThreadScheduler.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/ClipboardThreadScheduler.cs new file mode 100644 index 0000000000..0f36f66453 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/ClipboardThreadScheduler.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Concurrent; +using System.Threading; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers; + +public partial class ClipboardThreadQueue : IDisposable +{ + private readonly Thread _thread; + private readonly ConcurrentQueue _taskQueue = new ConcurrentQueue(); + private readonly AutoResetEvent _taskAvailable = new AutoResetEvent(false); + private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource(); + + public ClipboardThreadQueue() + { + _thread = new Thread(() => + { + var hr = NativeMethods.CoInitialize(IntPtr.Zero); + if (hr != 0) + { + ExtensionHost.LogMessage($"CoInitialize failed with HRESULT: {hr}"); + } + + while (true) + { + _taskAvailable.WaitOne(); + + if (cancellationToken.IsCancellationRequested) + { + break; + } + + while (_taskQueue.TryDequeue(out var task)) + { + try + { + task(); + } + catch (Exception ex) + { + ExtensionHost.LogMessage($"Error executing task in ClipboardThreadQueue: {ex.Message}"); + } + } + } + + NativeMethods.CoUninitialize(); + }); + + _thread.SetApartmentState(ApartmentState.STA); + _thread.IsBackground = true; + _thread.Start(); + } + + public void EnqueueTask(Action task) + { + _taskQueue.Enqueue(task); + _taskAvailable.Set(); + } + + public void Dispose() + { + cancellationToken.Cancel(); + _taskAvailable.Set(); + _thread.Join(); // Wait for the thread to finish processing tasks + + _taskAvailable.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/NativeMethods.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/NativeMethods.cs index f4b6089229..1e0a46f030 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/NativeMethods.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/NativeMethods.cs @@ -8,7 +8,7 @@ using Windows.Foundation; namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers; -internal static class NativeMethods +public static partial class NativeMethods { [StructLayout(LayoutKind.Sequential)] [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")] @@ -98,4 +98,10 @@ internal static class NativeMethods [DllImport("user32.dll")] internal static extern bool GetCursorPos(out PointInter lpPoint); + + [LibraryImport("ole32.dll")] + internal static partial int CoInitialize(IntPtr pvReserved); + + [LibraryImport("ole32.dll")] + internal static partial void CoUninitialize(); }