diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 0000000..c8c4f62 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,75 @@ +name: 🐛 Bug Report +description: Report an issue to help improve the project. +title: "[BUG] title" +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + We're sorry that you're experiencing an issue. + Please fill in this form to the best of your ability, So that we can help you effectively. + - type: checkboxes + id: duplicates + attributes: + label: Has this bug been raised before? + description: Increase the chances of your issue being accepted by ensuring it has not been raised before. + options: + - label: I have checked "open" AND "closed" issues and this is not a duplicate + validations: + required: true + - type: textarea + id: description + attributes: + label: "Describe your issue:" + placeholder: When I click here this happens + validations: + required: true + - type: input + id: os + attributes: + label: What OS do you have? + value: "Windows 11" + validations: + required: true + - type: input + id: edition + attributes: + label: What edition? + value: "Home.." + validations: + required: true + - type: input + id: version + attributes: + label: OS Version? + value: "22H2.." + validations: + required: true + - type: input + id: build + attributes: + label: OS Build? + value: "23585.xxx" + validations: + required: true + - type: dropdown + id: tweaked + attributes: + label: Have you tweaked your Windows 11 look? + multiple: false + options: + - "No" + - "Yes" + default: 0 + validations: + required: true + - type: textarea + attributes: + label: If "Yes" to the above, please explain how or what tools you have used. + - type: textarea + attributes: + label: Put here any screenshots or videos (optional) + - type: markdown + attributes: + value: | + Thanks for reporting this issue! We will get back to you as soon as possible. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..9e41d2c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,38 @@ +name: 💡 General Feature Request +description: Have a new idea/feature? Let us know... +title: "[FEATURE] title" +labels: ["enhancement"] +body: + - type: checkboxes + id: duplicates + attributes: + label: Is this a unique feature? + description: Increase the chances of your issue being accepted by making sure it has not been raised before. + options: + - label: I have checked "open" AND "closed" issues and this is not a duplicate + required: true + - type: textarea + id: description + attributes: + label: Proposed Solution + description: A clear description of the enhancement you propose. Please include relevant information and resources (for example another project's implementation of this feature). + validations: + required: true + - type: dropdown + id: assignee + attributes: + label: Do you want to work on this issue? + multiple: false + options: + - "No" + - "Yes" + default: 0 + validations: + required: false + - type: textarea + id: extrainfo + attributes: + label: If "yes" to the above, please explain how you would technically implement this + description: For example reference any existing code + validations: + required: false \ No newline at end of file diff --git a/ExplorerTabUtility/App.config b/ExplorerTabUtility/App.config index b42d465..9d29ca9 100644 --- a/ExplorerTabUtility/App.config +++ b/ExplorerTabUtility/App.config @@ -13,6 +13,12 @@ True + + True + + + False + \ No newline at end of file diff --git a/ExplorerTabUtility/ExplorerTabUtility.csproj b/ExplorerTabUtility/ExplorerTabUtility.csproj index 7466ea0..89651d2 100644 --- a/ExplorerTabUtility/ExplorerTabUtility.csproj +++ b/ExplorerTabUtility/ExplorerTabUtility.csproj @@ -24,6 +24,7 @@ + diff --git a/ExplorerTabUtility/Forms/TrayIcon.cs b/ExplorerTabUtility/Forms/TrayIcon.cs index 17eafbb..03fe09f 100644 --- a/ExplorerTabUtility/Forms/TrayIcon.cs +++ b/ExplorerTabUtility/Forms/TrayIcon.cs @@ -1,73 +1,95 @@ using System; using System.Linq; using System.Threading; -using System.Threading.Tasks; -using System.Windows.Forms; using System.Diagnostics; +using System.Windows.Forms; +using System.Threading.Tasks; +using System.Collections.Generic; +using FlaUI.Core.AutomationElements; using Microsoft.Win32; -using ExplorerTabUtility.Helpers; using ExplorerTabUtility.Hooks; using ExplorerTabUtility.Models; using ExplorerTabUtility.WinAPI; +using ExplorerTabUtility.Helpers; +using Window = ExplorerTabUtility.Models.Window; namespace ExplorerTabUtility.Forms; public class TrayIcon : ApplicationContext { - private static NotifyIcon _notifyIcon = default!; - private static KeyboardHook _keyboardHook = default!; - private static UiAutomation _uiAutomation = default!; - private static readonly SemaphoreSlim Limiter = new(1); private static IntPtr _mainWindowHandle = IntPtr.Zero; + private static readonly NotifyIcon NotifyIcon; + private static readonly Keyboard KeyboardHook; + private static readonly Shell32 WindowHook; + private static readonly SemaphoreSlim Limiter; + private static WindowHookVia _windowHookVia; - public TrayIcon() + static TrayIcon() { - _keyboardHook = new KeyboardHook(OnNewWindow); - _uiAutomation = new UiAutomation(OnNewWindow); + Limiter = new SemaphoreSlim(1); + WindowHook = new Shell32(OnNewWindow); + KeyboardHook = new Keyboard(OnNewWindow); + + var isKeyboardHookActive = Properties.Settings.Default.KeyboardHook; + var isWindowHookActive = Properties.Settings.Default.WindowHook; + _windowHookVia = Properties.Settings.Default.WindowViaUi + ? WindowHookVia.Ui + : WindowHookVia.Keys; + + NotifyIcon = new NotifyIcon + { + Icon = Helper.GetIcon(), + Text = "Explorer Tab Utility: Force new windows to tabs.", + + ContextMenuStrip = CreateContextMenuStrip(isKeyboardHookActive, isWindowHookActive), + Visible = true + }; - InitializeComponent(); + if (isKeyboardHookActive) KeyboardHook.StartHook(); + if (isWindowHookActive) WindowHook.StartHook(); Application.ApplicationExit += OnApplicationExit; } - - private static void InitializeComponent() + private static ContextMenuStrip CreateContextMenuStrip(bool isKeyboardHookActive, bool isWindowHookActive) { - var windowHMenuItem = new ToolStripMenuItem("All Windows"); - var keyboardHMenuItem = new ToolStripMenuItem("Keyboard (Win + E)"); - var startupMenuItem = new ToolStripMenuItem("Add to startup"); - var exitMenuItem = new ToolStripMenuItem("Exit"); + var strip = new ContextMenuStrip(); - windowHMenuItem.Checked = Properties.Settings.Default.WindowHook; - keyboardHMenuItem.Checked = Properties.Settings.Default.KeyboardHook; - startupMenuItem.Checked = IsInStartup(); + strip.Items.Add(CreateToolStripMenuItem("Keyboard (Win + E)", isKeyboardHookActive, ToggleKeyboardHook)); + strip.Items.Add(CreateWindowHookMenuItem(isWindowHookActive)); - if (windowHMenuItem.Checked) - _uiAutomation.StartHook(); + strip.Items.Add(new ToolStripSeparator()); + strip.Items.Add(CreateToolStripMenuItem("Add to startup", IsInStartup(), ToggleStartup)); - if (keyboardHMenuItem.Checked) - _keyboardHook.StartHook(); + strip.Items.Add(new ToolStripSeparator()); + strip.Items.Add(CreateToolStripMenuItem("Exit", false, static (_, _) => Application.Exit())); - _notifyIcon = new NotifyIcon - { - Icon = Helper.GetIcon(), - Text = "Explorer Tab Utility: Force new windows to tabs.", + return strip; + } + private static ToolStripMenuItem CreateWindowHookMenuItem(bool isWindowHookActive) + { + var windowHookMenuItem = CreateToolStripMenuItem("All Windows", isWindowHookActive, ToggleWindowHook); - ContextMenuStrip = new ContextMenuStrip() - }; + windowHookMenuItem.DropDownItems.Add( + CreateToolStripMenuItem("UI (Recommended)", _windowHookVia == WindowHookVia.Ui, WindowHookViaChanged, "WindowViaUi")); - _notifyIcon.ContextMenuStrip.Items.Add(windowHMenuItem); - _notifyIcon.ContextMenuStrip.Items.Add(keyboardHMenuItem); - _notifyIcon.ContextMenuStrip.Items.Add(new ToolStripSeparator()); - _notifyIcon.ContextMenuStrip.Items.Add(startupMenuItem); - _notifyIcon.ContextMenuStrip.Items.Add(new ToolStripSeparator()); - _notifyIcon.ContextMenuStrip.Items.Add(exitMenuItem); + windowHookMenuItem.DropDownItems.Add( + CreateToolStripMenuItem("Keys", _windowHookVia == WindowHookVia.Keys, WindowHookViaChanged, "WindowViaKeys")); - windowHMenuItem.Click += (_, _) => ToggleWindowHook(windowHMenuItem); - keyboardHMenuItem.Click += (_, _) => ToggleKeyboardHook(keyboardHMenuItem); - startupMenuItem.Click += (_, _) => ToggleStartup(startupMenuItem); - exitMenuItem.Click += (_, _) => Application.Exit(); + return windowHookMenuItem; + } + private static ToolStripMenuItem CreateToolStripMenuItem(string text, bool isChecked, EventHandler eventHandler, string? name = default) + { + var item = new ToolStripMenuItem + { + Text = text, + Checked = isChecked + }; - _notifyIcon.Visible = true; + if (name != default) + item.Name = name; + + item.Click += eventHandler; + return item; } private static bool IsInStartup() @@ -81,8 +103,10 @@ private static bool IsInStartup() var value = key.GetValue(Constants.AppName) as string; return string.Equals(value, executablePath, StringComparison.OrdinalIgnoreCase); } - private static void ToggleStartup(ToolStripMenuItem startupMenuItem) + private static void ToggleStartup(object? sender, EventArgs _) { + if (sender is not ToolStripMenuItem item) return; + var executablePath = Process.GetCurrentProcess().MainModule?.FileName; if (string.IsNullOrWhiteSpace(executablePath)) return; @@ -94,38 +118,66 @@ private static void ToggleStartup(ToolStripMenuItem startupMenuItem) { // Remove from startup key.DeleteValue(Constants.AppName, false); - startupMenuItem.Checked = false; + item.Checked = false; } else { // Add to startup key.SetValue(Constants.AppName, executablePath); - startupMenuItem.Checked = true; + item.Checked = true; } } - private static void ToggleWindowHook(ToolStripMenuItem windowHMenuItem) + private static void ToggleKeyboardHook(object? sender, EventArgs _) { - windowHMenuItem.Checked = !windowHMenuItem.Checked; + if (sender is not ToolStripMenuItem item) return; + + item.Checked = !item.Checked; - Properties.Settings.Default.WindowHook = windowHMenuItem.Checked; + Properties.Settings.Default.KeyboardHook = item.Checked; Properties.Settings.Default.Save(); - if (windowHMenuItem.Checked) - _uiAutomation.StartHook(); + if (item.Checked) + KeyboardHook.StartHook(); else - _uiAutomation.StopHook(); + KeyboardHook.StopHook(); } - private static void ToggleKeyboardHook(ToolStripMenuItem keyboardHMenuItem) + private static void ToggleWindowHook(object? sender, EventArgs _) { - keyboardHMenuItem.Checked = !keyboardHMenuItem.Checked; + if (sender is not ToolStripMenuItem item) return; - Properties.Settings.Default.KeyboardHook = keyboardHMenuItem.Checked; + item.Checked = !item.Checked; + + Properties.Settings.Default.WindowHook = item.Checked; Properties.Settings.Default.Save(); - if (keyboardHMenuItem.Checked) - _keyboardHook.StartHook(); + if (item.Checked) + WindowHook.StartHook(); else - _keyboardHook.StopHook(); + WindowHook.StopHook(); + + foreach (ToolStripItem subItem in item.DropDownItems) + subItem.Enabled = item.Checked; + } + private static void WindowHookViaChanged(object? sender, EventArgs _) + { + if (sender is not ToolStripMenuItem item) return; + + var container = item.GetCurrentParent(); + foreach (ToolStripMenuItem radio in container.Items) + { + radio.Checked = !radio.Checked; + + if (radio.Name == "WindowViaUi") + Properties.Settings.Default.WindowViaUi = radio.Checked; + else if (radio.Name == "WindowViaKeys") + Properties.Settings.Default.WindowViaKeys = radio.Checked; + } + + Properties.Settings.Default.Save(); + + _windowHookVia = Properties.Settings.Default.WindowViaUi + ? WindowHookVia.Ui + : WindowHookVia.Keys; } private static async Task OnNewWindow(Window window) @@ -141,24 +193,30 @@ private static async Task OnNewWindow(Window window) var windowElement = UiAutomation.FromHandle(windowHandle); if (windowElement == default) return; + // Store currently opened Tabs, before we open a new one. var oldTabs = WinApi.GetAllExplorerTabs(); - UiAutomation.AddNewTab(windowElement); + // Add new tab. + AddNewTab(windowElement); // If it is just a new (This PC | Home), return. if (string.IsNullOrWhiteSpace(window.Path)) return; + // Get newly created tab's handle (That is not in 'oldTabs') var newTabHandle = WinApi.ListenForNewExplorerTab(oldTabs); - if (newTabHandle == default) return; + if (newTabHandle == 0) return; + // Get the tab element out of that handle. var newTabElement = UiAutomation.FromHandle(newTabHandle); if (newTabElement == default) return; - UiAutomation.GoToLocation(window.Path, windowElement); + // Navigate to the target location + Navigate(windowElement, newTabHandle, window.Path); - if (window.SelectedItems is not { } selectedItems) return; + if (window.SelectedItems is not { Count: > 0 } selectedItems) return; - UiAutomation.SelectItems(newTabElement, selectedItems); + // Select items + SelectItems(newTabElement, selectedItems); } finally { @@ -168,9 +226,10 @@ private static async Task OnNewWindow(Window window) Limiter.Release(); } } + private static IntPtr GetMainWindowHWnd(IntPtr otherThan) { - if (WinApi.IsWindowStillHasClassName(_mainWindowHandle, "CabinetWClass")) + if (WinApi.IsWindowHasClassName(_mainWindowHandle, "CabinetWClass")) return _mainWindowHandle; var allWindows = WinApi.FindAllWindowsEx(); @@ -180,11 +239,37 @@ private static IntPtr GetMainWindowHWnd(IntPtr otherThan) return _mainWindowHandle; } + private static void AddNewTab(AutomationElement window) + { + // if via UI is selected try to add a new tab with UI Automation. + if (_windowHookVia == WindowHookVia.Ui && UiAutomation.AddNewTab(window)) + return; + // Via Keys is selected or UI Automation fails. + Keyboard.AddNewTab(window.Properties.NativeWindowHandle.Value); + } + private static void Navigate(AutomationElement window, nint tabHandle, string location) + { + // if via UI is selected try to Navigate with UI Automation. + if (_windowHookVia == WindowHookVia.Ui && UiAutomation.Navigate(window, tabHandle, location)) + return; + + // Via Keys is selected or UI Automation fails. + Keyboard.Navigate(window.Properties.NativeWindowHandle.Value, tabHandle, location); + } + private static void SelectItems(AutomationElement tab, ICollection names) + { + // if via UI is selected try to Select with UI Automation. + if (_windowHookVia == WindowHookVia.Ui && UiAutomation.SelectItems(tab, names)) + return; + + // Via Keys is selected or UI Automation fails. + Keyboard.SelectItems(tab.Properties.NativeWindowHandle.Value, names); + } private static void OnApplicationExit(object? _, EventArgs __) { - _notifyIcon.Visible = false; - _keyboardHook.Dispose(); - _uiAutomation.Dispose(); + NotifyIcon.Visible = false; + KeyboardHook.Dispose(); + WindowHook.Dispose(); } } \ No newline at end of file diff --git a/ExplorerTabUtility/Helpers/Helper.cs b/ExplorerTabUtility/Helpers/Helper.cs index f48af9f..911aebf 100644 --- a/ExplorerTabUtility/Helpers/Helper.cs +++ b/ExplorerTabUtility/Helpers/Helper.cs @@ -8,39 +8,60 @@ namespace ExplorerTabUtility.Helpers; public static class Helper { - public static T DoUntilCondition(Func action, Predicate predicate, int timeMs = 500, CancellationToken cancellationToken = default) + public static T DoUntilNotDefault(Func action, int timeMs = 500, int sleepMs = 20, CancellationToken cancellationToken = default) + { + return DoUntilCondition( + action, + result => !EqualityComparer.Default.Equals(result, default), + timeMs, + sleepMs, + cancellationToken); + } + public static void DoUntilTimeEnd(Action action, int timeMs = 500, int sleepMs = 20, CancellationToken cancellationToken = default) + { + DoUntilCondition(action, static () => false, timeMs, sleepMs, cancellationToken); + } + public static void DoUntilCondition(Action action, Func predicate, int timeMs = 500, int sleepMs = 20, CancellationToken cancellationToken = default) { var startTicks = Stopwatch.GetTimestamp(); while (!cancellationToken.IsCancellationRequested && !IsTimeUp(startTicks, timeMs)) { - var result = action.Invoke(); - if (predicate(result)) - return result; - } + action(); + if (predicate()) + return; - return action.Invoke(); + Thread.Sleep(sleepMs); + } } - public static T DoUntilNotDefault(Func action, int timeMs = 500, CancellationToken cancellationToken = default) + public static T DoUntilCondition(Func action, Predicate predicate, int timeMs = 500, int sleepMs = 20, CancellationToken cancellationToken = default) { var startTicks = Stopwatch.GetTimestamp(); while (!cancellationToken.IsCancellationRequested && !IsTimeUp(startTicks, timeMs)) { - var result = action.Invoke(); - if (!EqualityComparer.Default.Equals(result, default)) + var result = action(); + if (predicate(result)) return result; + + Thread.Sleep(sleepMs); } - return action.Invoke(); + return action(); } - public static void DoUntilTimeEnd(Action action, int timeMs = 5_000, CancellationToken cancellationToken = default) + public static void DoIfCondition(Action action, Func predicate, bool justOnce = false, int timeMs = 500, int sleepMs = 20, CancellationToken cancellationToken = default) { var startTicks = Stopwatch.GetTimestamp(); while (!cancellationToken.IsCancellationRequested && !IsTimeUp(startTicks, timeMs)) { - action.Invoke(); + if (predicate()) + { + action(); + + if (justOnce) return; + } + Thread.Sleep(sleepMs); } } diff --git a/ExplorerTabUtility/Hooks/Keyboard.cs b/ExplorerTabUtility/Hooks/Keyboard.cs new file mode 100644 index 0000000..e1f1719 --- /dev/null +++ b/ExplorerTabUtility/Hooks/Keyboard.cs @@ -0,0 +1,146 @@ +using WindowsInput; +using System; +using System.Linq; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using ExplorerTabUtility.Models; +using ExplorerTabUtility.WinAPI; +using System.Threading; + +namespace ExplorerTabUtility.Hooks; + +public class Keyboard : IDisposable +{ + private nint _hookId = 0; + private nint _user32LibraryHandle = 0; + private bool _isWinKeyDown; + private HookProc? _keyboardHookCallback; // We have to keep a reference because of GC + private readonly Func _onNewWindow; + private static readonly IKeyboardSimulator KeyboardSimulator = new InputSimulator().Keyboard; + + public Keyboard(Func onNewWindow) + { + _onNewWindow = onNewWindow; + } + + public void StartHook() + { + _keyboardHookCallback = KeyboardHookCallback; + _user32LibraryHandle = WinApi.LoadLibrary("User32"); + _hookId = WinApi.SetWindowsHookEx(WinHookType.WH_KEYBOARD_LL, _keyboardHookCallback, _user32LibraryHandle, 0); + } + + public void StopHook() + { + Dispose(); + } + + private nint KeyboardHookCallback(int nCode, nint wParam, nint lParam) + { + if (nCode < 0) + return WinApi.CallNextHookEx(_hookId, nCode, wParam, lParam); + + // Read key + var vkCode = Marshal.ReadInt32(lParam); + + // Windows key + if (vkCode == WinApi.VK_WIN) + _isWinKeyDown = wParam == WinApi.WM_KEYDOWN; //DOWN or UP + + if (!_isWinKeyDown || vkCode != WinApi.VK_E || wParam != WinApi.WM_KEYDOWN) + return WinApi.CallNextHookEx(_hookId, nCode, wParam, lParam); + + // No Explorer windows, Continue with normal flow. + if (!WinApi.FindAllWindowsEx().Take(1).Any()) + return WinApi.CallNextHookEx(_hookId, nCode, wParam, lParam); + + // It is better not to wait for the invocation, otherwise the normal flow might open a new window + Task.Run(() => _onNewWindow.Invoke(new Window(string.Empty))); + + // Return dummy value to prevent normal flow. + return 1; + } + + public static void AddNewTab(nint windowHandle) + { + // Restore the window to foreground. + WinApi.RestoreWindowToForeground(windowHandle); + + // Give the focus to the folder view. + WinApi.PostMessage(windowHandle, WinApi.WM_SETFOCUS, 0, 0); + + // Send CTRL + T + KeyboardSimulator + .Sleep(300) + .ModifiedKeyStroke(VirtualKeyCode.CONTROL, VirtualKeyCode.VK_T); + } + public static void Navigate(nint windowHandle, nint tabHandle, string location) + { + // Restore the window to foreground. + WinApi.RestoreWindowToForeground(windowHandle); + + // Give the keyboard focus to the tab. + WinApi.PostMessage(tabHandle, WinApi.WM_SETFOCUS, 0, 0); + + // Send CTRL + L to activate the address bar + KeyboardSimulator + .Sleep(300) + .ModifiedKeyStroke(VirtualKeyCode.CONTROL, VirtualKeyCode.VK_L); + + // Type the location. + KeyboardSimulator + .Sleep(300) + .TextEntry(location) + .Sleep(250 + location.Length * 5) // Longer locations require longer wait time. + .KeyPress(VirtualKeyCode.RETURN); // Press Enter + + // Do in the background + Task.Run(async () => + { + // for ~1250 Milliseconds (25 * 50) + for (var i = 0; i < 25; i++) + { + await Task.Delay(50); + + var popupHandle = WinApi.GetWindow(windowHandle, WinApi.GW_ENABLEDPOPUP); + + // If the suggestion popup is not visible, continue. + if (popupHandle == 0) continue; + + // Hide the suggestion popup. + WinApi.ShowWindow(popupHandle, WinApi.SW_HIDE); + } + }); + } + public static void SelectItems(nint tabHandle, ICollection names) + { + // Restore the window to foreground. + WinApi.RestoreWindowToForeground(tabHandle); + + Thread.Sleep(500); + + // Type the first name. + KeyboardSimulator.TextEntry(names.First()); + } + + public void Dispose() + { + if (_hookId != IntPtr.Zero) + { + WinApi.UnhookWindowsHookEx(_hookId); + _hookId = IntPtr.Zero; + } + + _keyboardHookCallback = null; + if (_user32LibraryHandle == IntPtr.Zero) return; + + // reduces reference to library by 1. + WinApi.FreeLibrary(_user32LibraryHandle); + _user32LibraryHandle = IntPtr.Zero; + } + ~Keyboard() + { + Dispose(); + } +} \ No newline at end of file diff --git a/ExplorerTabUtility/Hooks/KeyboardHook.cs b/ExplorerTabUtility/Hooks/KeyboardHook.cs deleted file mode 100644 index 9704aa8..0000000 --- a/ExplorerTabUtility/Hooks/KeyboardHook.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Linq; -using System.Runtime.InteropServices; -using System.Threading.Tasks; -using ExplorerTabUtility.Models; -using ExplorerTabUtility.WinAPI; - -namespace ExplorerTabUtility.Hooks; - -public class KeyboardHook : IDisposable -{ - private IntPtr _hookId = IntPtr.Zero; - private IntPtr _user32LibraryHandle = IntPtr.Zero; - private bool _isWinKeyDown; - private HookProc? _keyboardHookCallback; // We have to keep a reference because of GC - private readonly Func _onNewWindow; - - public KeyboardHook(Func onNewWindow) - { - _onNewWindow = onNewWindow; - } - - public void StartHook() - { - _keyboardHookCallback = KeyboardHookCallback; - _user32LibraryHandle = WinApi.LoadLibrary("User32"); - _hookId = WinApi.SetWindowsHookEx(WinHookType.WH_KEYBOARD_LL, _keyboardHookCallback, _user32LibraryHandle, 0); - } - - public void StopHook() - { - Dispose(); - } - - private IntPtr KeyboardHookCallback(int nCode, IntPtr wParam, IntPtr lParam) - { - if (nCode < 0) - return WinApi.CallNextHookEx(_hookId, nCode, wParam, lParam); - - // Read key - var vkCode = Marshal.ReadInt32(lParam); - - // Windows key - if (vkCode == WinApi.VK_WIN) - _isWinKeyDown = wParam == WinApi.WM_KEYDOWN; //DOWN or UP - - if (!_isWinKeyDown || vkCode != WinApi.VK_E || wParam != WinApi.WM_KEYDOWN) - return WinApi.CallNextHookEx(_hookId, nCode, wParam, lParam); - - // No Explorer windows, Continue with normal flow. - if (!WinApi.FindAllWindowsEx().Take(1).Any()) - return WinApi.CallNextHookEx(_hookId, nCode, wParam, lParam); - - // It is better not to wait for the invocation, otherwise the normal flow might open a new window - Task.Run(() => _onNewWindow.Invoke(new Window(string.Empty))); - - // Return dummy value to prevent normal flow. - return new IntPtr(1); - } - - public void Dispose() - { - if (_hookId != IntPtr.Zero) - { - WinApi.UnhookWindowsHookEx(_hookId); - _hookId = IntPtr.Zero; - } - - _keyboardHookCallback = null; - if (_user32LibraryHandle == IntPtr.Zero) return; - - // reduces reference to library by 1. - WinApi.FreeLibrary(_user32LibraryHandle); - _user32LibraryHandle = IntPtr.Zero; - - GC.SuppressFinalize(this); - } - ~KeyboardHook() - { - Dispose(); - } -} \ No newline at end of file diff --git a/ExplorerTabUtility/Hooks/Shell32.cs b/ExplorerTabUtility/Hooks/Shell32.cs new file mode 100644 index 0000000..6231979 --- /dev/null +++ b/ExplorerTabUtility/Hooks/Shell32.cs @@ -0,0 +1,238 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Threading; +using ExplorerTabUtility.WinAPI; +using ExplorerTabUtility.Models; + +namespace ExplorerTabUtility.Hooks; + +public class Shell32 : IDisposable +{ + private IntPtr _hookId = IntPtr.Zero; + private WinEventDelegate? _eventHookCallback; // We have to keep a reference because of GC + private readonly Func _onNewWindow; + private static object? _shell; + private static Type? _shellType; + private static Type? _windowType; + + public Shell32(Func onNewWindow) + { + _onNewWindow = onNewWindow; + } + + public void StartHook() + { + _eventHookCallback = OnWindowOpenHandler; + _hookId = WinApi.SetWinEventHook(WinApi.EVENT_OBJECT_CREATE, WinApi.EVENT_OBJECT_CREATE, IntPtr.Zero, _eventHookCallback, 0, 0, 0); + } + public void StopHook() + { + Dispose(); + } + + private void OnWindowOpenHandler(IntPtr hWinEventHook, uint eventType, IntPtr hWnd, int idObject, int idChild, uint dwEventThread, uint dWmsEventTime) + { + if (!WinApi.IsWindowHasClassName(hWnd, "CabinetWClass")) return; + if (WinApi.FindAllWindowsEx().Take(2).Count() < 2) return; + + var originalRect = WinApi.HideWindow(hWnd); + var showAgain = true; + try + { + var window = GetWindowByHandle(GetWindows(), hWnd); + if (window == default) return; + + var location = GetWindowLocation(window); + + // Home + if (location == string.Empty) + { + CloseAndNotifyNewWindow(window, new Window(string.Empty, oldWindowHandle: hWnd)); + showAgain = false; + return; + } + + if (!Uri.TryCreate(location, UriKind.Absolute, out var uri)) + return; + + // Give the selection some time to take effect. + Thread.Sleep(70); + var selectedItems = GetSelectedItems(window); + + CloseAndNotifyNewWindow(window, new Window(uri.LocalPath, selectedItems, oldWindowHandle: hWnd)); + showAgain = false; + } + finally + { + // Move the back to the screen (Show) + if (showAgain) + WinApi.SetWindowPos(hWnd, IntPtr.Zero, originalRect.Left, originalRect.Top, 0, 0, WinApi.SWP_NOSIZE | WinApi.SWP_NOZORDER); + } + } + + public static object? GetWindows() + { + _shellType ??= Type.GetTypeFromProgID("Shell.Application"); + if (_shellType == default) return default; + + _shell ??= Activator.CreateInstance(_shellType); + if (_shell == default) return default; + + var windows = _shellType.InvokeMember("Windows", BindingFlags.InvokeMethod, null, _shell, Array.Empty()); + _windowType ??= windows?.GetType(); + + return windows; + } + + /// + /// Retrieves the current location of the specified window. + /// Applies only to the first tab of a window. + /// + /// The window object for which to get the location. This should be an instance of a window obtained from the Shell32 API. + /// The current location of the window as a string, or null if the window or window type is not defined. + public static string? GetWindowLocation(object? window) + { + if (window == default || _windowType == default) return default; + + return _windowType.InvokeMember("LocationURL", BindingFlags.GetProperty, null, window, null) as string; + } + + /// + /// Navigates the specified window to a given location. + /// Applies only to the first tab of a window. + /// + /// The window object to navigate. This should be an instance of a window obtained from the Shell32 API. + /// The location to navigate to. + public static void NavigateToLocation(object? window, string location) + { + if (window == default || _windowType == default) return; + + _windowType.InvokeMember("Navigate", BindingFlags.InvokeMethod, null, window, new object?[] { location }); + } + + private static int GetCount(object? item) + { + if (item == default || _windowType == default) return default; + + var obj = _windowType.InvokeMember("Count", BindingFlags.GetProperty, null, item, null); + return obj is int count ? count : default; + } + public static object? GetWindowByHandle(object? windows, IntPtr hWnd) + { + if (hWnd == default || windows == default || _windowType == default) return default; + + var count = GetCount(windows); + for (var i = 0; i < count; i++) + { + var window = _windowType.InvokeMember("Item", BindingFlags.InvokeMethod, null, windows, new object[] { i }); + if (window == default) continue; + + var itemHWnd = _windowType.InvokeMember("HWND", BindingFlags.GetProperty, null, window, null); + if (itemHWnd == default) continue; + + if ((long)itemHWnd == hWnd.ToInt64()) + return window; + } + + return default; + } + + /// + /// Retrieves the list of selected items in the specified window. + /// Applies only to the first tab of a window. + /// + /// The window object for which to get the selected items. This should be an instance of a window obtained from the Shell32 API. + /// A list of selected items as strings, or null if the window, window type, or selected items are not defined. + public static List? GetSelectedItems(object? window) + { + if (window == default || _windowType == default) return default; + + var document = _windowType.InvokeMember("Document", BindingFlags.GetProperty, null, window, null); + if (document == default) return default; + + var selectedItems = _windowType.InvokeMember("SelectedItems", BindingFlags.InvokeMethod, null, document, null); + if (selectedItems == default) return default; + + var count = GetCount(selectedItems); + if (count == 0) return default; + + var selectedList = new List(count); + for (var i = 0; i < count; i++) + { + var selectedItem = _windowType.InvokeMember("Item", BindingFlags.InvokeMethod, null, selectedItems, new object[] { i }); + if (selectedItem == default) return default; + + if (_windowType.InvokeMember("Name", BindingFlags.GetProperty, null, selectedItem, null) is string selectedItemName) + selectedList.Add(selectedItemName); + } + + return selectedList; + } + + /// + /// Selects the specified items in the given window. + /// Applies only to the first tab of a window. + /// + /// The window object in which to select items. This should be an instance of a window obtained from the Shell32 API. + /// The collection of names of the items to be selected. + public static void SelectItems(object? window, ICollection? names) + { + if (window == default || _windowType == default || names == default || names.Count == 0) + return; + + var document = _windowType.InvokeMember("Document", BindingFlags.GetProperty, null, window, null); + if (document == default) return; + + var folder = _windowType.InvokeMember("Folder", BindingFlags.GetProperty, null, document, null); + if (folder == default) return; + + var files = _windowType.InvokeMember("Items", BindingFlags.InvokeMethod, null, folder, null); + if (files == default) return; + + var count = GetCount(files); + if (count == default) return; + + var selectedCount = 0; + for (var i = 0; i < count; i++) + { + var item = _windowType.InvokeMember("Item", BindingFlags.InvokeMethod, null, files, new object[] { i }); + if (item == default) return; + + var name = _windowType.InvokeMember("Name", BindingFlags.GetProperty, null, item, null) as string; + + if (!names.Any(n => n.Equals(name, StringComparison.OrdinalIgnoreCase))) + continue; + + _windowType.InvokeMember("SelectItem", BindingFlags.InvokeMethod, null, document, new[] { item, 1 }); + + if (++selectedCount >= names.Count) return; + } + } + + public static void CloseWindow(object? window) + { + if (window == default || _windowType == default) return; + + _windowType.InvokeMember("Quit", BindingFlags.InvokeMethod, null, window, null); + } + private void CloseAndNotifyNewWindow(object? item, Window window) + { + if (item == default || _windowType == default) return; + + CloseWindow(item); + + Task.Run(() => _onNewWindow.Invoke(window)); + } + + public void Dispose() + { + WinApi.UnhookWinEvent(_hookId); + } + ~Shell32() + { + Dispose(); + } +} \ No newline at end of file diff --git a/ExplorerTabUtility/Hooks/UiAutomation.cs b/ExplorerTabUtility/Hooks/UiAutomation.cs index 94514d5..caec918 100644 --- a/ExplorerTabUtility/Hooks/UiAutomation.cs +++ b/ExplorerTabUtility/Hooks/UiAutomation.cs @@ -2,14 +2,14 @@ using System.Linq; using System.Threading.Tasks; using System.Collections.Generic; -using FlaUI.Core.AutomationElements; +using FlaUI.UIA3; using FlaUI.Core.Conditions; -using FlaUI.Core.Identifiers; using FlaUI.Core.Definitions; -using FlaUI.UIA3; -using ExplorerTabUtility.Helpers; +using FlaUI.Core.Identifiers; +using FlaUI.Core.AutomationElements; using ExplorerTabUtility.WinAPI; using ExplorerTabUtility.Models; +using ExplorerTabUtility.Helpers; using Window = ExplorerTabUtility.Models.Window; namespace ExplorerTabUtility.Hooks; @@ -29,17 +29,16 @@ public void StartHook() { Automation .GetDesktop() - .RegisterAutomationEvent(Automation.EventLibrary.Window.WindowOpenedEvent, TreeScope.Children, OnWindowOpened); + .RegisterAutomationEvent(Automation.EventLibrary.Window.WindowOpenedEvent, TreeScope.Children, OnWindowOpenHandler); } public void StopHook() { Automation.UnregisterAllEvents(); } - private void OnWindowOpened(AutomationElement element, EventId _) + private void OnWindowOpenHandler(AutomationElement element, EventId _) { if (element.ClassName != "CabinetWClass") return; - if (WinApi.FindAllWindowsEx().Take(2).Count() < 2) return; var hWnd = element.Properties.NativeWindowHandle.Value; @@ -48,7 +47,7 @@ private void OnWindowOpened(AutomationElement element, EventId _) if (_cache.Contains(hWnd)) return; _cache.Add(hWnd); - var originalRect = HideWindow(hWnd); + var originalRect = WinApi.HideWindow(hWnd); var showAgain = true; try { @@ -115,75 +114,80 @@ private void OnWindowOpened(AutomationElement element, EventId _) } } public static AutomationElement? FromHandle(IntPtr hWnd) => Automation.FromHandle(hWnd); - public static void AddNewTab(AutomationElement windowElement) + public static bool AddNewTab(AutomationElement window) { - var addButton = GetAddNewTabButton(windowElement); - if (addButton != default) - { - addButton.Patterns.Invoke.Pattern.Invoke(); - return; - } - - WinApi.RestoreWindowToForeground(windowElement.Properties.NativeWindowHandle.Value); + var addButton = GetAddNewTabButton(window); + if (addButton == default) return false; - //CTRL + T Down - WinApi.keybd_event(WinApi.VK_CONTROL, 0, 0, 0); - WinApi.keybd_event(WinApi.VK_T, 0, 0, 0); - - //CTRL + T UP - WinApi.keybd_event(WinApi.VK_T, 0, WinApi.KEYEVENTF_KEYUP, 0); - WinApi.keybd_event(WinApi.VK_CONTROL, 0, WinApi.KEYEVENTF_KEYUP, 0); + addButton.Patterns.Invoke.Pattern.Invoke(); + return true; } - public static AutomationElement? GetAddNewTabButton(AutomationElement windowElement) + public static AutomationElement? GetAddNewTabButton(AutomationElement window) { - var headerBar = windowElement.FindFirstChild(c => c.ByClassName("Microsoft.UI.Content.DesktopChildSiteBridge")); + var headerBar = window.FindFirstChild(c => c.ByClassName("Microsoft.UI.Content.DesktopChildSiteBridge")); var tabView = headerBar?.FindFirstChild("TabView"); var addButton = tabView?.FindFirstChild("AddButton"); return addButton; } - public static void GoToLocation(string location, AutomationElement windowElement) + public static bool Navigate(AutomationElement window, nint tabHandle, string location) { - GetHeaderElements(windowElement, out var suggestBox, out var searchBox, out var addressBar); - if (suggestBox == default || searchBox == default || addressBar == default) return; + GetHeaderElements(window, out var suggestBox, out var searchBox, out var addressBar); + if (suggestBox == default || searchBox == default || addressBar == default) + return false; - suggestBox.Patterns.Invoke.Pattern.Invoke(); + // Set the location. addressBar.Patterns.Value.Pattern.SetValue(location); // We have to invoke the suggestBox to Navigate :( suggestBox.Patterns.Invoke.Pattern.Invoke(); - // Invoke searchBox to hide the suggestPopup window. - searchBox.Patterns.Invoke.Pattern.Invoke(); + // Wait in the background + Task.Run(async () => + { + await Task.Delay(700).ConfigureAwait(false); + + var popupHandle = WinApi.GetWindow(window.Properties.NativeWindowHandle.Value, WinApi.GW_ENABLEDPOPUP); + + // If for some reason the address bar doesn't have the focus anymore, return. + if (popupHandle == 0) return; - //var suggestList = Helper.DoUntilNotDefault(() => suggestBox.FindFirstDescendant("SuggestionsList")); - //if (suggestList != default) - // searchBox.Patterns.Invoke.Pattern.Invoke(); + // Hide Suggestion popup. + WinApi.ShowWindow(popupHandle, WinApi.SW_HIDE); + + // Give the focus to the tab to close the address bar. + WinApi.PostMessage(tabHandle, WinApi.WM_SETFOCUS, 0, 0); + }); + + return true; } - public static void SelectItems(AutomationElement tabElement, ICollection names) + public static bool SelectItems(AutomationElement tab, ICollection names) { - if (names.Count == 0) return; + if (names.Count == 0) return false; var condition = new PropertyCondition(Automation.PropertyLibrary.Element.ClassName, "UIItemsView"); - var itemsView = Helper.DoUntilNotDefault(() => tabElement.FindFirstWithOptions(TreeScope.Subtree, condition, TreeTraversalOptions.Default, tabElement)); + var itemsView = Helper.DoUntilNotDefault(() => tab.FindFirstWithOptions(TreeScope.Subtree, condition, TreeTraversalOptions.Default, tab)); var files = itemsView?.FindAllChildren(); - if (files == default) return; + if (files == default) return false; var selectedCount = 0; foreach (var fileElement in files) { - if (names.Any(n => n.Equals(fileElement.Name, StringComparison.OrdinalIgnoreCase))) - { - fileElement.Patterns.SelectionItem.Pattern.AddToSelection(); + if (!names.Any(n => n.Equals(fileElement.Name, StringComparison.OrdinalIgnoreCase))) + continue; - if (++selectedCount >= names.Count) return; - } + fileElement.Patterns.SelectionItem.Pattern.AddToSelection(); + + if (++selectedCount >= names.Count) return true; } + + // At least one is selected. + return selectedCount > 0; } - public static bool CloseRandomTabOfAWindow(AutomationElement windowElement) + public static bool CloseRandomTabOfAWindow(AutomationElement window) { - var headerBar = Helper.DoUntilNotDefault(() => windowElement.FindFirstChild(c => c.ByClassName("Microsoft.UI.Content.DesktopChildSiteBridge"))); + var headerBar = Helper.DoUntilNotDefault(() => window.FindFirstChild(c => c.ByClassName("Microsoft.UI.Content.DesktopChildSiteBridge"))); if (headerBar == default) return false; var closeButton = Helper.DoUntilNotDefault(() => headerBar.FindFirstDescendant("CloseButton")); @@ -211,13 +215,6 @@ private static void GetHeaderElements(AutomationElement window, out AutomationEl suggestBox = headerBar.FindFirstChild("PART_AutoSuggestBox"); addressBar = suggestBox?.FindFirstChild(c => c.ByName("Address Bar")); } - private static RECT HideWindow(IntPtr hWnd) - { - WinApi.GetWindowRect(hWnd, out var originalRect); - // Move the window outside the screen (Hide) - WinApi.SetWindowPos(hWnd, IntPtr.Zero, -1000, -1000, 0, 0, WinApi.SWP_NOSIZE | WinApi.SWP_NOZORDER); - return originalRect; - } private void CloseAndNotifyNewWindow(AutomationElement element, Window window) { // the window suppose to have only one tab, so we should be okay. @@ -230,7 +227,6 @@ public void Dispose() { Automation.UnregisterAllEvents(); Automation.Dispose(); - GC.SuppressFinalize(this); } ~UiAutomation() { diff --git a/ExplorerTabUtility/Models/WindowHookVia.cs b/ExplorerTabUtility/Models/WindowHookVia.cs new file mode 100644 index 0000000..c2734be --- /dev/null +++ b/ExplorerTabUtility/Models/WindowHookVia.cs @@ -0,0 +1,7 @@ +namespace ExplorerTabUtility.Models; + +public enum WindowHookVia +{ + Ui, + Keys +} \ No newline at end of file diff --git a/ExplorerTabUtility/Program.cs b/ExplorerTabUtility/Program.cs index eba83ae..6f2f89d 100644 --- a/ExplorerTabUtility/Program.cs +++ b/ExplorerTabUtility/Program.cs @@ -1,5 +1,4 @@ -using System; -using System.Threading; +using System.Threading; using System.Windows.Forms; using ExplorerTabUtility.Forms; using ExplorerTabUtility.Helpers; diff --git a/ExplorerTabUtility/Properties/Settings.Designer.cs b/ExplorerTabUtility/Properties/Settings.Designer.cs index 80852ad..427135a 100644 --- a/ExplorerTabUtility/Properties/Settings.Designer.cs +++ b/ExplorerTabUtility/Properties/Settings.Designer.cs @@ -46,5 +46,29 @@ public bool WindowHook { this["WindowHook"] = value; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("True")] + public bool WindowViaUi { + get { + return ((bool)(this["WindowViaUi"])); + } + set { + this["WindowViaUi"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool WindowViaKeys { + get { + return ((bool)(this["WindowViaKeys"])); + } + set { + this["WindowViaKeys"] = value; + } + } } } diff --git a/ExplorerTabUtility/Properties/Settings.settings b/ExplorerTabUtility/Properties/Settings.settings index e73ce8e..d8eabcd 100644 --- a/ExplorerTabUtility/Properties/Settings.settings +++ b/ExplorerTabUtility/Properties/Settings.settings @@ -8,5 +8,11 @@ True + + True + + + False + \ No newline at end of file diff --git a/ExplorerTabUtility/WinAPI/WinApi.cs b/ExplorerTabUtility/WinAPI/WinApi.cs index 3d43cab..e69812a 100644 --- a/ExplorerTabUtility/WinAPI/WinApi.cs +++ b/ExplorerTabUtility/WinAPI/WinApi.cs @@ -12,19 +12,20 @@ namespace ExplorerTabUtility.WinAPI; public static class WinApi { - public const nint WM_KEYDOWN = 0x0100; // Key down flag - - public const int KEYEVENTF_KEYUP = 0x0002; // EventKey up flag - public const int SW_SHOWNOACTIVATE = 4; // Show window but not activated + public const int EVENT_OBJECT_CREATE = 0x8000; public const int VK_WIN = 0x5B; // Windows key code - public const int VK_CONTROL = 0x11; // CTRL key code public const int VK_E = 0x45; // E key code - public const int VK_T = 0x54; // T key code - public static uint SWP_NOSIZE = 0x0001; - public static uint SWP_NOZORDER = 0x0004; + public const int WM_KEYDOWN = 0x0100; // Key down flag + public const int WM_SETFOCUS = 0x0007; // Set Keyboard focus + + public const int SW_HIDE = 0; // Hide window + public const int SW_SHOWNOACTIVATE = 4; // Show window but not activated + public const int SWP_NOSIZE = 0x0001; // Retains the current size + public const int SWP_NOZORDER = 0x0004; // Retains the current Z order + public const int GW_ENABLEDPOPUP = 6; // Get the popup window owned by the specified window [DllImport("kernel32.dll")] public static extern nint LoadLibrary(string lpFileName); @@ -32,6 +33,11 @@ public static class WinApi [DllImport("kernel32.dll", CharSet = CharSet.Auto)] public static extern bool FreeLibrary(nint hModule); + [DllImport("user32.dll")] + public static extern nint SetWinEventHook(uint eventMin, uint eventMax, nint hmodWinEventProc, WinEventDelegate lpfnWinEventProc, uint idProcess, uint idThread, uint dwFlags); + + [DllImport("user32.dll")] + public static extern bool UnhookWinEvent(nint hWinEventHook); [DllImport("user32.dll", SetLastError = true)] public static extern nint SetWindowsHookEx(WinHookType HookType, HookProc lpfn, nint hMod, uint dwThreadId); @@ -43,18 +49,14 @@ public static class WinApi [DllImport("user32.dll", SetLastError = true)] public static extern nint CallNextHookEx(nint hhk, int nCode, nint wParam, nint lParam); - [DllImport("user32.dll")] - public static extern IntPtr GetShellWindow(); - - [DllImport("user32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); - [DllImport("user32.dll", SetLastError = true)] public static extern nint FindWindow(string lpClassName, string? lpWindowName); [DllImport("user32.dll", SetLastError = true)] - public static extern IntPtr FindWindowEx(IntPtr parentHandle, IntPtr childAfter, string className, string? windowTitle); + public static extern nint FindWindowEx(nint parentHandle, nint childAfter, string className, string? windowTitle); + + [DllImport("user32.dll")] + public static extern nint GetWindow(nint hWnd, uint uCmd); [DllImport("user32.dll")] public static extern bool ShowWindow(nint handle, int nCmdShow); @@ -66,45 +68,43 @@ public static class WinApi public static extern bool SetWindowPos(nint hWnd, nint hWndInsertAfter, int x, int Y, int cx, int cy, uint wFlags); [DllImport("user32.dll", SetLastError = true)] - public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); + public static extern bool GetWindowRect(nint hWnd, out RECT lpRect); [DllImport("user32.dll")] public static extern bool IsIconic(nint handle); - [DllImport("user32.dll")] - public static extern uint GetWindowThreadProcessId(nint hWnd, out nint processId); - [DllImport("user32.dll")] public static extern uint RealGetWindowClass(nint hwnd, StringBuilder pszType, uint cchType); - [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] - public static extern void keybd_event(uint bVk, uint bScan, uint dwFlags, uint dwExtraInfo); + [return: MarshalAs(UnmanagedType.Bool)] + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] + public static extern bool PostMessage(nint hWnd, uint Msg, nint wParam, nint lParam); - public static IntPtr GetAnotherExplorerWindow(IntPtr currentWindow) + public static nint GetAnotherExplorerWindow(nint currentWindow) { return currentWindow == default ? FindWindow("CabinetWClass", default) : FindAllWindowsEx() .FirstOrDefault(window => window != currentWindow); } - public static IntPtr ListenForNewExplorerTab(IReadOnlyCollection currentTabs, int searchTimeMs = 1000) + public static nint ListenForNewExplorerTab(IReadOnlyCollection currentTabs, int searchTimeMs = 1000) { - return Helper.DoUntilNotDefault(() => + return Helper.DoUntilNotDefault(() => GetAllExplorerTabs() .Except(currentTabs) .FirstOrDefault(), searchTimeMs); } - public static List GetAllExplorerTabs() + public static List GetAllExplorerTabs() { - var tabs = new List(); + var tabs = new List(); foreach (var window in FindAllWindowsEx()) tabs.AddRange(FindAllWindowsEx("ShellTabWindowClass", window)); return tabs; } - public static IEnumerable FindAllWindowsEx(string className = "CabinetWClass", nint parent = 0, string? windowTitle = default) + public static IEnumerable FindAllWindowsEx(string className = "CabinetWClass", nint parent = 0, string? windowTitle = default) { var handle = IntPtr.Zero; do @@ -118,11 +118,25 @@ public static IEnumerable FindAllWindowsEx(string className = "CabinetWC } while (handle != default); } + /// + /// Hides the specified window by moving it outside the visible screen area. + /// + /// The handle to the window that needs to be hidden. + /// The original position and size of the window before it was hidden, represented as a RECT structure. + public static RECT HideWindow(nint hWnd) + { + GetWindowRect(hWnd, out var originalRect); + + // Move the window outside the screen (Hide) + SetWindowPos(hWnd, IntPtr.Zero, -1000, -1000, 0, 0, SWP_NOSIZE | SWP_NOZORDER); + return originalRect; + } + /// /// Restores the specified window to the foreground even if it was minimized. /// /// The handle to the window that needs to be restored to the foreground. - public static void RestoreWindowToForeground(IntPtr window) + public static void RestoreWindowToForeground(nint window) { //If Minimized if (IsIconic(window)) @@ -135,13 +149,19 @@ public static void RestoreWindowToForeground(IntPtr window) SetForegroundWindow(window); } - public static bool IsWindowStillHasClassName(IntPtr hWnd, string className) + public static string GetWindowClassName(nint hWnd, int maxClassNameLength = 254) { - if (hWnd == IntPtr.Zero) return false; + if (hWnd == IntPtr.Zero) return string.Empty; - var currentClassName = new StringBuilder(className.Length + 1); - _ = RealGetWindowClass(hWnd, currentClassName, (uint)(className.Length + 1)); + var className = new StringBuilder(maxClassNameLength); + _ = RealGetWindowClass(hWnd, className, (uint)(maxClassNameLength + 1)); + + return className.ToString(); + } + public static bool IsWindowHasClassName(nint hWnd, string className, StringComparison comparison = StringComparison.OrdinalIgnoreCase) + { + var currentClassName = GetWindowClassName(hWnd, className.Length); - return string.Equals(currentClassName.ToString(), className, StringComparison.OrdinalIgnoreCase); + return string.Equals(currentClassName, className, comparison); } } \ No newline at end of file