@@ -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