mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Take Thread.Sleep out of OnFocusChanged
This commit is contained in:
parent
75a03d0320
commit
cdb2d9e6dc
4 changed files with 173 additions and 44 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue