// 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.Generic; using System.Linq; using System.Runtime.InteropServices; using System.Threading.Tasks; using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers; using Microsoft.CmdPal.Ext.ClipboardHistory.Models; using Microsoft.CommandPalette.Extensions.Toolkit; using Windows.ApplicationModel.DataTransfer; using Windows.Data.Html; using Windows.Graphics.Imaging; using Windows.Storage; using Windows.Storage.Streams; using Windows.System; namespace Microsoft.CmdPal.Ext.ClipboardHistory; internal static class ClipboardHelper { private static readonly HashSet ImageFileTypes = new(StringComparer.InvariantCultureIgnoreCase) { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico", ".svg" }; private static readonly (string DataFormat, ClipboardFormat ClipboardFormat)[] DataFormats = [ (StandardDataFormats.Text, ClipboardFormat.Text), (StandardDataFormats.Html, ClipboardFormat.Html), (StandardDataFormats.Bitmap, ClipboardFormat.Image), ]; private static readonly ClipboardThreadQueue ClipboardThreadQueue = new ClipboardThreadQueue(); internal static async Task GetAvailableClipboardFormatsAsync(DataPackageView clipboardData) { var availableClipboardFormats = DataFormats.Aggregate( ClipboardFormat.None, (result, formatPair) => clipboardData.Contains(formatPair.DataFormat) ? (result | formatPair.ClipboardFormat) : result); if (clipboardData.Contains(StandardDataFormats.StorageItems)) { var storageItems = await clipboardData.GetStorageItemsAsync(); if (storageItems.Count == 1 && storageItems.Single() is StorageFile file && ImageFileTypes.Contains(file.FileType)) { availableClipboardFormats |= ClipboardFormat.ImageFile; } } return availableClipboardFormats; } internal static void SetClipboardTextContent(string text) { if (!string.IsNullOrEmpty(text)) { DataPackage output = new(); output.SetText(text); try { ClipboardThreadQueue.EnqueueTask(() => { Clipboard.SetContent(output); Flush(); ExtensionHost.LogMessage(new LogMessage() { Message = "Copied text to clipboard" }); }); } catch (COMException ex) { ExtensionHost.LogMessage($"Error: {ex.HResult}\n{ex.Source}\n{ex.StackTrace}"); } } } private static bool Flush() { // 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. ClipboardThreadQueue.EnqueueTask(() => { const int maxAttempts = 5; for (var i = 1; i <= maxAttempts; i++) { try { Clipboard.Flush(); return; } catch (Exception ex) { if (i == maxAttempts) { ExtensionHost.LogMessage(new LogMessage() { Message = $"{nameof(Clipboard)}.{nameof(Flush)}() failed: {ex}", }); } } } }); // 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); internal static async Task SetClipboardFileContentAsync(string fileName) { var storageFile = await StorageFile.GetFileFromPathAsync(fileName); DataPackage output = new(); output.SetStorageItems([storageFile]); ClipboardThreadQueue.EnqueueTask(() => Clipboard.SetContent(output)); await FlushAsync(); } internal static void SetClipboardImageContent(RandomAccessStreamReference image) { ExtensionHost.LogMessage(new LogMessage() { Message = "Copied image to clipboard" }); if (image is not null) { DataPackage output = new(); output.SetBitmap(image); ClipboardThreadQueue.EnqueueTask(() => Clipboard.SetContentWithOptions(output, null)); Flush(); } } internal static void SetClipboardContent(ClipboardItem clipboardItem, ClipboardFormat clipboardFormat) { switch (clipboardFormat) { case ClipboardFormat.Text: if (clipboardItem.Content is null) { ExtensionHost.LogMessage(new LogMessage() { Message = "No valid clipboard content" }); return; } else { SetClipboardTextContent(clipboardItem.Content); } break; case ClipboardFormat.Image: if (clipboardItem.ImageData is null) { ExtensionHost.LogMessage(new LogMessage() { Message = "No valid clipboard content" }); return; } else { SetClipboardImageContent(clipboardItem.ImageData); } break; default: ExtensionHost.LogMessage(new LogMessage { Message = "Unsupported clipboard format." }); break; } } // Function to send a single key event private static void SendSingleKeyboardInput(short keyCode, uint keyStatus) { var ignoreKeyEventFlag = (UIntPtr)0x5555; var inputShift = new NativeMethods.INPUT { type = NativeMethods.INPUTTYPE.INPUT_KEYBOARD, data = new NativeMethods.InputUnion { ki = new NativeMethods.KEYBDINPUT { wVk = keyCode, dwFlags = keyStatus, // Any keyevent with the extraInfo set to this value will be ignored by the keyboard hook and sent to the system instead. dwExtraInfo = ignoreKeyEventFlag, }, }, }; var inputs = new NativeMethods.INPUT[] { inputShift }; _ = NativeMethods.SendInput(1, inputs, NativeMethods.INPUT.Size); } internal static void SendPasteKeyCombination() { ExtensionHost.LogMessage(new LogMessage() { Message = "Sending paste keys..." }); SendSingleKeyboardInput((short)VirtualKey.LeftControl, (uint)NativeMethods.KeyEventF.KeyUp); SendSingleKeyboardInput((short)VirtualKey.RightControl, (uint)NativeMethods.KeyEventF.KeyUp); SendSingleKeyboardInput((short)VirtualKey.LeftWindows, (uint)NativeMethods.KeyEventF.KeyUp); SendSingleKeyboardInput((short)VirtualKey.RightWindows, (uint)NativeMethods.KeyEventF.KeyUp); SendSingleKeyboardInput((short)VirtualKey.LeftShift, (uint)NativeMethods.KeyEventF.KeyUp); SendSingleKeyboardInput((short)VirtualKey.RightShift, (uint)NativeMethods.KeyEventF.KeyUp); SendSingleKeyboardInput((short)VirtualKey.LeftMenu, (uint)NativeMethods.KeyEventF.KeyUp); SendSingleKeyboardInput((short)VirtualKey.RightMenu, (uint)NativeMethods.KeyEventF.KeyUp); // Send Ctrl + V SendSingleKeyboardInput((short)VirtualKey.Control, (uint)NativeMethods.KeyEventF.KeyDown); SendSingleKeyboardInput((short)VirtualKey.V, (uint)NativeMethods.KeyEventF.KeyDown); SendSingleKeyboardInput((short)VirtualKey.V, (uint)NativeMethods.KeyEventF.KeyUp); SendSingleKeyboardInput((short)VirtualKey.Control, (uint)NativeMethods.KeyEventF.KeyUp); ExtensionHost.LogMessage(new LogMessage() { Message = "Paste sent" }); } internal static async Task GetClipboardTextOrHtmlTextAsync(DataPackageView clipboardData) { if (clipboardData.Contains(StandardDataFormats.Text)) { return await clipboardData.GetTextAsync(); } else if (clipboardData.Contains(StandardDataFormats.Html)) { var html = await clipboardData.GetHtmlFormatAsync(); return HtmlUtilities.ConvertToText(html); } else { return string.Empty; } } internal static async Task GetClipboardHtmlContentAsync(DataPackageView clipboardData) => clipboardData.Contains(StandardDataFormats.Html) ? await clipboardData.GetHtmlFormatAsync() : string.Empty; internal static async Task GetClipboardImageContentAsync(DataPackageView clipboardData) { using var stream = await GetClipboardImageStreamAsync(clipboardData); if (stream is not null) { var decoder = await BitmapDecoder.CreateAsync(stream); return await decoder.GetSoftwareBitmapAsync(); } return null; } private static async Task GetClipboardImageStreamAsync(DataPackageView clipboardData) { if (clipboardData.Contains(StandardDataFormats.StorageItems)) { var storageItems = await clipboardData.GetStorageItemsAsync(); var file = storageItems.Count == 1 ? storageItems[0] as StorageFile : null; if (file is not null) { return await file.OpenReadAsync(); } } if (clipboardData.Contains(StandardDataFormats.Bitmap)) { var bitmap = await clipboardData.GetBitmapAsync(); return await bitmap.OpenReadAsync(); } return null; } }