| | | 1 | | using System; |
| | | 2 | | using System.Collections.Generic; |
| | | 3 | | using System.Text; |
| | | 4 | | using System.Diagnostics; |
| | | 5 | | using SwitchBlade.Contracts; |
| | | 6 | | using SwitchBlade.Services; |
| | | 7 | | |
| | | 8 | | namespace SwitchBlade.Core |
| | | 9 | | { |
| | | 10 | | public class WindowFinder : CachingWindowProviderBase |
| | | 11 | | { |
| | | 12 | | private readonly ISettingsService _settingsService; |
| | | 13 | | private readonly IWindowInterop _interop; |
| | 38 | 14 | | private IEnumerable<string> _dynamicExclusions = new List<string>(); |
| | | 15 | | |
| | 26 | 16 | | public override string PluginName => "WindowFinder"; |
| | 4 | 17 | | public override bool HasSettings => false; |
| | 1 | 18 | | public override bool IsUiaProvider => false; // Uses EnumWindows, not UIA |
| | | 19 | | |
| | 38 | 20 | | public WindowFinder(ISettingsService settingsService, IWindowInterop interop) |
| | 38 | 21 | | { |
| | 38 | 22 | | _settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService)); |
| | 37 | 23 | | _interop = interop ?? throw new ArgumentNullException(nameof(interop)); |
| | 36 | 24 | | } |
| | | 25 | | |
| | | 26 | | public override void SetExclusions(IEnumerable<string> exclusions) |
| | 2 | 27 | | { |
| | 2 | 28 | | _dynamicExclusions = exclusions; |
| | 2 | 29 | | } |
| | | 30 | | |
| | | 31 | | public override void Initialize(IPluginContext context) |
| | 24 | 32 | | { |
| | 24 | 33 | | base.Initialize(context); |
| | | 34 | | // Note: SettingsService is now injected via constructor, not through Initialize |
| | 24 | 35 | | } |
| | | 36 | | |
| | | 37 | | public override void ReloadSettings() |
| | 1 | 38 | | { |
| | | 39 | | // No plugin-specific settings to reload |
| | 1 | 40 | | } |
| | | 41 | | |
| | | 42 | | |
| | | 43 | | protected override IEnumerable<WindowItem> ScanWindowsCore() |
| | 12 | 44 | | { |
| | 12 | 45 | | var results = new List<WindowItem>(); |
| | 13 | 46 | | if (_settingsService == null) return results; // Add safety |
| | | 47 | | |
| | 11 | 48 | | var excluded = new HashSet<string>(_settingsService.Settings.ExcludedProcesses, StringComparer.OrdinalIgnore |
| | | 49 | | |
| | | 50 | | // Note: Browser processes are now managed by the ChromeTabFinder plugin. |
| | | 51 | | // To prevent duplicate windows, add browser process names to ExcludedProcesses in Settings. |
| | | 52 | | |
| | | 53 | | unsafe bool EnumCallback(IntPtr hwnd, IntPtr lParam) |
| | 11 | 54 | | { |
| | 11 | 55 | | if (!_interop.IsWindowVisible(hwnd)) |
| | 1 | 56 | | return true; |
| | | 57 | | |
| | | 58 | | // Bleeding edge optimization: Use stackalloc for zero-allocation title retrieval |
| | | 59 | | // Max window title length is technically 256, but can be larger. 512 is safe. |
| | | 60 | | const int simplifyTitleBuffer = 512; |
| | 10 | 61 | | char* buffer = stackalloc char[simplifyTitleBuffer]; |
| | | 62 | | |
| | 10 | 63 | | int length = _interop.GetWindowTextUnsafe(hwnd, (IntPtr)buffer, simplifyTitleBuffer); |
| | 10 | 64 | | if (length == 0) |
| | 1 | 65 | | return true; |
| | | 66 | | |
| | | 67 | | // Perform "Program Manager" check without allocating string |
| | | 68 | | // Check if starts with "Program Manager" (length 15) |
| | 9 | 69 | | if (length == 15) |
| | 5 | 70 | | { |
| | | 71 | | // Fast manual check |
| | | 72 | | // "Program Manager" |
| | 5 | 73 | | bool match = true; |
| | 5 | 74 | | string pm = "Program Manager"; |
| | 70 | 75 | | for (int i = 0; i < 15; i++) |
| | 34 | 76 | | { |
| | 46 | 77 | | if (buffer[i] != pm[i]) { match = false; break; } |
| | 30 | 78 | | } |
| | 6 | 79 | | if (match) return true; |
| | 4 | 80 | | } |
| | | 81 | | |
| | | 82 | | // Get Process Name and Path (Optimized Interop handles caching and minimal allocations internally) |
| | 8 | 83 | | string processName = "Window"; |
| | 8 | 84 | | string? executablePath = null; |
| | | 85 | | try |
| | 8 | 86 | | { |
| | | 87 | | uint pid; |
| | 8 | 88 | | _interop.GetWindowThreadProcessId(hwnd, out pid); |
| | 7 | 89 | | if (pid != 0) |
| | 5 | 90 | | { |
| | 5 | 91 | | (processName, executablePath) = _interop.GetProcessInfo(pid); |
| | 5 | 92 | | } |
| | 7 | 93 | | } |
| | 1 | 94 | | catch |
| | 1 | 95 | | { |
| | | 96 | | // Ignore access denied errors etc. |
| | 1 | 97 | | } |
| | | 98 | | |
| | | 99 | | // fast-path rejection before allocating title string |
| | 8 | 100 | | if (excluded.Contains(processName) || _dynamicExclusions.Contains(processName, StringComparer.OrdinalIgn |
| | 2 | 101 | | { |
| | 2 | 102 | | return true; |
| | | 103 | | } |
| | | 104 | | |
| | | 105 | | // Only allocate string if we are keeping the window |
| | 6 | 106 | | string title = new string(buffer, 0, length); |
| | | 107 | | |
| | | 108 | | // Debug log |
| | 6 | 109 | | base.Logger?.Log($"Included Window: '{title}', Process: '{processName}' (Exclusions: {string.Join(",", _ |
| | | 110 | | |
| | 6 | 111 | | results.Add(new WindowItem |
| | 6 | 112 | | { |
| | 6 | 113 | | Hwnd = hwnd, |
| | 6 | 114 | | Title = title, |
| | 6 | 115 | | ProcessName = processName, |
| | 6 | 116 | | ExecutablePath = executablePath, |
| | 6 | 117 | | Source = this |
| | 6 | 118 | | }); |
| | | 119 | | |
| | 6 | 120 | | return true; |
| | 11 | 121 | | } |
| | | 122 | | |
| | 11 | 123 | | _interop.EnumWindows(EnumCallback, IntPtr.Zero); |
| | | 124 | | |
| | 11 | 125 | | return results; |
| | 12 | 126 | | } |
| | | 127 | | |
| | | 128 | | public override void ActivateWindow(WindowItem windowItem) |
| | 1 | 129 | | { |
| | | 130 | | // Robust window activation using the improved helper |
| | 1 | 131 | | _interop.ForceForegroundWindow(windowItem.Hwnd); |
| | 1 | 132 | | } |
| | | 133 | | |
| | | 134 | | protected override int GetPid(IntPtr hwnd) |
| | 8 | 135 | | { |
| | | 136 | | try |
| | 8 | 137 | | { |
| | 8 | 138 | | _interop.GetWindowThreadProcessId(hwnd, out uint pid); |
| | 5 | 139 | | return (int)pid != 0 ? (int)pid : -1; |
| | | 140 | | } |
| | 3 | 141 | | catch |
| | 3 | 142 | | { |
| | 3 | 143 | | return -1; |
| | | 144 | | } |
| | 8 | 145 | | } |
| | | 146 | | |
| | | 147 | | protected override (string ProcessName, string? ExecutablePath) GetProcessInfo(uint pid) |
| | 4 | 148 | | { |
| | 6 | 149 | | if ((int)pid == -1) return ("Window", null); |
| | | 150 | | |
| | | 151 | | try |
| | 2 | 152 | | { |
| | 2 | 153 | | return _interop.GetProcessInfo(pid); |
| | | 154 | | } |
| | 2 | 155 | | catch |
| | 2 | 156 | | { |
| | 2 | 157 | | return ("Window", null); |
| | | 158 | | } |
| | 4 | 159 | | } |
| | | 160 | | |
| | | 161 | | protected override bool IsWindowValid(IntPtr hwnd) |
| | 1 | 162 | | { |
| | 1 | 163 | | return _interop.IsWindowVisible(hwnd); |
| | 1 | 164 | | } |
| | | 165 | | } |
| | | 166 | | } |