[cmdpal] Re-enable Clipboard History extention (#39800)
Some checks failed
Spell checking / Check Spelling (push) Has been cancelled
Spell checking / Report (Push) (push) Has been cancelled
Spell checking / Report (PR) (push) Has been cancelled
Spell checking / Update PR (push) Has been cancelled

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
Due to some windows sdk bugs, we can not use those API in main thread.
So, create a separate thread for clipboard.

history:

![image](https://github.com/user-attachments/assets/4da5a4eb-5d9d-475c-ab13-a2d585d2fffc)

success to paste to chat:

![image](https://github.com/user-attachments/assets/3fef43e5-4fc5-492c-b81e-599a9746d413)

![image](https://github.com/user-attachments/assets/1f4232bb-de76-40e5-96dd-43beb0ca8423)



<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] **Closes:** #38344
- [ ] **Communication:** I've discussed this with core contributors
already. If work hasn't been agreed, this work might be rejected
- [x] **Tests:** Added/updated and all pass
- [x] **Localization:** All end user facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

---------

Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
This commit is contained in:
Yu Leng 2025-06-05 15:23:16 +08:00 committed by GitHub
parent df8ace3ab6
commit dd2e7d17f9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 112 additions and 22 deletions

View File

@ -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<ICommandProvider, IndexerCommandsProvider>();
services.AddSingleton<ICommandProvider, BookmarksCommandProvider>();
// TODO GH #527 re-enable the clipboard commands
// services.AddSingleton<ICommandProvider, ClipboardHistoryCommandsProvider>();
services.AddSingleton<ICommandProvider, ClipboardHistoryCommandsProvider>();
services.AddSingleton<ICommandProvider, WindowWalkerCommandsProvider>();
services.AddSingleton<ICommandProvider, WebSearchCommandsProvider>();

View File

@ -30,6 +30,8 @@ internal static class ClipboardHelper
(StandardDataFormats.Bitmap, ClipboardFormat.Image),
];
private static readonly ClipboardThreadQueue ClipboardThreadQueue = new ClipboardThreadQueue();
internal static async Task<ClipboardFormat> 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<bool> 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();
}

View File

@ -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<Action> _taskQueue = new ConcurrentQueue<Action>();
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);
}
}

View File

@ -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();
}