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