Skip to content

Commit 8d81f8f

Browse files
authored
Merge pull request #1 from Yusyuriv/main
Add Firefox support
2 parents 8d19f1d + 28e848f commit 8d81f8f

File tree

4 files changed

+335
-12
lines changed

4 files changed

+335
-12
lines changed

BrowserTabs/BrowserTab.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,19 @@ public bool ActivateTab()
5555
if (IsMinimized)
5656
NativeMethods.ShowWindow(Hwnd, NativeMethods.SW_RESTORE);
5757

58-
var selectionPattern = AutomationElement.GetCurrentPattern(SelectionItemPattern.Pattern) as SelectionItemPattern;
59-
if (selectionPattern != null)
58+
// Chromium
59+
if (AutomationElement.TryGetCurrentPattern(SelectionItemPattern.Pattern, out var pattern) && pattern is SelectionItemPattern selectionPattern)
6060
{
6161
selectionPattern.Select();
6262
return true;
6363
}
64+
65+
// Firefox
66+
if (AutomationElement.TryGetCurrentPattern(InvokePattern.Pattern, out var invokePattern) && invokePattern is InvokePattern invoke)
67+
{
68+
invoke.Invoke();
69+
return true;
70+
}
6471
}
6572
catch (ElementNotAvailableException ex)
6673
{

BrowserTabs/BrowserTabManager.cs

Lines changed: 317 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,90 @@ namespace BrowserTabs
1515
/// </summary>
1616
public class BrowserTabManager
1717
{
18+
private const string ChromiumName = "chromium";
19+
20+
private const string FirefoxName = "firefox";
21+
1822
private static readonly HashSet<string> ChromiumProcessNames = new(
19-
new[] { "msedge", "chrome", "brave", "vivaldi", "opera", "chromium" },
23+
new[] { "msedge", "chrome", "brave", "vivaldi", "opera", ChromiumName },
24+
StringComparer.OrdinalIgnoreCase);
25+
26+
private static readonly HashSet<string> FirefoxProcessNames = new(
27+
new[] { FirefoxName },
2028
StringComparer.OrdinalIgnoreCase);
2129

2230
/// <summary>
23-
/// Retrieves all open tabs from all Chromium-based browser windows,
31+
/// Retrieves open tabs from all Chromium-based and Firefox-based browser windows,
32+
/// </summary>
33+
/// <param name="cancellationToken">Token to cancel the operation.</param>
34+
/// <returns>List of BrowserTab objects representing each open tab.</returns>
35+
public static List<BrowserTab> GetAllTabs(CancellationToken cancellationToken = default)
36+
{
37+
var tabBag = new ConcurrentBag<BrowserTab>();
38+
39+
try
40+
{
41+
if (cancellationToken.IsCancellationRequested)
42+
return new List<BrowserTab>();
43+
44+
var browserWindows = GetAllBrowserWindows(cancellationToken);
45+
46+
if (cancellationToken.IsCancellationRequested)
47+
return new List<BrowserTab>();
48+
49+
Parallel.ForEach(
50+
browserWindows,
51+
new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount, CancellationToken = cancellationToken },
52+
window =>
53+
{
54+
cancellationToken.ThrowIfCancellationRequested();
55+
56+
try
57+
{
58+
var process = Process.GetProcessById(window.processId);
59+
var mainWindow = AutomationElement.FromHandle(window.hwnd);
60+
if (mainWindow is null)
61+
return;
62+
63+
var tabs = new List<BrowserTab>();
64+
if (window.browserTypeName == ChromiumName)
65+
{
66+
tabs = !IsWindowMinimized(window.hwnd)
67+
? GetTabsFromWindow(mainWindow, process, cancellationToken)
68+
: GetTabsFromWindowMinimized(mainWindow, process, window.hwnd, cancellationToken);
69+
}
70+
else if (window.browserTypeName == FirefoxName)
71+
{
72+
tabs = GetFirefoxTabsFromWindow(mainWindow, process, cancellationToken);
73+
}
74+
75+
for (int i = 0; i < tabs.Count; i++)
76+
{
77+
cancellationToken.ThrowIfCancellationRequested();
78+
79+
tabBag.Add(tabs[i]);
80+
}
81+
}
82+
catch (ArgumentException)
83+
{
84+
// Process might have exited, ignore
85+
}
86+
});
87+
}
88+
catch (OperationCanceledException)
89+
{
90+
return new List<BrowserTab>();
91+
}
92+
catch (Exception ex)
93+
{
94+
Console.Error.WriteLine($"Error getting Chromium tabs: {ex}");
95+
}
96+
97+
return new List<BrowserTab>(tabBag);
98+
}
99+
100+
/// <summary>
101+
/// Retrieves open tabs from all Chromium-based browser windows,
24102
/// using a unified logic to avoid duplicate or separate calls.
25103
/// </summary>
26104
/// <param name="cancellationToken">Token to cancel the operation.</param>
@@ -82,6 +160,68 @@ public static List<BrowserTab> GetAllChromiumTabs(CancellationToken cancellation
82160
return new List<BrowserTab>(tabBag);
83161
}
84162

163+
/// <summary>
164+
/// Retrieves open tabs from all Firefox-based browser windows,
165+
/// using a unified logic to avoid duplicate or separate calls.
166+
/// </summary>
167+
/// <param name="cancellationToken">Token to cancel the operation.</param>
168+
/// <returns>List of BrowserTab objects representing each open tab.</returns>
169+
public static List<BrowserTab> GetAllFirefoxTabs(CancellationToken cancellationToken = default)
170+
{
171+
var tabBag = new ConcurrentBag<BrowserTab>();
172+
173+
try
174+
{
175+
if (cancellationToken.IsCancellationRequested)
176+
return new List<BrowserTab>();
177+
178+
var browserWindows = GetAllFirefoxWindows(cancellationToken);
179+
180+
if (cancellationToken.IsCancellationRequested)
181+
return new List<BrowserTab>();
182+
183+
Parallel.ForEach(
184+
browserWindows,
185+
new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount, CancellationToken = cancellationToken },
186+
window =>
187+
{
188+
cancellationToken.ThrowIfCancellationRequested();
189+
190+
try
191+
{
192+
var process = Process.GetProcessById(window.processId);
193+
var mainWindow = AutomationElement.FromHandle(window.hwnd);
194+
if (mainWindow is null)
195+
return;
196+
197+
var tabs = GetFirefoxTabsFromWindow(mainWindow, process, cancellationToken);
198+
199+
for (int i = 0; i < tabs.Count; i++)
200+
{
201+
cancellationToken.ThrowIfCancellationRequested();
202+
203+
tabBag.Add(tabs[i]);
204+
}
205+
}
206+
catch (ArgumentException)
207+
{
208+
// Process might have exited, ignore
209+
}
210+
});
211+
}
212+
catch (OperationCanceledException)
213+
{
214+
return new List<BrowserTab>();
215+
}
216+
catch (Exception ex)
217+
{
218+
Console.Error.WriteLine($"Error getting Firefox tabs: {ex}");
219+
}
220+
221+
return new List<BrowserTab>(tabBag);
222+
}
223+
224+
85225
/// <summary>
86226
/// Retrieves all tabs from a specific browser window.
87227
/// </summary>
@@ -152,6 +292,55 @@ private static List<BrowserTab> GetTabsFromWindow(AutomationElement mainWindow,
152292
return tabs;
153293
}
154294

295+
/// <summary>
296+
/// Retrieves all tabs from Firefox browser window.
297+
/// </summary>
298+
/// <param name="mainWindow">AutomationElement representing the browser window.</param>
299+
/// <param name="process">Process object for the browser.</param>
300+
/// <param name="cancellationToken">Token to cancel the operation.</param>
301+
/// <returns>List of BrowserTab objects found in the window.</returns>
302+
private static List<BrowserTab> GetFirefoxTabsFromWindow(AutomationElement mainWindow, Process process, CancellationToken cancellationToken)
303+
{
304+
var tabs = new List<BrowserTab>();
305+
try
306+
{
307+
if (cancellationToken.IsCancellationRequested)
308+
return new List<BrowserTab>();
309+
310+
// Firefox: In both horizontal and vertical tabs, Tab element with TabItem elements is always
311+
// the first Tab element, so we can find it and then look through its children.
312+
var tabElement = mainWindow.FindFirst(TreeScope.Descendants,
313+
new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Tab));
314+
315+
if (tabElement == null)
316+
return tabs;
317+
318+
if (cancellationToken.IsCancellationRequested)
319+
return new List<BrowserTab>();
320+
321+
var tabItems = tabElement.FindAll(TreeScope.Children, new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.TabItem));
322+
323+
for (int i = 0; i < tabItems.Count; i++)
324+
{
325+
cancellationToken.ThrowIfCancellationRequested();
326+
327+
var tab = CreateTabFromElement(tabItems[i], process, i);
328+
if (tab != null)
329+
tabs.Add(tab);
330+
}
331+
}
332+
catch (ElementNotAvailableException ex)
333+
{
334+
Console.Error.WriteLine($"Element not available: {ex}");
335+
}
336+
catch (Exception ex)
337+
{
338+
Console.Error.WriteLine($"Error getting tabs from window: {ex}");
339+
}
340+
341+
return tabs;
342+
}
343+
155344
/// <summary>
156345
/// Retrieves all tabs from a minimized browser window, specifically handling Edge's minimized tabs.
157346
/// This is currently unable to retrieve tabs from minimized windows in Chrome, and possibly other browsers like Brave, etc.
@@ -265,6 +454,72 @@ private static List<BrowserTab> GetTabsFromWindowMinimized(AutomationElement mai
265454
}
266455
}
267456

457+
/// <summary>
458+
/// Finds all top-level browser windows for Chromium and Firefox type browsers.
459+
/// </summary>
460+
/// <param name="cancellationToken">Token to cancel the operation.</param>
461+
/// <returns>List of tuples containing window handle and process ID.</returns>
462+
private static List<(IntPtr hwnd, int processId, string browserTypeName)> GetAllBrowserWindows(CancellationToken cancellationToken)
463+
{
464+
var browserWindows = new ConcurrentBag<(IntPtr, int, string)>();
465+
var windowHandles = new List<(IntPtr hwnd, uint pid)>();
466+
467+
if (cancellationToken.IsCancellationRequested)
468+
return new List<(IntPtr, int, string)>();
469+
470+
Task.Run(() =>
471+
{
472+
NativeMethods.EnumWindows((hwnd, lParam) =>
473+
{
474+
cancellationToken.ThrowIfCancellationRequested();
475+
476+
uint pid;
477+
NativeMethods.GetWindowThreadProcessId(hwnd, out pid);
478+
windowHandles.Add((hwnd, pid));
479+
480+
return true;
481+
482+
}, IntPtr.Zero);
483+
}, cancellationToken).Wait(cancellationToken);
484+
485+
if (cancellationToken.IsCancellationRequested)
486+
return new List<(IntPtr, int, string)>();
487+
488+
Parallel.ForEach(
489+
windowHandles,
490+
new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount, CancellationToken = cancellationToken },
491+
window =>
492+
{
493+
cancellationToken.ThrowIfCancellationRequested();
494+
495+
try
496+
{
497+
var process = Process.GetProcessById((int)window.pid);
498+
if (ChromiumProcessNames.Contains(process.ProcessName) || FirefoxProcessNames.Contains(process.ProcessName))
499+
{
500+
int length = NativeMethods.GetWindowTextLength(window.hwnd);
501+
if (length > 0)
502+
{
503+
if (process.ProcessName == FirefoxName)
504+
{
505+
browserWindows.Add((window.hwnd, (int)window.pid, FirefoxName));
506+
}
507+
else
508+
{
509+
browserWindows.Add((window.hwnd, (int)window.pid, ChromiumName));
510+
}
511+
}
512+
}
513+
}
514+
catch (ArgumentException)
515+
{
516+
// Process might have exited, ignore
517+
}
518+
});
519+
520+
return new List<(IntPtr, int, string)>(browserWindows);
521+
}
522+
268523
/// <summary>
269524
/// Finds all top-level Chromium browser windows on the system.
270525
/// </summary>
@@ -324,6 +579,66 @@ private static List<BrowserTab> GetTabsFromWindowMinimized(AutomationElement mai
324579
return new List<(IntPtr, int)>(browserWindows);
325580
}
326581

582+
/// <summary>
583+
/// Finds all top-level Firefox browser windows on the system.
584+
/// </summary>
585+
/// <param name="cancellationToken">Token to cancel the operation.</param>
586+
/// <returns>List of tuples containing window handle and process ID.</returns>
587+
private static List<(IntPtr hwnd, int processId)> GetAllFirefoxWindows(CancellationToken cancellationToken)
588+
{
589+
var browserWindows = new ConcurrentBag<(IntPtr, int)>();
590+
var windowHandles = new List<(IntPtr hwnd, uint pid)>();
591+
592+
if (cancellationToken.IsCancellationRequested)
593+
return new List<(IntPtr, int)>();
594+
595+
Task.Run(() =>
596+
{
597+
NativeMethods.EnumWindows((hwnd, lParam) =>
598+
{
599+
cancellationToken.ThrowIfCancellationRequested();
600+
601+
uint pid;
602+
NativeMethods.GetWindowThreadProcessId(hwnd, out pid);
603+
windowHandles.Add((hwnd, pid));
604+
605+
return true;
606+
607+
}, IntPtr.Zero);
608+
}, cancellationToken).Wait(cancellationToken);
609+
610+
if (cancellationToken.IsCancellationRequested)
611+
return new List<(IntPtr, int)>();
612+
613+
Parallel.ForEach(
614+
windowHandles,
615+
new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount, CancellationToken = cancellationToken },
616+
window =>
617+
{
618+
cancellationToken.ThrowIfCancellationRequested();
619+
620+
try
621+
{
622+
var process = Process.GetProcessById((int)window.pid);
623+
if (FirefoxProcessNames.Contains(process.ProcessName))
624+
{
625+
int length = NativeMethods.GetWindowTextLength(window.hwnd);
626+
if (length > 0)
627+
{
628+
browserWindows.Add((window.hwnd, (int)window.pid));
629+
}
630+
}
631+
}
632+
catch (ArgumentException)
633+
{
634+
// Process might have exited, ignore
635+
}
636+
});
637+
638+
return new List<(IntPtr, int)>(browserWindows);
639+
}
640+
641+
327642
/// <summary>
328643
/// Determines whether the specified window is minimized.
329644
/// </summary>

0 commit comments

Comments
 (0)