diff --git a/PowerToys.sln b/PowerToys.sln index d9b3de4d4769..cabb485ee5fd 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -674,6 +674,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.UI", "src\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.UI.ViewModels", "src\modules\cmdpal\Microsoft.CmdPal.UI.ViewModels\Microsoft.CmdPal.UI.ViewModels.csproj", "{C66020D1-CB10-4CF7-8715-84C97FD5E5E2}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.ClipboardHistory", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.ClipboardHistory\Microsoft.CmdPal.Ext.ClipboardHistory.csproj", "{79775343-7A3D-445D-9104-3DD5B2893DF9}" +EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CmdPalModuleInterface", "src\modules\cmdpal\CmdPalModuleInterface\CmdPalModuleInterface.vcxproj", "{0ADEB797-C8C7-4FFA-ACD5-2AF6CAD7ECD8}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PokedexExtension", "src\modules\cmdpal\Exts\PokedexExtension\PokedexExtension.csproj", "{D8DD2E06-7956-4673-95E7-F395AB5A5485}" @@ -3146,6 +3148,18 @@ Global {C66020D1-CB10-4CF7-8715-84C97FD5E5E2}.Release|x64.Build.0 = Release|x64 {C66020D1-CB10-4CF7-8715-84C97FD5E5E2}.Release|x86.ActiveCfg = Release|x64 {C66020D1-CB10-4CF7-8715-84C97FD5E5E2}.Release|x86.Build.0 = Release|x64 + {79775343-7A3D-445D-9104-3DD5B2893DF9}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {79775343-7A3D-445D-9104-3DD5B2893DF9}.Debug|ARM64.Build.0 = Debug|ARM64 + {79775343-7A3D-445D-9104-3DD5B2893DF9}.Debug|x64.ActiveCfg = Debug|x64 + {79775343-7A3D-445D-9104-3DD5B2893DF9}.Debug|x64.Build.0 = Debug|x64 + {79775343-7A3D-445D-9104-3DD5B2893DF9}.Debug|x86.ActiveCfg = Debug|x64 + {79775343-7A3D-445D-9104-3DD5B2893DF9}.Debug|x86.Build.0 = Debug|x64 + {79775343-7A3D-445D-9104-3DD5B2893DF9}.Release|ARM64.ActiveCfg = Release|ARM64 + {79775343-7A3D-445D-9104-3DD5B2893DF9}.Release|ARM64.Build.0 = Release|ARM64 + {79775343-7A3D-445D-9104-3DD5B2893DF9}.Release|x64.ActiveCfg = Release|x64 + {79775343-7A3D-445D-9104-3DD5B2893DF9}.Release|x64.Build.0 = Release|x64 + {79775343-7A3D-445D-9104-3DD5B2893DF9}.Release|x86.ActiveCfg = Release|x64 + {79775343-7A3D-445D-9104-3DD5B2893DF9}.Release|x86.Build.0 = Release|x64 {0ADEB797-C8C7-4FFA-ACD5-2AF6CAD7ECD8}.Debug|ARM64.ActiveCfg = Debug|ARM64 {0ADEB797-C8C7-4FFA-ACD5-2AF6CAD7ECD8}.Debug|ARM64.Build.0 = Debug|ARM64 {0ADEB797-C8C7-4FFA-ACD5-2AF6CAD7ECD8}.Debug|x64.ActiveCfg = Debug|x64 @@ -3622,6 +3636,7 @@ Global {7520A2FE-00A2-49B8-83ED-DB216E874C04} = {3846508C-77EB-4034-A702-F8BB263C4F79} {8FBDABA4-40EE-4C0E-9BC8-2F6444A6EF90} = {7520A2FE-00A2-49B8-83ED-DB216E874C04} {C66020D1-CB10-4CF7-8715-84C97FD5E5E2} = {7520A2FE-00A2-49B8-83ED-DB216E874C04} + {79775343-7A3D-445D-9104-3DD5B2893DF9} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} {0ADEB797-C8C7-4FFA-ACD5-2AF6CAD7ECD8} = {3846508C-77EB-4034-A702-F8BB263C4F79} {D8DD2E06-7956-4673-95E7-F395AB5A5485} = {071E18A4-A530-46B8-AB7D-B862EE55E24E} {8ABE2195-7514-425E-9A89-685FA42CEFC3} = {071E18A4-A530-46B8-AB7D-B862EE55E24E} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/ClipboardHistoryCommandsProvider.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/ClipboardHistoryCommandsProvider.cs new file mode 100644 index 000000000000..2d3841d500fc --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/ClipboardHistoryCommandsProvider.cs @@ -0,0 +1,32 @@ +// 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 Microsoft.CmdPal.Ext.ClipboardHistory.Pages; +using Microsoft.CmdPal.Extensions; +using Microsoft.CmdPal.Extensions.Helpers; + +namespace Microsoft.CmdPal.Ext.ClipboardHistory; + +public partial class ClipboardHistoryCommandsProvider : CommandProvider +{ + private readonly ListItem _clipboardHistoryListItem; + + public ClipboardHistoryCommandsProvider() + { + _clipboardHistoryListItem = new ListItem(new ClipboardHistoryListPage()) + { + Title = "Search Clipboard History", + Icon = new("ClipboardHistory"), + }; + + DisplayName = $"Settings"; + Icon = new("ClipboardHistory"); + } + + public override IListItem[] TopLevelCommands() + { + return [_clipboardHistoryListItem]; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Commands/CopyCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Commands/CopyCommand.cs new file mode 100644 index 000000000000..cd9ae79e83b4 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Commands/CopyCommand.cs @@ -0,0 +1,40 @@ +// 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.Diagnostics; +using System.Linq; +using System.Resources; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CmdPal.Ext.ClipboardHistory.Models; +using Microsoft.CmdPal.Extensions; +using Microsoft.CmdPal.Extensions.Helpers; +using Windows.ApplicationModel.DataTransfer; +using Windows.Networking.NetworkOperators; +using Windows.UI; + +namespace Microsoft.CmdPal.Ext.ClipboardHistory.Commands; + +internal sealed partial class CopyCommand : InvokableCommand +{ + private readonly ClipboardItem _clipboardItem; + private readonly ClipboardFormat _clipboardFormat; + + internal CopyCommand(ClipboardItem clipboardItem, ClipboardFormat clipboardFormat) + { + _clipboardItem = clipboardItem; + _clipboardFormat = clipboardFormat; + Name = "Copy"; + Icon = new("\xE8C8"); // Copy icon + } + + public override CommandResult Invoke() + { + ClipboardHelper.SetClipboardContent(_clipboardItem, _clipboardFormat); + + return CommandResult.Dismiss(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Commands/PasteCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Commands/PasteCommand.cs new file mode 100644 index 000000000000..1fe8da9fea6c --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Commands/PasteCommand.cs @@ -0,0 +1,67 @@ +// 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; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Resources; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers; +using Microsoft.CmdPal.Ext.ClipboardHistory.Models; +using Microsoft.CmdPal.Extensions; +using Microsoft.CmdPal.Extensions.Helpers; +using Windows.ApplicationModel.DataTransfer; +using Windows.Networking.NetworkOperators; +using Windows.System; +using WinRT.Interop; + +namespace Microsoft.CmdPal.Ext.ClipboardHistory.Commands; + +internal sealed partial class PasteCommand : InvokableCommand +{ + [DllImport("user32.dll", SetLastError = true)] + private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); + + private readonly ClipboardItem _clipboardItem; + private readonly ClipboardFormat _clipboardFormat; + + private const int HIDE = 0; + private const int SHOW = 5; + + internal PasteCommand(ClipboardItem clipboardItem, ClipboardFormat clipboardFormat) + { + _clipboardItem = clipboardItem; + _clipboardFormat = clipboardFormat; + Name = "Paste"; + Icon = new("\xE8C8"); // Copy icon + } + + private void HideWindow() + { + var hostHwnd = ExtensionHost.Host.HostingHwnd; + + ShowWindow(new IntPtr((long)hostHwnd), HIDE); + } + + private void ShowWindow() + { + var hostHwnd = ExtensionHost.Host.HostingHwnd; + + ShowWindow(new IntPtr((long)hostHwnd), SHOW); + } + + public override CommandResult Invoke() + { + ClipboardHelper.SetClipboardContent(_clipboardItem, _clipboardFormat); + HideWindow(); + ClipboardHelper.SendPasteKeyCombination(); + Clipboard.DeleteItemFromHistory(_clipboardItem.Item); + + return CommandResult.Dismiss(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/ClipboardHelper.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/ClipboardHelper.cs new file mode 100644 index 000000000000..90b569b14f93 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/ClipboardHelper.cs @@ -0,0 +1,248 @@ +// 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.Threading.Tasks; +using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers; +using Microsoft.CmdPal.Ext.ClipboardHistory.Models; +using Microsoft.CmdPal.Extensions.Helpers; +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), + ]; + + 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) + { + // Logger.LogTrace(); + // TODO GH #108 -- need to figure out how to do equivalent of LogTrace() from PT? + ExtensionHost.LogMessage(new LogMessage() { Message = "This is a test for logging" }); + + if (!string.IsNullOrEmpty(text)) + { + DataPackage output = new(); + output.SetText(text); + Clipboard.SetContentWithOptions(output, null); + + Flush(); + } + } + + 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. + const int maxAttempts = 5; + for (var i = 1; i <= maxAttempts; i++) + { + try + { + Task.Run(Clipboard.Flush).Wait(); + return true; + } + catch (Exception ex) + { + if (i == maxAttempts) + { + // TODO GH #108 -- need to figure out how to do equivalent of LogTrace() from PT? + ExtensionHost.LogMessage(new LogMessage() { Message = ex.ToString() }); + + // Logger.LogError($"{nameof(Clipboard)}.{nameof(Flush)}() failed", ex); + } + } + } + + return false; + } + + 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]); + Clipboard.SetContent(output); + + await FlushAsync(); + } + + internal static void SetClipboardImageContent(RandomAccessStreamReference image) + { + // Logger.LogTrace(); + // TODO GH #108 -- need to figure out how to do equivalent of LogTrace() from PT? + ExtensionHost.LogMessage(new LogMessage() { Message = "This is a test for logging" }); + + if (image is not null) + { + DataPackage output = new(); + output.SetBitmap(image); + Clipboard.SetContentWithOptions(output, null); + + Flush(); + } + } + + internal static void SetClipboardContent(ClipboardItem clipboardItem, ClipboardFormat clipboardFormat) + { + if (clipboardItem.Content == null && clipboardItem.ImageData == null) + { + ExtensionHost.LogMessage(new LogMessage() { Message = "No valid clipboard content" }); + return; + } + + switch (clipboardFormat) + { + case ClipboardFormat.Text: + SetClipboardTextContent(clipboardItem.Content); break; + + case ClipboardFormat.Image: + 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; + + NativeMethods.INPUT 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, + }, + }, + }; + + NativeMethods.INPUT[] inputs = new NativeMethods.INPUT[] { inputShift }; + _ = NativeMethods.SendInput(1, inputs, NativeMethods.INPUT.Size); + } + + internal static void SendPasteKeyCombination() + { + // Logger.LogTrace(); + // TODO GH #108 -- need to figure out how to do equivalent of LogTrace() from PT? + ExtensionHost.LogMessage(new LogMessage() { Message = "This is a test for logging" }); + + 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); + + // Logger.LogInfo("Paste sent"); + // TODO GH #108 -- need to figure out how to do equivalent of LogTrace() from PT? + 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 != 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 != null) + { + return await file.OpenReadAsync(); + } + } + + if (clipboardData.Contains(StandardDataFormats.Bitmap)) + { + var bitmap = await clipboardData.GetBitmapAsync(); + return await bitmap.OpenReadAsync(); + } + + return null; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/NativeMethods.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/NativeMethods.cs new file mode 100644 index 000000000000..50ff3461037c --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/NativeMethods.cs @@ -0,0 +1,101 @@ +// 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.Runtime.InteropServices; +using Windows.Foundation; + +namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers; + +internal static 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")] + internal struct INPUT + { + internal INPUTTYPE type; + internal InputUnion data; + + internal static int Size => Marshal.SizeOf(typeof(INPUT)); + } + + [StructLayout(LayoutKind.Explicit)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")] + internal struct InputUnion + { + [FieldOffset(0)] + internal MOUSEINPUT mi; + [FieldOffset(0)] + internal KEYBDINPUT ki; + [FieldOffset(0)] + internal HARDWAREINPUT hi; + } + + [StructLayout(LayoutKind.Sequential)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")] + internal struct MOUSEINPUT + { + internal int dx; + internal int dy; + internal int mouseData; + internal uint dwFlags; + internal uint time; + internal UIntPtr dwExtraInfo; + } + + [StructLayout(LayoutKind.Sequential)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")] + internal struct KEYBDINPUT + { + internal short wVk; + internal short wScan; + internal uint dwFlags; + internal int time; + internal UIntPtr dwExtraInfo; + } + + [StructLayout(LayoutKind.Sequential)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")] + internal struct HARDWAREINPUT + { + internal int uMsg; + internal short wParamL; + internal short wParamH; + } + + internal enum INPUTTYPE : uint + { + INPUT_MOUSE = 0, + INPUT_KEYBOARD = 1, + INPUT_HARDWARE = 2, + } + + [Flags] + internal enum KeyEventF + { + KeyDown = 0x0000, + ExtendedKey = 0x0001, + KeyUp = 0x0002, + Unicode = 0x0004, + Scancode = 0x0008, + } + + [DllImport("user32.dll")] + internal static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize); + + [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall, SetLastError = true)] + internal static extern short GetAsyncKeyState(int vKey); + + [StructLayout(LayoutKind.Sequential)] + internal struct PointInter + { + public int X; + public int Y; + + public static explicit operator Point(PointInter point) => new(point.X, point.Y); + } + + [DllImport("user32.dll")] + internal static extern bool GetCursorPos(out PointInter lpPoint); +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Microsoft.CmdPal.Ext.ClipboardHistory.csproj b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Microsoft.CmdPal.Ext.ClipboardHistory.csproj new file mode 100644 index 000000000000..25aad2b84c3b --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Microsoft.CmdPal.Ext.ClipboardHistory.csproj @@ -0,0 +1,13 @@ + + + + Microsoft.CmdPal.Ext.ClipboardHistory + $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal + false + false + + + + + + diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Models/ClipboardFormat.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Models/ClipboardFormat.cs new file mode 100644 index 000000000000..7ba791c930ba --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Models/ClipboardFormat.cs @@ -0,0 +1,18 @@ +// 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; + +namespace Microsoft.CmdPal.Ext.ClipboardHistory.Models; + +[Flags] +public enum ClipboardFormat +{ + None, + Text = 1 << 0, + Html = 1 << 1, + Audio = 1 << 2, + Image = 1 << 3, + ImageFile = 1 << 4, +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Models/ClipboardItem.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Models/ClipboardItem.cs new file mode 100644 index 000000000000..1d4d40077758 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Models/ClipboardItem.cs @@ -0,0 +1,107 @@ +// 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.Globalization; +using Microsoft.CmdPal.Ext.ClipboardHistory.Commands; +using Microsoft.CmdPal.Extensions; +using Microsoft.CmdPal.Extensions.Helpers; +using Windows.ApplicationModel.DataTransfer; +using Windows.Storage.Streams; + +namespace Microsoft.CmdPal.Ext.ClipboardHistory.Models; + +public class ClipboardItem +{ + public string Content { get; set; } + + public ClipboardHistoryItem Item { get; set; } + + public DateTimeOffset Timestamp => Item?.Timestamp ?? DateTimeOffset.MinValue; + + public RandomAccessStreamReference ImageData { get; set; } + + public string GetDataType() + { + // Check if there is valid image data + if (ImageData != null) + { + return "Image"; + } + + // Check if there is valid text content + return !string.IsNullOrEmpty(Content) ? "Text" : "Unknown"; + } + + private bool IsImage() + { + return GetDataType() == "Image"; + } + + private bool IsText() + { + return GetDataType() == "Text"; + } + + public ListItem ToListItem() + { + ListItem listItem; + + if (IsImage()) + { + var iconData = IconData.FromStream(ImageData); + var heroImage = new IconInfo(iconData, iconData); + listItem = new(new CopyCommand(this, ClipboardFormat.Image)) + { + // Placeholder subtitle as there’s no BitmapImage dimensions to retrieve + Title = "Image Data", + Tags = [new Tag() + { + Text = GetDataType(), + } + ], + + Details = new Details() + { + HeroImage = heroImage, // new("\uF0E3"), + Title = GetDataType(), + Body = Timestamp.ToString(CultureInfo.InvariantCulture), + }, + MoreCommands = [ + new CommandContextItem(new PasteCommand(this, ClipboardFormat.Image)) + ], + }; + } + else + { + listItem = IsText() + ? new(new CopyCommand(this, ClipboardFormat.Text)) + { + Title = Content.Length > 20 ? string.Concat(Content.AsSpan(0, 20), "...") : Content, + Tags = [new Tag() + { + Text = GetDataType(), + } + ], + Details = new Details { Title = GetDataType(), Body = $"```text\n{Content}\n```" }, + MoreCommands = [ + new CommandContextItem(new PasteCommand(this, ClipboardFormat.Text)), + ], + } + : new(new NoOpCommand()) + { + Title = "Unknown", + Subtitle = GetDataType(), + Tags = [new Tag() + { + Text = GetDataType(), + } + ], + Details = new Details { Title = GetDataType(), Body = Content }, + }; + } + + return listItem; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Pages/ClipboardHistoryListPage.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Pages/ClipboardHistoryListPage.cs new file mode 100644 index 000000000000..b301b486e25e --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.ClipboardHistory/Pages/ClipboardHistoryListPage.cs @@ -0,0 +1,147 @@ +// 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.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CmdPal.Ext.ClipboardHistory.Models; +using Microsoft.CmdPal.Extensions; +using Microsoft.CmdPal.Extensions.Helpers; +using Microsoft.Win32; +using Windows.ApplicationModel.DataTransfer; +using Windows.Storage.Streams; + +namespace Microsoft.CmdPal.Ext.ClipboardHistory.Pages; + +internal sealed partial class ClipboardHistoryListPage : ListPage +{ + private readonly ObservableCollection clipboardHistory; + private readonly string _defaultIconPath; + + public ClipboardHistoryListPage() + { + clipboardHistory = new ObservableCollection(); + _defaultIconPath = string.Empty; + Icon = new("\uF0E3"); // ClipboardList icon + Name = "Clipboard History"; + ShowDetails = true; + + // Clipboard.ContentChanged += Clipboard_ContentChanged; TODO GH #131 -- fixed in PR #142 -- need to raise the event here and handle clipboard change + } + + private bool IsClipboardHistoryEnabled() + { + var registryKey = @"HKEY_CURRENT_USER\Software\Microsoft\Clipboard\"; + try + { + var enableClipboardHistory = (int)Registry.GetValue(registryKey, "EnableClipboardHistory", false); + return enableClipboardHistory != 0; + } + catch (Exception) + { + return false; + } + } + + private bool IsClipboardHistoryDisabledByGPO() + { + var registryKey = @"HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\System\"; + try + { + var allowClipboardHistory = Registry.GetValue(registryKey, "AllowClipboardHistory", null); + if (allowClipboardHistory != null) + { + return (int)allowClipboardHistory == 0; + } + else + { + return false; + } + } + catch (Exception) + { + return false; + } + } + + private async Task LoadClipboardHistoryAsync() + { + try + { + List items = new(); + + if (Clipboard.IsHistoryEnabled()) + { + var historyItems = await Clipboard.GetHistoryItemsAsync(); + if (historyItems.Status == ClipboardHistoryItemsResultStatus.Success) + { + foreach (var item in historyItems.Items) + { + if (item.Content.Contains(StandardDataFormats.Text)) + { + var text = await item.Content.GetTextAsync(); + items.Add(new ClipboardItem { Content = text, Item = item }); + } + else if (item.Content.Contains(StandardDataFormats.Bitmap)) + { + items.Add(new ClipboardItem { Item = item }); + } + } + } + } + + clipboardHistory.Clear(); + + foreach (var item in items) + { + if (item.Item.Content.Contains(StandardDataFormats.Bitmap)) + { + RandomAccessStreamReference imageReceived = await item.Item.Content.GetBitmapAsync(); + + if (imageReceived != null) + { + item.ImageData = imageReceived; + } + } + + clipboardHistory.Add(item); + } + } +#pragma warning disable CS0168, IDE0059 + catch (Exception ex) + { + // TODO GH #108 We need to figure out some logging + // Logger.LogError("Loading clipboard history failed", ex); + } +#pragma warning restore CS0168, IDE0059 + } + + private async Task GetClipboardHistoryListItems() + { + await LoadClipboardHistoryAsync(); + ListItem[] listItems = new ListItem[clipboardHistory.Count]; + for (var i = 0; i < clipboardHistory.Count; i++) + { + var item = clipboardHistory[i]; + listItems[i] = item.ToListItem(); + } + + return listItems; + } + + public override IListItem[] GetItems() + { + var t = DoGetItems(); + t.ConfigureAwait(false); + return t.Result; + } + + private async Task DoGetItems() + { + return await GetClipboardHistoryListItems(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Commands/CopyRegistryInfoCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Commands/CopyRegistryInfoCommand.cs index b291e5512f1e..d876eb7efc80 100644 --- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Commands/CopyRegistryInfoCommand.cs +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Commands/CopyRegistryInfoCommand.cs @@ -44,6 +44,6 @@ public override CommandResult Invoke() { ClipboardHelper.SetText(_stringToCopy); - return CommandResult.KeepOpen(); + return CommandResult.Dismiss(); } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs index 02bec2921180..e441c7c29ac2 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs @@ -6,6 +6,7 @@ using Microsoft.CmdPal.Ext.Apps.Programs; 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; @@ -87,6 +88,7 @@ private static ServiceProvider ConfigureServices() services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(winget); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj index e10183127168..1ea130a568fb 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj @@ -107,6 +107,7 @@ + diff --git a/src/modules/cmdpal/WindowsCommandPalette/Microsoft.CmdPal.UI.Poc.csproj b/src/modules/cmdpal/WindowsCommandPalette/Microsoft.CmdPal.UI.Poc.csproj new file mode 100644 index 000000000000..c972608adb35 --- /dev/null +++ b/src/modules/cmdpal/WindowsCommandPalette/Microsoft.CmdPal.UI.Poc.csproj @@ -0,0 +1,150 @@ + + + + WinExe + WindowsCommandPalette + app.manifest + win-$(Platform).pubxml + true + true + enable + enable + $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal.Poc + false + false + true + + + + + + + + + + + + + + Microsoft.Terminal.UI + $(OutDir) + + + DISABLE_XAML_GENERATED_MAIN + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + True + + + + + + true + + + + + + + + + + + + MSBuild:Compile + + + + + MSBuild:Compile + + + + + MSBuild:Compile + + + + + + + + + + + diff --git a/src/modules/cmdpal/WindowsCommandPalette/Views/CommandProviderWrapper.cs b/src/modules/cmdpal/WindowsCommandPalette/Views/CommandProviderWrapper.cs new file mode 100644 index 000000000000..bbbe680d39c5 --- /dev/null +++ b/src/modules/cmdpal/WindowsCommandPalette/Views/CommandProviderWrapper.cs @@ -0,0 +1,111 @@ +// 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.Diagnostics; +using System.Runtime.InteropServices; +using Microsoft.CmdPal.Common.Services; +using Microsoft.CmdPal.Extensions; +using Windows.Win32; + +namespace WindowsCommandPalette.Views; + +public sealed class CommandProviderWrapper +{ + public bool IsExtension => extensionWrapper != null; + + private readonly bool isValid; + + private ICommandProvider CommandProvider { get; } + + private readonly IExtensionWrapper? extensionWrapper; + private ICommandItem[] _topLevelItems = []; + private IFallbackCommandItem[] _topLevelFallbacks = []; + + public IEnumerable TopLevelItems => _topLevelItems.Concat(_topLevelFallbacks); + + public CommandProviderWrapper(ICommandProvider provider) + { + CommandProvider = provider; + isValid = true; + CommandProvider.InitializeWithHost(CommandPaletteHost.Instance); + } + + public CommandProviderWrapper(IExtensionWrapper extension) + { + extensionWrapper = extension; + var extensionImpl = extension.GetExtensionObject(); + if (extensionImpl?.GetProvider(ProviderType.Commands) is not ICommandProvider provider) + { + throw new ArgumentException("extension didn't actually implement ICommandProvider"); + } + + CommandProvider = provider; + + // Hook the extension back into us + CommandProvider.InitializeWithHost(CommandPaletteHost.Instance); + + isValid = true; + } + + public async Task LoadTopLevelCommands() + { + if (!isValid) + { + return; + } + + var t = new Task<(ICommandItem[], IFallbackCommandItem[])>(() => + { + try + { + return (CommandProvider.TopLevelCommands(), CommandProvider.FallbackCommands()); + } + catch (COMException e) + { + if (extensionWrapper != null) + { + Debug.WriteLine($"Error loading commands from {extensionWrapper.ExtensionDisplayName}", "error"); + } + + Debug.WriteLine(e.ToString(), "error"); + } + + return ([], []); + }); + t.Start(); + var (commands, fallbacks) = await t.ConfigureAwait(false); + + // On a BG thread here + if (commands != null) + { + _topLevelItems = commands; + } + + if (fallbacks != null) + { + _topLevelFallbacks = fallbacks; + } + } + + public void AllowSetForeground(bool allow) + { + if (!IsExtension) + { + return; + } + + var iextn = extensionWrapper?.GetExtensionObject(); + unsafe + { + PInvoke.CoAllowSetForegroundWindow(iextn); + } + } + + public override bool Equals(object? obj) => obj is CommandProviderWrapper wrapper && isValid == wrapper.isValid; + + public override int GetHashCode() + { + throw new NotImplementedException(); + } +} diff --git a/src/modules/cmdpal/WindowsCommandPalette/Views/MainViewModel.xaml.cs b/src/modules/cmdpal/WindowsCommandPalette/Views/MainViewModel.xaml.cs new file mode 100644 index 000000000000..118f3a0e712c --- /dev/null +++ b/src/modules/cmdpal/WindowsCommandPalette/Views/MainViewModel.xaml.cs @@ -0,0 +1,165 @@ +// 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.Collections.ObjectModel; +using Microsoft.CmdPal.Ext.Apps.Programs; +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; +using Microsoft.CmdPal.Ext.WebSearch; +using Microsoft.CmdPal.Ext.WindowsServices; +using Microsoft.CmdPal.Ext.WindowsSettings; +using Microsoft.CmdPal.Ext.WindowsTerminal; +using Microsoft.CmdPal.Ext.WindowWalker; +using Microsoft.CmdPal.Extensions; +using Microsoft.CmdPal.Extensions.Helpers; +using Windows.Foundation; +using WindowsCommandPalette.BuiltinCommands; +using WindowsCommandPalette.Models; + +namespace WindowsCommandPalette.Views; + +public sealed class MainViewModel : IDisposable +{ + private readonly QuitCommandProvider _quitCommandProvider = new(); + private readonly ReloadExtensionsCommandProvider _reloadCommandProvider = new(); + + public AllAppsPage Apps { get; set; } = new(); + + public event TypedEventHandler? QuitRequested { add => _quitCommandProvider.QuitRequested += value; remove => _quitCommandProvider.QuitRequested -= value; } + + public ObservableCollection ActionsProvider { get; set; } = []; + + public ObservableCollection> TopLevelCommands { get; set; } = []; + + public List BuiltInCommands { get; set; } = []; + + public bool Loaded { get; set; } + + public bool LoadingExtensions { get; set; } + + public bool LoadedApps { get; set; } + + public event TypedEventHandler? HideRequested; + + public event TypedEventHandler? SummonRequested; + + public event TypedEventHandler? AppsReady; + + public event TypedEventHandler? GoToCommandRequested; + + private readonly Dictionary _aliases = []; + + internal MainViewModel() + { + BuiltInCommands.Add(new IndexerCommandsProvider()); + BuiltInCommands.Add(new BookmarksCommandProvider()); + BuiltInCommands.Add(new CalculatorCommandProvider()); + BuiltInCommands.Add(_quitCommandProvider); + BuiltInCommands.Add(_reloadCommandProvider); + BuiltInCommands.Add(new WindowsTerminalCommandsProvider()); + BuiltInCommands.Add(new WindowsServicesCommandsProvider()); + BuiltInCommands.Add(new RegistryCommandsProvider()); + BuiltInCommands.Add(new ClipboardHistoryCommandsProvider()); + BuiltInCommands.Add(new ShellCommandsProvider()); + BuiltInCommands.Add(new WindowWalkerCommandsProvider()); + BuiltInCommands.Add(new WebSearchCommandsProvider()); + + ResetTopLevel(); + + PopulateAliases(); + + // On a background thread, warm up the app cache since we want it more often than not + new Task(() => + { + _ = AppCache.Instance.Value; + + LoadedApps = true; + AppsReady?.Invoke(this, null); + }).Start(); + } + + public void ResetTopLevel() + { + TopLevelCommands.Clear(); + TopLevelCommands.Add(new(new ListItem(Apps))); + } + + internal void RequestHide() + { + var handlers = HideRequested; + handlers?.Invoke(this, null); + } + + public void Summon() + { + var handlers = SummonRequested; + handlers?.Invoke(this, null); + } + + public IEnumerable AppItems => LoadedApps ? Apps.GetItems() : []; + + // Okay this is definitely bad - Evaluating this re-wraps every app in the list with a new wrapper, holy fuck that's stupid + public IEnumerable> Everything => TopLevelCommands + .Concat(AppItems.Select(i => new ExtensionObject(i))) + .Where(i => + { + var v = i != null; + return v; + }); + + public void Dispose() + { + _quitCommandProvider.Dispose(); + _reloadCommandProvider.Dispose(); + } + + private void AddAlias(CommandAlias a) => _aliases.Add(a.SearchPrefix, a); + + public bool CheckAlias(string searchText) + { + // var foundAlias = searchText == "vd"; + // var aliasTarget = "com.zadjii.VirtualDesktopsList"; + if (_aliases.TryGetValue(searchText, out var alias)) + { + try + { + foreach (var listItemWrapper in this.TopLevelCommands) + { + var li = listItemWrapper.Unsafe; + if (li == null) + { + continue; + } + + var id = li.Command?.Id; + if (!string.IsNullOrEmpty(id) && id == alias.CommandId) + { + GoToCommandRequested?.Invoke(this, li.Command); + return true; + } + } + } + catch + { + } + } + + return false; + } + + private void PopulateAliases() + { + this.AddAlias(new CommandAlias("vd", "com.zadjii.VirtualDesktopsList", true)); + this.AddAlias(new CommandAlias(":", "com.microsoft.cmdpal.registry", true)); + this.AddAlias(new CommandAlias("$", "com.microsoft.cmdpal.windowsSettings", true)); + this.AddAlias(new CommandAlias("=", "com.microsoft.cmdpal.calculator", true)); + this.AddAlias(new CommandAlias(">", "com.microsoft.cmdpal.shell", true)); + this.AddAlias(new CommandAlias("<", "com.microsoft.cmdpal.windowwalker", true)); + this.AddAlias(new CommandAlias("??", "com.microsoft.cmdpal.websearch", true)); + } +}