diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs
index e8235764c..7831acbc7 100644
--- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs
+++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs
@@ -28,7 +28,7 @@ public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContex
private static bool _initialized = false;
- private readonly TabsTracker _tabsTracker = new();
+ private static readonly TabsReservationService _tabsReservationService = new();
public void Init(PluginInitContext context)
{
@@ -61,8 +61,13 @@ public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContex
}
LoadBookmarksIfEnabled();
+ SetReuseTabs(_settings.ReuseTabs);
+ }
- _tabsTracker.Init();
+ public static void SetReuseTabs(bool reuseTabs)
+ {
+ _tabsReservationService.EnableTracking(reuseTabs);
+ _settings.ReuseTabs = reuseTabs;
}
private static void LoadBookmarksIfEnabled()
@@ -97,7 +102,7 @@ public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContex
if (!topResults)
{
// Since we mixed chrome and firefox bookmarks, we should order them again
- return _tabsTracker.InjectExistingTabs(_settings, _cachedBookmarks
+ return _tabsReservationService.InjectExistingTabs(_settings, _cachedBookmarks
.Select(
c => new Result
{
@@ -109,7 +114,7 @@ public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContex
Score = BookmarkLoader.MatchProgram(c, param).Score,
Action = _ =>
{
- _tabsTracker.OpenUrlAndTrack(_settings, c.Url);
+ _tabsReservationService.OpenUrlAndTrack(_settings, c.Url);
return true;
},
ContextData = new BookmarkAttributes { Url = c.Url }
@@ -120,7 +125,7 @@ public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContex
}
else
{
- return _tabsTracker.InjectExistingTabs(_settings, _cachedBookmarks
+ return _tabsReservationService.InjectExistingTabs(_settings, _cachedBookmarks
.Select(
c => new Result
{
@@ -132,7 +137,7 @@ public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContex
Score = 5,
Action = _ =>
{
- _tabsTracker.OpenUrlAndTrack(_settings, c.Url);
+ _tabsReservationService.OpenUrlAndTrack(_settings, c.Url);
return true;
},
ContextData = new BookmarkAttributes { Url = c.Url }
@@ -265,7 +270,7 @@ public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContex
public void Dispose()
{
- _tabsTracker.Dispose();
+ _tabsReservationService.Dispose();
DisposeFileWatchers();
}
diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsCache.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsCache.cs
index a4eba2799..52de5ed5d 100644
--- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsCache.cs
+++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsCache.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Windows.Automation;
@@ -7,90 +8,129 @@ using static Flow.Launcher.Plugin.BrowserBookmark.Main;
namespace Flow.Launcher.Plugin.BrowserBookmark.Tabs;
///
-/// Keeps record of all known browser's tabs.
-/// It is used by TabsWalker to identify new tabs as they appear.
+/// TabsCache keeps record of all known browser's tabs in a single web browser window.
///
-internal class TabsCache
+public class TabsCache
{
private static readonly string ClassName = nameof(TabsCache);
- private readonly HashSet _knownTabs = [];
- private readonly Lock _sync = new();
- public static string RuntimeIdToKey(int[] runtimeId)
+ // UIA is unreliable. It may return zero elements or a subset of elements.
+ // Thus removal of an AutomationElement from cache SHOULD NOT be done immediately but rather after a few times the element no longer exists.
+ private static int _removeAtAge = 3;
+
+ private readonly Lock _sync = new();
+ private Dictionary _elementToInfo = [];
+ private SortedDictionary _indexToElement = [];
+ public bool Valid { get; private set; } = false;
+
+ private class Info(int index)
{
- return string.Join("-", runtimeId);
+ public int Index { get; init; } = index;
+ public int Age { get; set; } = 0;
}
- public static string RuntimeIdToKey(AutomationElement elem)
+ private static string TryName(AutomationElement element)
{
try
{
- return elem != null ? RuntimeIdToKey(elem.GetRuntimeId()) : null;
+ return element.Current.Name;
+ }
+ catch (Exception e)
+ {
+ return e.GetType().ToString();
+ }
+ }
+
+ private static bool Destroyed(AutomationElement element)
+ {
+ try
+ {
+ var _ = element.Current.Name;
}
catch (ElementNotAvailableException)
{
- return null;
- }
- }
-
- public bool Empty()
- {
- lock (_sync)
- {
- return _knownTabs.Count == 0;
- }
- }
-
- public void Add(AutomationElement tab)
- {
- lock (_sync)
- {
- var key = RuntimeIdToKey(tab);
- if (key != null)
- {
- Context.API.LogDebug(ClassName, $"TABS:{key}:Adding to cache: {tab.Current.Name}");
- _knownTabs.Add(key);
- }
- }
- }
- public void Add(IEnumerable tabs)
- {
- foreach (var tab in tabs)
- {
- Add(tab);
- }
- }
-
- public bool Contains(AutomationElement tab)
- {
- return Contains(RuntimeIdToKey(tab));
- }
-
- public bool Contains(string runtimeId)
- {
- lock (_sync)
- {
- if (runtimeId != null)
- {
- return _knownTabs.Contains(runtimeId);
- }
+ return true;
}
return false;
}
- public void RemoveAllNonExistentTabs(AutomationElement rootElement, IEnumerable existingTabs)
+ public void Invalidate()
{
- if (rootElement == null || existingTabs == null)
- return;
-
- var rootKey = RuntimeIdToKey(rootElement);
- var existingKeys = existingTabs.Select(RuntimeIdToKey).Where(k => k != null).ToHashSet();
- var keysToRemove = _knownTabs.Where(t => t.StartsWith(rootKey) && !existingKeys.Contains(t));
-
- foreach (var key in keysToRemove)
+ lock (_sync)
{
- Context.API.LogDebug(ClassName, $"TABS:{key}:Removing from cache");
- _knownTabs.Remove(key);
+ Valid = false;
+ }
+ }
+
+ public List GetTabs()
+ {
+ lock (_sync)
+ {
+ return [.. _indexToElement.Values];
+ }
+ }
+
+ public AutomationElement TryGetTab(int index)
+ {
+ lock (_sync)
+ {
+ Context.API.LogDebug(ClassName, $"TABS:Checking if tab {index} exists in the cache of {_elementToInfo.Count} size and indices between {_indexToElement.Keys.FirstOrDefault()} and {_indexToElement.Keys.LastOrDefault()}");
+
+ if (_indexToElement.TryGetValue(index, out var tab))
+ {
+ return tab;
+ }
+ return null;
+ }
+ }
+
+ public int UpdateTabs(int lastAssignedIndex, List actualTabs, out List removedTabs)
+ {
+ lock (_sync)
+ {
+ Context.API.LogDebug(ClassName, $"TABS:Start comparing {actualTabs.Count} actual tabs to {_elementToInfo.Count} tabs in the cache; new tabs will start from {lastAssignedIndex+1}");
+
+ removedTabs = [];
+
+ var tabsToRemove = _elementToInfo.Where(t => !actualTabs.Contains(t.Key)).Select(t => t.Key).ToList();
+ var tabsToAdd = actualTabs.Where(t => !_elementToInfo.ContainsKey(t)).ToList();
+ var tabsToRevive = _elementToInfo.Where(t => actualTabs.Contains(t.Key) && t.Value.Age > 0).ToList();
+
+ foreach (var tabToRemove in tabsToRemove)
+ {
+ if (_elementToInfo.TryGetValue(tabToRemove, out var info))
+ {
+ if (Destroyed(tabToRemove) || info.Age >= _removeAtAge)
+ {
+ Context.API.LogDebug(ClassName, $"TABS:Removing {TryName(tabToRemove)} from cache");
+ _elementToInfo.Remove(tabToRemove);
+ _indexToElement.Remove(info.Index);
+ removedTabs.Add(tabToRemove);
+ }
+ else
+ {
+ Context.API.LogDebug(ClassName, $"TABS:Aging {TryName(tabToRemove)} in cache (got age {info.Age + 1}, will be removed at age {_removeAtAge} or on ElementNotAvailableException)");
+ _elementToInfo[tabToRemove].Age++;
+ }
+ }
+ }
+
+ foreach (var tabToAdd in tabsToAdd)
+ {
+ Context.API.LogDebug(ClassName, $"TABS:Adding {TryName(tabToAdd)} to cache");
+ var newIndex = ++lastAssignedIndex;
+ _elementToInfo[tabToAdd] = new Info(newIndex);
+ _indexToElement[newIndex] = tabToAdd;
+ }
+
+ foreach (var tabtoRevive in tabsToRevive)
+ {
+ Context.API.LogDebug(ClassName, $"TABS:Reset age of {TryName(tabtoRevive.Key)} as it appeared again");
+ _elementToInfo[tabtoRevive.Key].Age = 0;
+ }
+
+ Valid = true;
+ return lastAssignedIndex;
}
}
}
diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsDebug.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsDebug.cs
deleted file mode 100644
index 30bdfa6a1..000000000
--- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsDebug.cs
+++ /dev/null
@@ -1,81 +0,0 @@
-using System;
-using System.Windows.Automation;
-using static Flow.Launcher.Plugin.BrowserBookmark.Main;
-
-namespace Flow.Launcher.Plugin.BrowserBookmark.Tabs;
-
-///
-/// Just for debugging.
-/// Call DumpElements whenever you need to analyze browser's internal structure.
-///
-internal class TabsDebug
-{
- private static readonly string ClassName = nameof(TabsDebug);
-
- public static void DumpTabs(AutomationElement parent)
- {
- DumpElements(parent, null, "Tab");
- }
-
- public static void DumpElements(AutomationElement parent, string classNameOnly = null, string controlTypeOnly = null, int indent = 0)
- {
- if (parent == null)
- return;
-
- AutomationElementCollection children;
- try
- {
- children = parent.FindAll(TreeScope.Children, Condition.TrueCondition);
- }
- catch (ElementNotAvailableException ex)
- {
- Context.API.LogDebug(ClassName, $"TABS:Parent not available: {ex.Message}");
- return;
- }
-
- foreach (AutomationElement child in children)
- {
- try
- {
- var ct = child.Current.ControlType;
- var type = ct?.ProgrammaticName?.Replace("ControlType.", "");
- var name = child.Current.Name;
- var className = child.Current.ClassName;
- var isOffscreen = child.Current.IsOffscreen;
- var isEnabled = child.Current.IsEnabled;
- var rect = child.Current.BoundingRectangle;
-
- var dump = true;
- if (!string.IsNullOrEmpty(classNameOnly) && className != classNameOnly)
- dump = false;
-
- if (!string.IsNullOrEmpty(controlTypeOnly) && type != controlTypeOnly)
- dump = false;
-
- if (dump)
- {
- Context.API.LogDebug(
- ClassName,
- $"TABS:{new string(' ', indent)}" +
- $"Type='{type}', " +
- $"ClassName='{className}', " +
- $"Name='{name}', " +
- $"IsOffscreen={isOffscreen}, " +
- $"IsEnabled={isEnabled}, " +
- $"BoundingRectangle={rect}"
- );
- }
-
- DumpElements(child, classNameOnly, controlTypeOnly, indent + 2);
- }
- catch (ElementNotAvailableException ex)
- {
- Context.API.LogDebug(ClassName, $"TABS:Child not available: {ex.Message}");
- }
- catch (Exception ex)
- {
- Context.API.LogException(ClassName, $"TABS:Unexpected error", ex);
- }
- }
- }
-}
diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsEventsDispatcher.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsEventsDispatcher.cs
new file mode 100644
index 000000000..77197a26c
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsEventsDispatcher.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Collections.Concurrent;
+using System.Diagnostics;
+using System.Threading;
+using Flow.Launcher.Plugin.BrowserBookmark.Tabs;
+using static Flow.Launcher.Plugin.BrowserBookmark.Main;
+
+///
+/// TabsEventsDispatcher handles events in a separate thread.
+/// This is to make handlers fast by not to blocking them.
+///
+public sealed class TabsEventsDispatcher : IDisposable
+{
+ private static readonly string ClassName = nameof(TabsEventsDispatcher);
+
+ private readonly BlockingCollection> _queue = [];
+ private readonly Thread _worker;
+ private readonly CancellationTokenSource _cts = new();
+ private readonly TabsReservationService _reservationService;
+
+ private readonly TimeSpan _tabRetryTimeout = TimeSpan.FromSeconds(10);
+ private readonly TimeSpan _tabRetryInterval = TimeSpan.FromMilliseconds(250);
+
+ public TabsEventsDispatcher(TabsTracker tracker, TabsReservationService reservationService)
+ {
+ _reservationService = reservationService;
+
+ _worker = new Thread(WorkerLoop)
+ {
+ IsBackground = true,
+ Name = "TabsEventsDispatcher"
+ };
+ _worker.Start();
+ }
+
+ public void Enqueue(string url, TabsReservationService.TokenForNewTab token)
+ {
+ if (!_queue.IsAddingCompleted)
+ _queue.Add(Tuple.Create(url, token));
+ }
+
+ void WorkerLoop()
+ {
+ try
+ {
+ foreach (var tuple in _queue.GetConsumingEnumerable(_cts.Token))
+ {
+ HandleUrl(tuple);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // shutting down
+ }
+ }
+
+ void HandleUrl(Tuple tuple)
+ {
+ var url = tuple.Item1;
+ var tokenForNewTab = tuple.Item2;
+
+ var sw = Stopwatch.StartNew();
+ var count = 1;
+
+ while (sw.Elapsed < _tabRetryTimeout && !_cts.Token.IsCancellationRequested)
+ {
+ var element = _reservationService.TryToResolveToken(tokenForNewTab, out var trackingInfo);
+ if (element != null)
+ {
+ _reservationService.RegisterTab(url, trackingInfo, element);
+ return;
+ }
+
+ Context.API.LogDebug(ClassName, $"TABS:No new tab found on try {count++}. Will sleep for {_tabRetryInterval.TotalMilliseconds} ms.");
+ Thread.Sleep(_tabRetryInterval);
+ }
+
+ Context.API.LogError(ClassName, "TABS:Timeout waiting for a new tab - assuming events are guaranteed and handled well this situation should not happen");
+ }
+
+ public void Dispose()
+ {
+ _cts.Cancel();
+ _queue.CompleteAdding();
+ _worker.Join();
+ _cts.Dispose();
+ _queue.Dispose();
+ }
+}
diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsFocusEventDispatcher.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsFocusEventDispatcher.cs
deleted file mode 100644
index 78c82c922..000000000
--- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsFocusEventDispatcher.cs
+++ /dev/null
@@ -1,90 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Diagnostics;
-using System.Threading;
-using System.Windows.Automation;
-using System.Windows.Input;
-using Flow.Launcher.Plugin.BrowserBookmark.Tabs;
-using static Flow.Launcher.Plugin.BrowserBookmark.Main;
-
-///
-/// TabsFocusEventDispatcher handles TabsTracker.OnFocusChanged events in a separate thread.
-/// This is to avoid sleeping in Automation.AddAutomationFocusChangedEventHandler.
-///
-internal sealed class TabsFocusEventDispatcher : IDisposable
-{
- private static readonly string ClassName = nameof(TabsFocusEventDispatcher);
- private readonly BlockingCollection> _queue = [];
- private readonly Thread _worker;
- private readonly CancellationTokenSource _cts = new();
- private readonly TabsWalker _walker;
- private readonly TabsTracker _tracker;
-
- public TabsFocusEventDispatcher(TabsWalker walker, TabsTracker tracker)
- {
- _worker = new Thread(WorkerLoop)
- {
- IsBackground = true,
- Name = "FocusEventWorker"
- };
- _worker.Start();
- _walker = walker;
- _tracker = tracker;
- }
-
- public void Enqueue(string url, AutomationElement element, int processId)
- {
- if (!_queue.IsAddingCompleted)
- _queue.Add(Tuple.Create(url, element, processId));
- }
-
- void WorkerLoop()
- {
- try
- {
- foreach (var element in _queue.GetConsumingEnumerable(_cts.Token))
- {
- HandleFocus(element);
- }
- }
- catch (OperationCanceledException)
- {
- // shutting down
- }
- }
-
- void HandleFocus(Tuple tuple)
- {
- var url = tuple.Item1;
- var rootElement = tuple.Item2;
- var processId = tuple.Item3;
-
- try
- {
- using var process = Process.GetProcessById(processId);
-
- var currentTab = _walker.GetCurrentTabFromWindow(rootElement, process, CancellationToken.None);
- if (currentTab != null)
- {
- _tracker.RegisterTab(url, rootElement, currentTab);
- }
- }
- catch (ArgumentException)
- {
- Context.API.LogError(ClassName, $"TABS:Process {processId} no longer runs");
- }
- catch (Exception ex)
- {
- Context.API.LogException(ClassName, "TABS:Exception", ex);
- }
- }
-
- public void Dispose()
- {
- _cts.Cancel();
- _queue.CompleteAdding();
- _worker.Join();
- _cts.Dispose();
- _queue.Dispose();
- }
-}
diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsReservationService.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsReservationService.cs
new file mode 100644
index 000000000..7f3549e11
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsReservationService.cs
@@ -0,0 +1,196 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Threading;
+using System.Windows.Automation;
+using BrowserTabs;
+using Flow.Launcher.Plugin.BrowserBookmark.Models;
+using static Flow.Launcher.Plugin.BrowserBookmark.Main;
+using static Flow.Launcher.Plugin.BrowserBookmark.Tabs.TabsTracker;
+
+namespace Flow.Launcher.Plugin.BrowserBookmark.Tabs;
+
+///
+/// TabsReservationService provides mapping between URLs and browser tabs.
+/// 1. You get a token while registering an URL
+/// 2. Later on you may replace token for corresponding browser tab after it appears
+/// TabsReservationService also integrates with BrowserBookmark's query by injecting activation of tabs instead of OpenUrl (InjectExistingTabs).
+///
+public class TabsReservationService : IDisposable
+{
+ private static readonly string ClassName = nameof(TabsReservationService);
+
+ private readonly Lock _sync = new();
+
+ private readonly Dictionary _tokens = [];
+
+ private readonly ConcurrentDictionary _urlToBrowserTab = [];
+ private readonly ConcurrentDictionary _automationElementToUrl = [];
+
+ private static TabsTracker _tabsTracker;
+
+ public TabsReservationService()
+ {
+ _tabsTracker = new TabsTracker(this);
+ }
+
+ ///
+ /// TokenForNewTab is kind of a promise that may be replaced for a real tab after it is finally created and available
+ ///
+ public class TokenForNewTab(int index)
+ {
+ public int Index { get; init; } = index;
+ }
+
+ ///
+ /// TokenForNewTabHandling is a utility class for proper handling of tokens for new tabs (TokenForNewTab)
+ ///
+ private class TokenForNewTabHandling(TokenForNewTab token)
+ {
+ public TokenForNewTab Token { get; init; } = token;
+ public int LastReturnedIndex { get; set; }
+ public int RequestedStill { get; set; } = 1;
+ }
+
+ public void OpenUrlAndTrack(Settings settings, string url)
+ {
+ if (settings.ReuseTabs)
+ {
+ _tabsTracker.MakeSnapshot(RegisterToken, url);
+ }
+
+ Context.API.OpenUrl(url);
+ }
+
+ public List InjectExistingTabs(Settings settings, List results)
+ {
+ if (!settings.ReuseTabs)
+ {
+ return results;
+ }
+
+ foreach (var r in results)
+ {
+ var bookmarkUrl = ((BookmarkAttributes)r.ContextData).Url;
+ if (_urlToBrowserTab.TryGetValue(bookmarkUrl, out var existingTab))
+ {
+ r.ContextData = existingTab;
+ r.Action = c =>
+ {
+ if (!existingTab.ActivateTab())
+ {
+ Context.API.LogError(ClassName, "TABS:Failed to activate a tab");
+ _urlToBrowserTab.Remove(bookmarkUrl, out _);
+ _automationElementToUrl.Remove(existingTab.AutomationElement, out _);
+
+ OpenUrlAndTrack(settings, bookmarkUrl);
+ }
+ return true;
+ };
+ }
+ }
+ return results;
+ }
+
+ private TokenForNewTab RegisterToken(int lastAssignedIndex)
+ {
+ lock (_sync)
+ {
+ if (_tokens.TryGetValue(lastAssignedIndex, out var tokenHandling))
+ {
+ ++tokenHandling.RequestedStill;
+ return tokenHandling.Token;
+ }
+
+ var token = new TokenForNewTab(lastAssignedIndex);
+ _tokens[lastAssignedIndex] = new TokenForNewTabHandling(token);
+ return token;
+ }
+ }
+
+ public AutomationElement TryToResolveToken(TokenForNewTab token, out TrackingInfo trackingInfo)
+ {
+ lock (_sync)
+ {
+ if (!_tokens.TryGetValue(token.Index, out var tokenHandling))
+ {
+ Context.API.LogError(ClassName, $"Trying to use an invalid token for index {token.Index}");
+ trackingInfo = null;
+ return null;
+ }
+
+ _tabsTracker.MakeSnapshot();
+
+ int expectedIndex = Math.Max(tokenHandling.LastReturnedIndex, token.Index) + 1;
+ var tab = _tabsTracker.TryGetTab(expectedIndex, out var foundInTrackingInfo);
+ trackingInfo = foundInTrackingInfo;
+ if (tab != null)
+ {
+ if (tokenHandling.RequestedStill <= 1)
+ {
+ _tokens.Remove(token.Index, out var _);
+ }
+ else
+ {
+ --tokenHandling.RequestedStill;
+ tokenHandling.LastReturnedIndex = expectedIndex;
+ }
+ }
+ return tab;
+ }
+ }
+
+ public void RegisterTab(string url, TrackingInfo trackingInfo, AutomationElement tab)
+ {
+ lock (_sync)
+ {
+ var currentTab = new BrowserTab
+ {
+ Title = tab.Current.Name,
+ BrowserName = trackingInfo.ProcessName,
+ Hwnd = trackingInfo.ProcessMainWindowHandle,
+ AutomationElement = tab
+ };
+
+ Context.API.LogDebug(ClassName, $"TABS:{RuntimeIdToKey(currentTab.AutomationElement)}:Registering {url} as tab: {currentTab.Title}");
+ _urlToBrowserTab[url] = currentTab;
+ _automationElementToUrl[currentTab.AutomationElement] = url;
+
+ // required to take the tab into account by Flow Launcher main UI search window
+ Context.API.ReQuery();
+ }
+ }
+
+ public void UnregisterTabs(IEnumerable elements)
+ {
+ lock (_sync)
+ {
+ foreach (var element in elements)
+ {
+ if (_automationElementToUrl.TryGetValue(element, out var url))
+ {
+ _urlToBrowserTab.Remove(url, out _);
+ _automationElementToUrl.Remove(element, out _);
+ }
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ _tabsTracker.Dispose();
+ }
+
+ public void EnableTracking(bool reuseTabs)
+ {
+ _tabsTracker.EnableTracking(reuseTabs);
+ if (reuseTabs)
+ {
+ lock (_sync)
+ {
+ _urlToBrowserTab.Clear();
+ _automationElementToUrl.Clear();
+ }
+ }
+ }
+}
diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsTracker.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsTracker.cs
index a8f67ba8d..857ab355f 100644
--- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsTracker.cs
+++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsTracker.cs
@@ -1,283 +1,356 @@
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
+using System.Linq;
using System.Threading;
using System.Windows.Automation;
-using System.Windows.Forms;
-using System.Windows.Input;
-using BrowserTabs;
-using Flow.Launcher.Plugin.BrowserBookmark.Models;
using static Flow.Launcher.Plugin.BrowserBookmark.Main;
+using static Flow.Launcher.Plugin.BrowserBookmark.Tabs.TabsReservationService;
namespace Flow.Launcher.Plugin.BrowserBookmark.Tabs;
-#nullable enable
-
///
-/// TabsTracker maps initial URLs into existing browser's tabs.
-/// The sequence of events:
-/// 1. OpenUrlAndTrack - before launching an URL it is remembered for later mapping to a browser's tab
-/// 2. OnFocusChanged - whenever a browser's window gets focused a new tab discovery is started and result is put into the UrlToBrowserTab map
-/// 3. InjectExistingTabs - iterates over BrowserBookmark's query result and replaces OpenUrl with ActivateTab for known, existing tabs
+/// TabsTracker builds full list of all browsers windows and their tabs.
+/// TabsTracker also maintains the lists (invalidates on events, updates lazily on demand)
///
public class TabsTracker : IDisposable
{
private static readonly string ClassName = nameof(TabsTracker);
- private static readonly HashSet chromiumProcessNames = new(["msedge", "chrome", "brave", "vivaldi", "opera", "chromium"], StringComparer.OrdinalIgnoreCase);
+
+ // Firefox - tested on version: 146.0.1
+ // Chrome - tested on version: 143.0.7499.170
+ // Edge - tested on version: 143.0.3650.96
+ // Brave - NOT tested
+ // Vivaldi - NOT tested
+ // Opera - NOT tested
private static readonly HashSet firefoxProcessNames = new(["firefox"], StringComparer.OrdinalIgnoreCase);
- private readonly TabsWalker _walker = new();
- private readonly Queue _expectedUrls = [];
- private Dictionary UrlToBrowserTab { get; } = [];
+ private static readonly HashSet chromiumProcessNames = new(["msedge", "chrome", "brave", "vivaldi", "opera", "chromium"], StringComparer.OrdinalIgnoreCase);
+
+ private bool _trackingEnabled = false;
+
private readonly Lock _sync = new();
+ private int _lastAssignedIndex;
+ private bool _windowsHandlerInitialized = false;
+ private Dictionary _browserWindowsTracked = [];
- private TabsFocusEventDispatcher? _focusHandlerDispatcher;
- private AutomationFocusChangedEventHandler? _focusHandler;
- private readonly HashSet _browserWindowsTracked = [];
- private bool _initialized;
+ private readonly ConcurrentQueue> _expectedUrls = [];
- public void OpenUrlAndTrack(Settings settings, string url)
+ private TabsEventsDispatcher _eventsDispatcher;
+ private ConcurrentQueue _structureInvalidations = [];
+
+ private TabsReservationService _service;
+
+ public TabsTracker(TabsReservationService service)
{
- if (settings.ReuseTabs)
- {
- ExpectUrl(url);
- }
- Context.API.OpenUrl(url);
+ _service = service;
}
- public List InjectExistingTabs(Settings settings, List results)
+ public static string RuntimeIdToKey(AutomationElement elem)
{
- if (!settings.ReuseTabs)
+ try
{
- return results;
+ return elem != null ? string.Join("-", elem.GetRuntimeId()) : null;
}
- foreach (var r in results)
+ catch (ElementNotAvailableException)
{
- var bookmarkUrl = ((BookmarkAttributes)r.ContextData).Url;
- var existingTab = GetExistingTab(bookmarkUrl);
- if (existingTab != null)
+ return null;
+ }
+ }
+
+ private static IEnumerable FindAllValidTabs(AutomationElement browserWindow)
+ {
+ Condition tabCondition = new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.TabItem);
+
+ foreach (AutomationElement tab in browserWindow.FindAll(TreeScope.Descendants, tabCondition))
+ {
+ var name = tab.Current.Name;
+ if (string.IsNullOrWhiteSpace(name))
{
- r.ContextData = existingTab;
- r.Action = c =>
- {
- if (!existingTab.ActivateTab())
- {
- Context.API.LogError(ClassName, $"TABS:{TabsCache.RuntimeIdToKey(existingTab.AutomationElement)}:Failed to activate");
- Remove(bookmarkUrl);
- OpenUrlAndTrack(settings, bookmarkUrl);
- }
- return true;
- };
+ continue;
}
+
+ // 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, $"TABS:Skipping name='{name}', className='{className}'");
+ continue;
+ }
+
+ yield return tab;
}
- return results;
}
- public void Init()
+ ///
+ /// TrackingInfo keeps context of a single browser window
+ ///
+ public class TrackingInfo(AutomationElement rootElement, StructureChangedEventHandler structureChangedHandler, AutomationEventHandler windowCloseHandler, string processName, nint processMainWindowHandle)
{
- if (_initialized)
- return;
-
- _focusHandlerDispatcher = new(_walker, this);
- _focusHandler = OnFocusChanged;
- Automation.AddAutomationFocusChangedEventHandler(_focusHandler);
- _initialized = true;
+ public AutomationElement RootElement { get; init; } = rootElement;
+ public StructureChangedEventHandler StructureChangedHandler { get; init; } = structureChangedHandler;
+ public AutomationEventHandler WindowCloseHandler { get; init; } = windowCloseHandler;
+ public string ProcessName { get; init; } = processName;
+ public nint ProcessMainWindowHandle { get; init; } = processMainWindowHandle;
+ public TabsCache Cache { get; init; } = new TabsCache();
}
public void Dispose()
{
- List windowsToUnsubscribe;
- lock (_sync)
- {
- windowsToUnsubscribe = [.. _browserWindowsTracked];
- }
-
- foreach (var wnd in windowsToUnsubscribe)
- {
- UnsubscribeStructureChangedForWindow(wnd);
- }
- if (_focusHandler != null)
- {
- Automation.RemoveAutomationFocusChangedEventHandler(_focusHandler);
- _focusHandler = null;
- _initialized = false;
- }
- _focusHandlerDispatcher?.Dispose();
+ DisableTracking();
}
- public void ExpectUrl(string url)
+ ///
+ /// Makes snapshot of all browsers windows and all their tabs.
+ /// Optionally it may register a token.
+ ///
+ public void MakeSnapshot(Func registerToken = null, string requestedUrl = null)
{
lock (_sync)
{
- _expectedUrls.Enqueue(url);
- }
- }
-
- private BrowserTab? GetExistingTab(string url)
- {
- lock (_sync)
- {
- if (UrlToBrowserTab.TryGetValue(url, out var existingTab))
+ EnsureHavingAllBrowsersWindows();
+ EnsureHavingAllBrowsersTabs();
+ if (registerToken != null)
{
- return existingTab;
+ var token = registerToken(_lastAssignedIndex);
+ _expectedUrls.Enqueue(Tuple.Create(requestedUrl, token));
}
}
- return null;
}
- private void Remove(string url)
+ private void EnsureHavingAllBrowsersWindows()
{
- lock (_sync)
+ // this is done once
+ // later on list is updated using WindowOpen / WindowClose events
+ if (!_windowsHandlerInitialized)
{
- UrlToBrowserTab.Remove(url);
+ Context.API.LogDebug(ClassName, "TABS:EnsureHavingAllBrowsersWindows initializing ...");
+
+ var desktop = AutomationElement.RootElement;
+ Automation.AddAutomationEventHandler(WindowPattern.WindowOpenedEvent, desktop, TreeScope.Children, OnWindowOpen);
+
+ var topLevelWindows = desktop.FindAll(TreeScope.Children, Condition.TrueCondition);
+ foreach (AutomationElement element in topLevelWindows)
+ {
+ HandleProcessStart(element);
+ }
+
+ _windowsHandlerInitialized = true;
}
}
- private void OnFocusChanged(object sender, AutomationFocusChangedEventArgs e)
+ private void OnWindowOpen(object src, AutomationEventArgs e)
{
+ Context.API.LogDebug(ClassName, "TABS:OnWindowOpen");
+ AutomationElement element = src as AutomationElement;
+ if (element != null)
+ HandleProcessStart(element);
+ }
+
+ private void OnWindowClose(AutomationElement element, object src, AutomationEventArgs e)
+ {
+ Context.API.LogDebug(ClassName, $"TABS:OnWindowClose {RuntimeIdToKey(element)}");
lock (_sync)
{
- if (_expectedUrls.Count == 0)
- return;
- }
+ bool structureChangesTracked = _browserWindowsTracked.TryGetValue(element, out var trackingInfo);
+ if (structureChangesTracked && trackingInfo.WindowCloseHandler != null)
+ {
+ Automation.RemoveAutomationEventHandler(WindowPattern.WindowClosedEvent, element, trackingInfo.WindowCloseHandler);
+ }
+ HandleProcessExit(element);
+ }
+ }
+
+ private static Process TryProcess(int processId)
+ {
try
{
- if (sender is not AutomationElement element)
- return;
-
- var pid = 0;
- try
- {
- pid = element.Current.ProcessId;
- }
- catch (ElementNotAvailableException)
- {
- return;
- }
-
- using var process = Process.GetProcessById(pid);
-
- var chromium = chromiumProcessNames.Contains(process.ProcessName);
- var firefox = firefoxProcessNames.Contains(process.ProcessName);
- if (!chromium && !firefox)
- return; // not a browser
-
- Context.API.LogDebug(ClassName, $"TABS:The active browser is {process.ProcessName}");
-
- var rootElement = AutomationElement.FromHandle(process.MainWindowHandle);
- if (rootElement == null)
- return;
-
- string? urlToBind;
- lock (_sync)
- {
- if (_expectedUrls.Count == 0)
- return;
-
- urlToBind = _expectedUrls.Dequeue();
- }
- if (urlToBind is null)
- return;
-
- Context.API.LogDebug(ClassName, $"TABS:Searching for... {urlToBind}");
-
- // Further handling requires waiting for tabs so its better to run it on a separate thread
- _focusHandlerDispatcher?.Enqueue(urlToBind, rootElement, pid);
+ return Process.GetProcessById(processId);
}
- catch (Exception ex)
+ catch (Exception)
{
- Context.API.LogException(ClassName, "TABS:Exception", ex);
+ return null;
}
}
- private void OnStructureChanged(object sender, StructureChangedEventArgs e)
+ private void HandleProcessStart(AutomationElement element)
{
- //if (e.StructureChangeType == StructureChangeType.ChildAdded || e.StructureChangeType == StructureChangeType.ChildrenBulkAdded)
- //{
- //}
- var eventRuntimeId = TabsCache.RuntimeIdToKey(e.GetRuntimeId());
+ var processId = element.Current.ProcessId;
+ using var process = TryProcess(processId);
+ if (process == null)
+ return;
+
+ string processName = process.ProcessName.ToLowerInvariant();
+ var chromium = chromiumProcessNames.Contains(processName);
+ var firefox = firefoxProcessNames.Contains(processName);
+ if (!chromium && !firefox)
+ return;
+
+ Context.API.LogDebug(ClassName, $"TABS:Found a browser window for {processName}, PID={processId}");
+ lock (_sync)
+ {
+ bool structureChangesTracked = _browserWindowsTracked.ContainsKey(element);
+ if (!structureChangesTracked)
+ {
+ SubscribeStructureChangedEventHandler(element, process);
+ }
+ }
+ }
+
+ private void HandleProcessExit(AutomationElement element)
+ {
+ lock (_sync)
+ {
+ bool structureChangesTracked = _browserWindowsTracked.TryGetValue(element, out var trackingInfo);
+ if (structureChangesTracked)
+ {
+ UnsubscribeStructureChangedEventHandler(element, trackingInfo);
+ }
+ }
+ }
+
+ private void SubscribeStructureChangedEventHandler(AutomationElement element, Process process)
+ {
+ void structureChangedHandler(object sender, StructureChangedEventArgs e)
+ {
+ OnStructureChanged(element, sender, e);
+ }
+
+ Automation.AddStructureChangedEventHandler(element, TreeScope.Subtree, structureChangedHandler);
+
+ void windowCloseHandler(object src, AutomationEventArgs e)
+ {
+ OnWindowClose(element, src, e);
+ }
+
+ _browserWindowsTracked[element] = new TrackingInfo(element, structureChangedHandler, windowCloseHandler, process.ProcessName, process.MainWindowHandle);
+
+ Automation.AddAutomationEventHandler(WindowPattern.WindowClosedEvent, element, TreeScope.Element, windowCloseHandler);
+
+ Context.API.LogDebug(ClassName, $"TABS:Window {RuntimeIdToKey(element)} SUBSCRIBED for StructureChanged events");
+ }
+
+ private void UnsubscribeStructureChangedEventHandler(AutomationElement element, TrackingInfo trackingInfo)
+ {
+ Automation.RemoveStructureChangedEventHandler(element, trackingInfo.StructureChangedHandler);
+ _service.UnregisterTabs(trackingInfo.Cache.GetTabs());
+ _browserWindowsTracked.Remove(element);
+
+ Context.API.LogDebug(ClassName, $"TABS:Window {RuntimeIdToKey(element)} UNSUBSCRIBED from StructureChanged events");
+ }
+
+ private void EnsureHavingAllBrowsersTabs()
+ {
+ Context.API.LogDebug(ClassName, "TABS:EnsureHavingAllBrowsersTabs ...");
+
+ var unique = _structureInvalidations.ToArray().Distinct().Where(_browserWindowsTracked.ContainsKey);
+ foreach (var element in unique)
+ {
+ _browserWindowsTracked[element].Cache.Invalidate();
+ }
+
+ foreach (var pair in _browserWindowsTracked)
+ {
+ if (!pair.Value.Cache.Valid)
+ {
+ try
+ {
+ _lastAssignedIndex = pair.Value.Cache.UpdateTabs(_lastAssignedIndex, [.. FindAllValidTabs(pair.Key)], out var removedTabs);
+ _service.UnregisterTabs(removedTabs);
+ }
+ catch (ElementNotAvailableException)
+ {
+ Context.API.LogError(ClassName, "ElementNotAvailableException while updating tabs");
+ }
+ }
+ }
+ }
+
+ private void OnStructureChanged(AutomationElement window, object sender, StructureChangedEventArgs e)
+ {
+ //Context.API.LogDebug(ClassName, $"TABS:Received {e.StructureChangeType.ToString()} on {sender}");
switch (e.StructureChangeType)
{
- // TODO: Consider ChildAdded to handle new tabs appearance instead of AutomationFocusChangedEventHandler
- // However think twice if it is worthwhile as the current approach based on focus might already be a good one
- //case StructureChangeType.ChildAdded:
- // Context.API.LogDebug(ClassName, $"TABS:StructureChangeType.ChildAdded occurred on {sender} for {eventRuntimeId}");
- // break;
- //case StructureChangeType.ChildrenBulkAdded:
- // Context.API.LogDebug(ClassName, $"TABS:StructureChangeType.ChildrenBulkAdded occurred on {sender} for {eventRuntimeId}");
- // break;
- //case StructureChangeType.ChildrenInvalidated:
- // Context.API.LogDebug(ClassName, $"TABS:StructureChangeType.ChildrenInvalidated occurred on {sender} for {eventRuntimeId}");
- // _walker.CheckTabExistence(eventRuntimeId, "StructureChangeType.ChildrenInvalidated");
- // break;
- //case StructureChangeType.ChildrenReordered:
- // Context.API.LogDebug(ClassName, $"TABS:StructureChangeType.ChildrenReordered occurred on {sender} for {eventRuntimeId}");
- // _walker.CheckTabExistence(eventRuntimeId, "StructureChangeType.ChildrenReordered");
- // break;
-
+ case StructureChangeType.ChildAdded:
+ case StructureChangeType.ChildrenBulkAdded:
+ case StructureChangeType.ChildrenInvalidated:
+ case StructureChangeType.ChildrenReordered:
case StructureChangeType.ChildRemoved:
case StructureChangeType.ChildrenBulkRemoved:
- AutomationElement? foundWindow = null;
- lock (_sync)
+ _structureInvalidations.Enqueue(window);
+ break;
+ }
+ switch (e.StructureChangeType)
+ {
+ case StructureChangeType.ChildAdded:
+ case StructureChangeType.ChildrenBulkAdded:
+ while (_expectedUrls.TryDequeue(out var tuple))
{
- foreach (var window in _browserWindowsTracked)
+ lock (_sync)
{
- var windowRuntimeId = TabsCache.RuntimeIdToKey(window);
- if (windowRuntimeId != null && eventRuntimeId.StartsWith(windowRuntimeId))
- {
- foundWindow = window;
- }
+ _eventsDispatcher ??= new(this, _service);
+ _eventsDispatcher.Enqueue(tuple.Item1, tuple.Item2);
}
}
- if (foundWindow != null)
- {
- _walker.RescanTabsForContainer(foundWindow);
- }
break;
}
}
- private void OnWindowClosed(object sender, AutomationEventArgs e)
+ public AutomationElement TryGetTab(int expectedIndex, out TrackingInfo foundInTrackingInfo)
{
- var element = sender as AutomationElement;
- if (element == null)
- return;
- UnsubscribeStructureChangedForWindow(element);
- }
-
- private void UnsubscribeStructureChangedForWindow(AutomationElement wnd)
- {
- var contains = false;
lock (_sync)
{
- contains = _browserWindowsTracked.Contains(wnd);
- _browserWindowsTracked.Remove(wnd);
- }
- if (contains)
- {
- Context.API.LogDebug(ClassName, "TABS:Unsubscribe window from StructureChanged events");
- Automation.RemoveStructureChangedEventHandler(wnd, OnStructureChanged);
- _walker?.RemoveAllTabs(wnd);
+ foreach (var trackingInfo in _browserWindowsTracked.Values)
+ {
+ var tab = trackingInfo.Cache.TryGetTab(expectedIndex);
+ if (tab != null)
+ {
+ foundInTrackingInfo = trackingInfo;
+ return tab;
+ }
+ }
+ foundInTrackingInfo = null;
+ return null;
}
}
- internal void RegisterTab(string url, AutomationElement rootElement, BrowserTab currentTab)
+ public void EnableTracking(bool enable)
{
lock (_sync)
{
- Context.API.LogDebug(ClassName, $"TABS:{TabsCache.RuntimeIdToKey(currentTab.AutomationElement)}:Registering {url} as tab: {currentTab.Title}");
- UrlToBrowserTab[url] = currentTab;
+ if (_trackingEnabled == enable)
+ return;
- // required to take the tab into account by Flow Launcher main UI search window
- Context.API.ReQuery();
+ _trackingEnabled = enable;
+ if (!_trackingEnabled)
+ {
+ DisableTracking();
+ }
}
+ }
- Automation.AddStructureChangedEventHandler(rootElement, TreeScope.Subtree, OnStructureChanged);
- Automation.AddAutomationEventHandler(WindowPattern.WindowClosedEvent, rootElement, TreeScope.Subtree, OnWindowClosed);
-
+ private void DisableTracking()
+ {
lock (_sync)
{
- _browserWindowsTracked.Add(rootElement);
+ _eventsDispatcher?.Dispose();
+ _eventsDispatcher = null;
+
+ if (_windowsHandlerInitialized)
+ {
+ Automation.RemoveAutomationEventHandler(WindowPattern.WindowOpenedEvent, AutomationElement.RootElement, OnWindowOpen);
+
+ foreach (var tracking in _browserWindowsTracked)
+ {
+ UnsubscribeStructureChangedEventHandler(tracking.Key, tracking.Value);
+ }
+ _browserWindowsTracked.Clear();
+ _structureInvalidations.Clear();
+ _expectedUrls.Clear();
+
+ _windowsHandlerInitialized = false;
+ }
}
}
}
diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsWalker.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsWalker.cs
deleted file mode 100644
index 90a3a7510..000000000
--- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsWalker.cs
+++ /dev/null
@@ -1,142 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-using System.Threading;
-using System.Windows.Automation;
-using System.Windows.Forms;
-using System.Windows.Input;
-using BrowserTabs;
-using static Flow.Launcher.Plugin.BrowserBookmark.Main;
-
-namespace Flow.Launcher.Plugin.BrowserBookmark.Tabs;
-
-///
-/// 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.
-///
-internal class TabsWalker
-{
- private static readonly string ClassName = nameof(TabsWalker);
- private readonly TimeSpan _tabRetryTimeout = TimeSpan.FromSeconds(5);
- private readonly TimeSpan _tabRetryInterval = TimeSpan.FromMilliseconds(250);
- private readonly TabsCache _cache = new();
-
- private static IEnumerable 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;
- }
-
- // 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, $"TABS:Skipping name='{name}', className='{className}'");
- continue;
- }
-
- yield return tab;
- }
- }
-
- private static BrowserTab InitiateTab(Process process, AutomationElement tab)
- {
- return 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, $"TABS:Start searching for a new tab... Try no {count++}");
-
- var tabs = FindAllValidTabs(mainWindow).ToList();
- if (tabs.Count == 0)
- {
- Context.API.LogDebug(ClassName, "TABS:No valid tabs found");
- Thread.Sleep(_tabRetryInterval);
- continue;
- }
-
- if (_cache.Empty())
- {
- Context.API.LogDebug(ClassName, "TABS: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, $"TABS:Found tabs: {tabs.Count}");
- //TabsDebug.DumpTabs(mainWindow);
-
- // 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, $"TABS:FOUND A NEW TAB: name={tab.Current.Name}, className={tab.Current.ClassName}");
- _cache.Add(tab);
- return InitiateTab(process, tab);
- }
- }
-
- Context.API.LogDebug(ClassName, "TABS:No new tab found");
- Thread.Sleep(_tabRetryInterval);
- }
-
- Context.API.LogDebug(ClassName, "TABS:Timeout waiting for new tab");
- }
- catch (ElementNotAvailableException ex)
- {
- Context.API.LogException(ClassName, "TABS:Element not available", ex);
- }
- catch (Exception ex)
- {
- Context.API.LogException(ClassName, "TABS:Error getting current tab from window", ex);
- }
- return null;
- }
-
- internal void RescanTabsForContainer(AutomationElement browserWindow)
- {
- Context.API.LogDebug(ClassName, "TABS:Rescanning tabs in order to find removed tabs");
- _cache.RemoveAllNonExistentTabs(browserWindow, FindAllValidTabs(browserWindow));
- }
-
- internal void RemoveAllTabs(AutomationElement browserWindow)
- {
- Context.API.LogDebug(ClassName, "TABS:Removing all tabs in a window");
- _cache.RemoveAllNonExistentTabs(browserWindow, []);
- }
-
- internal void CheckTabExistence(string runtimeId, string reason = "")
- {
- if (_cache.Contains(runtimeId))
- {
- Context.API.LogDebug(ClassName, $"TABS:{runtimeId}:Tab exists {reason}");
- }
- }
-}
diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs
index 0fca79409..643c397c8 100644
--- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs
+++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs
@@ -53,8 +53,7 @@ public partial class SettingsControl
get => Settings.ReuseTabs;
set
{
- Settings.ReuseTabs = value;
- // reloading of bookmarks is not needed while this settings changes
+ _ = Task.Run(() => Main.SetReuseTabs(value));
}
}