Take Thread.Sleep out of OnFocusChanged

This commit is contained in:
Andrzej Martyna 2025-12-30 09:05:46 +01:00
parent 75a03d0320
commit cdb2d9e6dc
4 changed files with 173 additions and 44 deletions

View file

@ -57,13 +57,17 @@ internal class TabsCache
}
public bool Contains(AutomationElement tab)
{
return Contains(RuntimeIdToKey(tab));
}
public bool Contains(string runtimeId)
{
lock (_sync)
{
var key = RuntimeIdToKey(tab);
if (key != null)
if (runtimeId != null)
{
return _knownTabs.Contains(key);
return _knownTabs.Contains(runtimeId);
}
}
return false;

View file

@ -0,0 +1,85 @@
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Threading;
using System.Windows.Automation;
using Flow.Launcher.Plugin.BrowserBookmark.Tabs;
using static Flow.Launcher.Plugin.BrowserBookmark.Main;
internal sealed class TabsFocusEventDispatcher : IDisposable
{
private static readonly string ClassName = nameof(TabsFocusEventDispatcher);
private readonly BlockingCollection<Tuple<string, AutomationElement, int>> _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<string, AutomationElement, int> 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, $"Process {processId} no longer runs");
}
catch (Exception ex)
{
Context.API.LogException(ClassName, "Exception", ex);
}
}
public void Dispose()
{
_cts.Cancel();
_queue.CompleteAdding();
_worker.Join();
_cts.Dispose();
_queue.Dispose();
}
}

View file

@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Windows.Automation;
using BrowserTabs;
using Flow.Launcher.Plugin.BrowserBookmark.Models;
@ -23,10 +22,11 @@ public class TabsTracker : IDisposable
private static readonly HashSet<string> chromiumProcessNames = new HashSet<string>(["msedge", "chrome", "brave", "vivaldi", "opera", "chromium"], StringComparer.OrdinalIgnoreCase);
private static readonly HashSet<string> firefoxProcessNames = new HashSet<string>(["firefox"], StringComparer.OrdinalIgnoreCase);
private readonly TabsWalker _walker = new();
private string? _expectedUrl;
private readonly Queue<string> _expectedUrls = [];
private Dictionary<string, BrowserTab> UrlToBrowserTab { get; } = [];
private readonly object _sync = new();
private TabsFocusEventDispatcher _focusHandlerDispatcher;
private AutomationFocusChangedEventHandler? _focusHandler;
private readonly HashSet<AutomationElement> _browserWindowsTracked = [];
private bool _initialized;
@ -76,6 +76,7 @@ public class TabsTracker : IDisposable
if (_initialized)
return;
_focusHandlerDispatcher = new(_walker, this);
_focusHandler = OnFocusChanged;
Automation.AddAutomationFocusChangedEventHandler(_focusHandler);
_initialized = true;
@ -94,17 +95,14 @@ public class TabsTracker : IDisposable
_focusHandler = null;
_initialized = false;
}
_focusHandlerDispatcher?.Dispose();
}
public void ExpectUrl(string url)
{
lock (_sync)
{
if (_expectedUrl != null)
{
Context.API.LogError(ClassName, $"Opening {url} while older is still not resolved ({_expectedUrl}). Forgetting the older.");
}
_expectedUrl = url;
_expectedUrls.Enqueue(url);
}
}
@ -130,22 +128,27 @@ public class TabsTracker : IDisposable
private void OnFocusChanged(object sender, AutomationFocusChangedEventArgs e)
{
string? urlToBind;
lock (_sync)
{
urlToBind = _expectedUrl;
if (_expectedUrls.Count == 0)
return;
}
if (urlToBind is null)
return;
try
{
Context.API.LogDebug(ClassName, $"Searching for... {urlToBind}");
if (sender is not AutomationElement element)
return;
int pid = element.Current.ProcessId;
int pid = 0;
try
{
pid = element.Current.ProcessId;
}
catch (ElementNotAvailableException)
{
return;
}
using var process = Process.GetProcessById(pid);
var chromium = chromiumProcessNames.Contains(process.ProcessName);
@ -161,23 +164,21 @@ public class TabsTracker : IDisposable
Context.API.LogDebug(ClassName, $"The root element is {rootElement.Current.Name}");
var currentTab = _walker.GetCurrentTabFromWindow(rootElement, process, CancellationToken.None);
if (currentTab != null)
string? urlToBind;
lock (_sync)
{
lock (_sync)
{
Context.API.LogDebug(ClassName, $"Registering {urlToBind} as tab: {currentTab.Title}");
UrlToBrowserTab[urlToBind] = currentTab;
_expectedUrl = null;
if (_expectedUrls.Count == 0)
return;
// required to take the tab into account by Flow Launcher main UI search window
Context.API.ReQuery();
}
Automation.AddStructureChangedEventHandler(rootElement, TreeScope.Subtree, OnStructureChanged);
Automation.AddAutomationEventHandler(WindowPattern.WindowClosedEvent, rootElement, TreeScope.Subtree, OnWindowClosed);
_browserWindowsTracked.Add(rootElement);
urlToBind = _expectedUrls.Dequeue();
}
if (urlToBind is null)
return;
Context.API.LogDebug(ClassName, $"Searching for... {urlToBind}");
// Further handling requires waiting for tabs so its better to run it on a separate thread
_focusHandlerDispatcher?.Enqueue(urlToBind, rootElement, pid);
}
catch (Exception ex)
{
@ -187,22 +188,37 @@ public class TabsTracker : IDisposable
private void OnStructureChanged(object sender, StructureChangedEventArgs e)
{
// TODO: Consider ChildAdded to handle new tabs appearance instead of AutomationFocusChangedEventHandler
// However think twice if it is worthwhile as the current approach might already be a good one
//if (e.StructureChangeType == StructureChangeType.ChildAdded)
//if (e.StructureChangeType == StructureChangeType.ChildAdded || e.StructureChangeType == StructureChangeType.ChildrenBulkAdded)
//{
//}
if (e.StructureChangeType == StructureChangeType.ChildRemoved)
var eventRuntimeId = TabsCache.RuntimeIdToKey(e.GetRuntimeId());
switch (e.StructureChangeType)
{
var eventRuntimeId = TabsCache.RuntimeIdToKey(e.GetRuntimeId());
foreach (var window in _browserWindowsTracked)
{
var windowRuntimeId = TabsCache.RuntimeIdToKey(window);
if (windowRuntimeId != null && eventRuntimeId.StartsWith(windowRuntimeId))
// 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, $"StructureChangeType.ChildAdded occurred on {eventRuntimeId}");
// break;
//case StructureChangeType.ChildrenBulkAdded:
// Context.API.LogDebug(ClassName, $"StructureChangeType.ChildrenBulkAdded occurred on {eventRuntimeId}");
// break;
//case StructureChangeType.ChildrenInvalidated:
// _walker.CheckTabExistence(eventRuntimeId, "StructureChangeType.ChildrenInvalidated");
// break;
//case StructureChangeType.ChildrenReordered:
// _walker.CheckTabExistence(eventRuntimeId, "StructureChangeType.ChildrenReordered");
// break;
case StructureChangeType.ChildRemoved:
case StructureChangeType.ChildrenBulkRemoved:
foreach (var window in _browserWindowsTracked)
{
_walker.RescanTabsForContainer(window);
var windowRuntimeId = TabsCache.RuntimeIdToKey(window);
if (windowRuntimeId != null && eventRuntimeId.StartsWith(windowRuntimeId))
{
_walker.RescanTabsForContainer(window);
}
}
}
break;
}
}
@ -224,4 +240,20 @@ public class TabsTracker : IDisposable
_walker?.RemoveAllTabs(wnd);
}
}
internal void RegisterTab(string url, AutomationElement rootElement, BrowserTab currentTab)
{
lock (_sync)
{
Context.API.LogDebug(ClassName, $"Registering {url} as tab: {currentTab.Title}");
UrlToBrowserTab[url] = currentTab;
// required to take the tab into account by Flow Launcher main UI search window
Context.API.ReQuery();
}
Automation.AddStructureChangedEventHandler(rootElement, TreeScope.Subtree, OnStructureChanged);
Automation.AddAutomationEventHandler(WindowPattern.WindowClosedEvent, rootElement, TreeScope.Subtree, OnWindowClosed);
_browserWindowsTracked.Add(rootElement);
}
}

View file

@ -68,7 +68,7 @@ internal class TabsWalker
if (tabs.Count == 0)
{
Context.API.LogDebug(ClassName, "No valid tabs found");
Task.Delay(_tabRetryInterval, cancellationToken);
Thread.Sleep(_tabRetryInterval);
continue;
}
@ -100,7 +100,7 @@ internal class TabsWalker
}
Context.API.LogDebug(ClassName, "No new tab found");
Task.Delay(_tabRetryInterval, cancellationToken);
Thread.Sleep(_tabRetryInterval);
}
Context.API.LogDebug(ClassName, "Timeout waiting for new tab");
@ -127,4 +127,12 @@ internal class TabsWalker
Context.API.LogDebug(ClassName, "Removing all tabs in a window");
_cache.RemoveAllNonExistentTabs(browserWindow, []);
}
internal void CheckTabExistence(string runtimeId, string reason = "")
{
if (_cache.Contains(runtimeId))
{
Context.API.LogDebug(ClassName, $"Tab exists {reason}");
}
}
}