diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 9115249393..4d7cc47d3c 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -639,6 +639,7 @@ Hiber Hiberboot HIBYTE hicon +HICONSM HIDEREADONLY HIDEWINDOW Hif diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WindowWalker.UnitTests/Settings.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WindowWalker.UnitTests/Settings.cs index e8271da371..cbbe365a1b 100644 --- a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WindowWalker.UnitTests/Settings.cs +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WindowWalker.UnitTests/Settings.cs @@ -17,6 +17,7 @@ public class Settings : ISettingsInterface private readonly bool hideKillProcessOnElevatedProcesses; private readonly bool hideExplorerSettingInfo; private readonly bool inMruOrder; + private readonly bool useWindowIcon; public Settings( bool resultsFromVisibleDesktopOnly = false, @@ -27,7 +28,8 @@ public class Settings : ISettingsInterface bool openAfterKillAndClose = false, bool hideKillProcessOnElevatedProcesses = false, bool hideExplorerSettingInfo = true, - bool inMruOrder = true) + bool inMruOrder = true, + bool useWindowIcon = true) { this.resultsFromVisibleDesktopOnly = resultsFromVisibleDesktopOnly; this.subtitleShowPid = subtitleShowPid; @@ -38,6 +40,7 @@ public class Settings : ISettingsInterface this.hideKillProcessOnElevatedProcesses = hideKillProcessOnElevatedProcesses; this.hideExplorerSettingInfo = hideExplorerSettingInfo; this.inMruOrder = inMruOrder; + this.useWindowIcon = useWindowIcon; } public bool ResultsFromVisibleDesktopOnly => resultsFromVisibleDesktopOnly; @@ -57,4 +60,6 @@ public class Settings : ISettingsInterface public bool HideExplorerSettingInfo => hideExplorerSettingInfo; public bool InMruOrder => inMruOrder; + + public bool UseWindowIcon => useWindowIcon; } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Commands/SwitchToWindowCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Commands/SwitchToWindowCommand.cs index 695eaa2c83..c215d0f300 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Commands/SwitchToWindowCommand.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Commands/SwitchToWindowCommand.cs @@ -2,11 +2,16 @@ // 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.Diagnostics; +using System.Drawing; +using System.IO; using Microsoft.CmdPal.Ext.WindowWalker.Components; +using Microsoft.CmdPal.Ext.WindowWalker.Helpers; using Microsoft.CmdPal.Ext.WindowWalker.Properties; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.Storage.Streams; namespace Microsoft.CmdPal.Ext.WindowWalker.Commands; @@ -16,20 +21,53 @@ internal sealed partial class SwitchToWindowCommand : InvokableCommand public SwitchToWindowCommand(Window? window) { + Icon = Icons.GenericAppIcon; // Fallback to default icon Name = Resources.switch_to_command_title; _window = window; if (_window is not null) { - var p = Process.GetProcessById((int)_window.Process.ProcessID); - if (p is not null) + // Use window icon + if (SettingsManager.Instance.UseWindowIcon) { - try + if (_window.TryGetWindowIcon(out var icon) && icon is not null) { - var processFileName = p.MainModule?.FileName; - Icon = new IconInfo(processFileName); + try + { + using var bitmap = icon.ToBitmap(); + using var memoryStream = new MemoryStream(); + bitmap.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png); + var raStream = new InMemoryRandomAccessStream(); + using var outputStream = raStream.GetOutputStreamAt(0); + using var dataWriter = new DataWriter(outputStream); + dataWriter.WriteBytes(memoryStream.ToArray()); + dataWriter.StoreAsync().AsTask().Wait(); + dataWriter.FlushAsync().AsTask().Wait(); + Icon = IconInfo.FromStream(raStream); + } + catch + { + } + finally + { + icon.Dispose(); + } } - catch + } + + // Use process icon + else + { + var p = Process.GetProcessById((int)_window.Process.ProcessID); + if (p is not null) { + try + { + var processFileName = p.MainModule?.FileName; + Icon = new IconInfo(processFileName); + } + catch + { + } } } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Components/Window.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Components/Window.cs index 1dc43600c2..c071d55e80 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Components/Window.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Components/Window.cs @@ -188,6 +188,62 @@ internal sealed class Window thread.Start(); } + /// + /// Tries to get the window icon. + /// + /// The window icon if found; otherwise, null. + /// True if an icon was found; otherwise, false. + internal bool TryGetWindowIcon(out System.Drawing.Icon? icon) + { + icon = null; + + if (hwnd == IntPtr.Zero) + { + return false; + } + + // Try WM_GETICON with SendMessageTimeout + if (NativeMethods.SendMessageTimeout(hwnd, Win32Constants.WM_GETICON, (UIntPtr)Win32Constants.ICON_BIG, IntPtr.Zero, Win32Constants.SMTO_ABORTIFHUNG, 100, out var result) != 0 && result != 0) + { + icon = System.Drawing.Icon.FromHandle((IntPtr)result); + NativeMethods.DestroyIcon((IntPtr)result); + return true; + } + + if (NativeMethods.SendMessageTimeout(hwnd, Win32Constants.WM_GETICON, (UIntPtr)Win32Constants.ICON_SMALL, IntPtr.Zero, Win32Constants.SMTO_ABORTIFHUNG, 100, out result) != 0 && result != 0) + { + icon = System.Drawing.Icon.FromHandle((IntPtr)result); + NativeMethods.DestroyIcon((IntPtr)result); + return true; + } + + if (NativeMethods.SendMessageTimeout(hwnd, Win32Constants.WM_GETICON, (UIntPtr)Win32Constants.ICON_SMALL2, IntPtr.Zero, Win32Constants.SMTO_ABORTIFHUNG, 100, out result) != 0 && result != 0) + { + icon = System.Drawing.Icon.FromHandle((IntPtr)result); + NativeMethods.DestroyIcon((IntPtr)result); + return true; + } + + // Fallback to GetClassLongPtr + var iconHandle = NativeMethods.GetClassLongPtr(hwnd, Win32Constants.GCLP_HICON); + if (iconHandle != IntPtr.Zero) + { + icon = System.Drawing.Icon.FromHandle(iconHandle); + NativeMethods.DestroyIcon((IntPtr)iconHandle); + return true; + } + + iconHandle = NativeMethods.GetClassLongPtr(hwnd, Win32Constants.GCLP_HICONSM); + if (iconHandle != IntPtr.Zero) + { + icon = System.Drawing.Icon.FromHandle(iconHandle); + NativeMethods.DestroyIcon((IntPtr)iconHandle); + return true; + } + + return false; + } + /// /// Converts the window name to string along with the process name /// diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/ISettingsInterface.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/ISettingsInterface.cs index e77acb56cf..de3827c76a 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/ISettingsInterface.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/ISettingsInterface.cs @@ -23,4 +23,6 @@ public interface ISettingsInterface public bool HideExplorerSettingInfo { get; } public bool InMruOrder { get; } + + public bool UseWindowIcon { get; } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/NativeMethods.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/NativeMethods.cs index 382d1a56d1..57d65a305e 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/NativeMethods.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/NativeMethods.cs @@ -84,6 +84,12 @@ public static partial class NativeMethods [DllImport("user32.dll")] public static extern int SendMessageTimeout(IntPtr hWnd, uint msg, UIntPtr wParam, IntPtr lParam, int fuFlags, int uTimeout, out int lpdwResult); + [DllImport("user32.dll", EntryPoint = "GetClassLongPtr")] + public static extern IntPtr GetClassLongPtr(IntPtr hWnd, int nIndex); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + internal static extern bool DestroyIcon(IntPtr hIcon); + [DllImport("kernel32.dll")] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool CloseHandle(IntPtr hObject); @@ -143,6 +149,41 @@ public static class Win32Constants /// public const int SC_CLOSE = 0xF060; + /// + /// Sent to a window to retrieve a handle to the large or small icon associated with a window. + /// + public const uint WM_GETICON = 0x007F; + + /// + /// Retrieve the large icon for the window. + /// + public const int ICON_BIG = 1; + + /// + /// Retrieve the small icon for the window. + /// + public const int ICON_SMALL = 0; + + /// + /// Retrieve the small icon provided by the application. + /// + public const int ICON_SMALL2 = 2; + + /// + /// The function returns if the receiving thread does not respond within the timeout period. + /// + public const int SMTO_ABORTIFHUNG = 0x0002; + + /// + /// Retrieves a handle to the icon associated with the class. + /// + public const int GCLP_HICON = -14; + + /// + /// Retrieves a handle to the small icon associated with the class. + /// + public const int GCLP_HICONSM = -34; + /// /// RPC call succeeded /// diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/SettingsManager.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/SettingsManager.cs index b2a248beca..1b223fea9b 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/SettingsManager.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Helpers/SettingsManager.cs @@ -70,6 +70,12 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface Resources.windowwalker_SettingInMruOrder_Description, true); + private readonly ToggleSetting _useWindowIcon = new( + Namespaced(nameof(UseWindowIcon)), + Resources.windowwalker_SettingUseWindowIcon, + Resources.windowwalker_SettingUseWindowIcon_Description, + true); + public bool ResultsFromVisibleDesktopOnly => _resultsFromVisibleDesktopOnly.Value; public bool SubtitleShowPid => _subtitleShowPid.Value; @@ -88,6 +94,8 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface public bool InMruOrder => _inMruOrder.Value; + public bool UseWindowIcon => _useWindowIcon.Value; + internal static string SettingsJsonPath() { var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); @@ -110,6 +118,7 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface Settings.Add(_hideKillProcessOnElevatedProcesses); Settings.Add(_hideExplorerSettingInfo); Settings.Add(_inMruOrder); + Settings.Add(_useWindowIcon); // Load settings from file upon initialization LoadSettings(); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Icons.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Icons.cs index bfcb47f428..dd2ae9b1cb 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Icons.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Icons.cs @@ -15,4 +15,6 @@ internal sealed class Icons internal static IconInfo CloseWindow { get; } = new IconInfo("\uE894"); // Clear internal static IconInfo Info { get; } = new IconInfo("\uE946"); // Info + + internal static IconInfo GenericAppIcon { get; } = new("\uE737"); // Favicon } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Properties/Resources.Designer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Properties/Resources.Designer.cs index ecb09c8c38..1a8c5106d4 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Properties/Resources.Designer.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Properties/Resources.Designer.cs @@ -401,5 +401,23 @@ namespace Microsoft.CmdPal.Ext.WindowWalker.Properties { return ResourceManager.GetString("windowwalker_SettingTagPid", resourceCulture); } } + + /// + /// Looks up a localized string similar to Use window icons. + /// + public static string windowwalker_SettingUseWindowIcon { + get { + return ResourceManager.GetString("windowwalker_SettingUseWindowIcon", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show the actual window icon instead of the process icon. + /// + public static string windowwalker_SettingUseWindowIcon_Description { + get { + return ResourceManager.GetString("windowwalker_SettingUseWindowIcon_Description", resourceCulture); + } + } } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Properties/Resources.resx b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Properties/Resources.resx index c610b7b09c..3d61936a1d 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Properties/Resources.resx +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Properties/Resources.resx @@ -235,4 +235,10 @@ No open windows found + + Use window icons + + + Show the actual window icon instead of the process icon + \ No newline at end of file