Flow.Launcher/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsWalker.cs
2025-12-28 17:32:20 +01:00

117 lines
4.5 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Windows.Automation;
using BrowserTabs;
using static Flow.Launcher.Plugin.BrowserBookmark.Main;
namespace Flow.Launcher.Plugin.BrowserBookmark.Tabs;
/// <summary>
/// TabsWalker waits for a new browser's tab to appear.
/// It uses TabsCache to keep known tabs.
/// Note that browsers don't provide full control over this process, so we have to rely on heuristics and a "best effort" approach.
/// </summary>
internal class TabsWalker
{
private static readonly string ClassName = nameof(TabsTracker);
private readonly TimeSpan _tabRetryTimeout = TimeSpan.FromSeconds(4);
private readonly TimeSpan _tabRetryInterval = TimeSpan.FromMilliseconds(250);
private readonly TabsCache _cache = new();
private static IEnumerable<AutomationElement> FindAllValidTabs(AutomationElement mainWindow)
{
Condition tabCondition = new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.TabItem);
foreach (AutomationElement tab in mainWindow.FindAll(TreeScope.Descendants, tabCondition))
{
var name = tab.Current.Name;
if (string.IsNullOrWhiteSpace(name))
{
continue;
}
// on Chrome, there are kind of technical tabs that should be ignored
var className = tab.Current.ClassName;
if (className.Contains("bolt-tab", StringComparison.OrdinalIgnoreCase))
{
Context.API.LogDebug(ClassName, $"Skipping name='{name}', className='{className}'");
continue;
}
yield return tab;
}
}
private static BrowserTab InitiateTab(Process process, AutomationElement tab) => new()
{
Title = tab.Current.Name,
BrowserName = process.ProcessName,
Hwnd = process.MainWindowHandle,
AutomationElement = tab
};
public BrowserTab GetCurrentTabFromWindow(AutomationElement mainWindow, Process process, CancellationToken cancellationToken)
{
try
{
var sw = Stopwatch.StartNew();
var count = 1;
while (sw.Elapsed < _tabRetryTimeout && !cancellationToken.IsCancellationRequested)
{
Context.API.LogDebug(ClassName, $"Start searching for a new tab... Try no {count++}");
var tabs = FindAllValidTabs(mainWindow).ToList();
if (tabs.Count == 0)
{
Context.API.LogDebug(ClassName, "No valid tabs found");
Thread.Sleep(_tabRetryInterval);
continue;
}
if (_cache.Empty())
{
Context.API.LogDebug(ClassName, "First time filling the cache");
_cache.Add(tabs);
// Let's take the last one and assume this is the one that was created recently
// This is the best known approach as of today
// There might be some browsers' settings that change this behavior but weren't tested nor considered yet
//TODO: research browsers' settings and check if it may break current assumption of just taking the last tab
return InitiateTab(process, tabs.Last());
}
Context.API.LogDebug(ClassName, $"Found tabs: {tabs.Count}");
//TabsDebug.DumpElements(mainWindow, null, "Tab");
// searching from the end and looking for a tab not in the cache
for (var i = tabs.Count - 1; i >= 0; i--)
{
var tab = tabs[i];
if (!_cache.Contains(tab))
{
Context.API.LogDebug(ClassName, $"FOUND NEW TAB: name={tab.Current.Name}, className={tab.Current.ClassName}");
_cache.Add(tab);
return InitiateTab(process, tab);
}
}
Context.API.LogDebug(ClassName, "No new tab found");
Thread.Sleep(_tabRetryInterval);
}
Context.API.LogDebug(ClassName, "Timeout waiting for new tab");
}
catch (ElementNotAvailableException ex)
{
Context.API.LogException(ClassName, "Element not available", ex);
}
catch (Exception ex)
{
Context.API.LogException(ClassName, "Error getting current tab from window", ex);
}
return null;
}
}