feat: added cleaning cache on tabs and windows closing

This commit is contained in:
Andrzej Martyna 2025-12-29 23:38:00 +01:00
parent d1e5b70759
commit 9d27bcf589
4 changed files with 107 additions and 18 deletions

View file

@ -1,8 +0,0 @@
# Items to consider to improve the code
- TabsWalker.GetCurrentTabFromWindow
- Research browsers' settings and check if it may break current assumption of just taking the last tab
- TabsTracker
- AutomationFocusChangedEventHandler could be replaced with AutomationStructureChangedEventHandler, StructureChangeType.ChildAdded
- Removal of tabs should be handled to save memory by using AutomationStructureChangedEventHandler, StructureChangeType.ChildRemoved

View file

@ -1,5 +1,11 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Automation;
using System.Windows.Forms;
using System.Windows.Input;
using System.Xml.Linq;
using static Flow.Launcher.Plugin.BrowserBookmark.Main;
namespace Flow.Launcher.Plugin.BrowserBookmark.Tabs;
@ -9,13 +15,16 @@ namespace Flow.Launcher.Plugin.BrowserBookmark.Tabs;
/// </summary>
internal class TabsCache
{
private readonly HashSet<string> _knownTabs = new();
private readonly object sync = new();
private static readonly string ClassName = nameof(TabsCache);
private readonly HashSet<string> _knownTabs = [];
private readonly object _sync = new();
private static string RuntimeIdToKey(AutomationElement elem) {
public static string RuntimeIdToKey(int[] runtimeId) => string.Join("-", runtimeId);
public static string RuntimeIdToKey(AutomationElement elem) {
try
{
return elem != null ? string.Join("-", elem.GetRuntimeId()) : null;
return elem != null ? RuntimeIdToKey(elem.GetRuntimeId()) : null;
}
catch (ElementNotAvailableException)
{
@ -25,7 +34,7 @@ internal class TabsCache
public bool Empty()
{
lock (sync)
lock (_sync)
{
return _knownTabs.Count == 0;
}
@ -33,16 +42,16 @@ internal class TabsCache
public void Add(AutomationElement tab)
{
lock (sync)
lock (_sync)
{
var key = RuntimeIdToKey(tab);
if (key != null)
{
Context.API.LogDebug(ClassName, $"Adding a tab to cache: {tab.Current.Name}");
_knownTabs.Add(key);
}
}
}
public void Add(IEnumerable<AutomationElement> tabs)
{
foreach (var tab in tabs)
@ -53,7 +62,7 @@ internal class TabsCache
public bool Contains(AutomationElement tab)
{
lock (sync)
lock (_sync)
{
var key = RuntimeIdToKey(tab);
if (key != null)
@ -63,4 +72,25 @@ internal class TabsCache
}
return false;
}
public void RemoveAllNonExistentTabs(AutomationElement rootElement, IEnumerable<AutomationElement> existingTabs)
{
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));
//Context.API.LogDebug(ClassName, $"Rootkey: {rootKey}");
//Context.API.LogDebug(ClassName, $"Existing keys:\r\n{string.Join("\r\n", existingKeys)}");
//Context.API.LogDebug(ClassName, $"Known Tabs:\r\n{string.Join("\r\n", _knownTabs)}");
//Context.API.LogDebug(ClassName, $"Tabs to remove:\r\n{string.Join("\r\n", keysToRemove)}");
foreach (var key in keysToRemove)
{
Context.API.LogDebug(ClassName, $"Removing a tab from cache: {key}");
_knownTabs.Remove(key);
}
}
}

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Windows.Automation;
using BrowserTabs;
@ -27,6 +28,7 @@ public class TabsTracker : IDisposable
private readonly object _sync = new();
private AutomationFocusChangedEventHandler? _focusHandler;
private readonly HashSet<AutomationElement> _browserWindowsTracked = [];
private bool _initialized;
public void OpenUrlAndTrack(Settings settings, string url)
@ -82,7 +84,15 @@ public class TabsTracker : IDisposable
public void Dispose()
{
if (_focusHandler != null)
{
Automation.RemoveAutomationFocusChangedEventHandler(_focusHandler);
_focusHandler = null;
}
var windowsToUnsubscribe = _browserWindowsTracked.ToList();
foreach (var wnd in windowsToUnsubscribe)
{
UnsubscribeStructureChangedForWindow(wnd);
}
}
public void ExpectUrl(string url)
@ -163,6 +173,10 @@ public class TabsTracker : IDisposable
// 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);
}
}
catch (ArgumentException)
@ -176,4 +190,44 @@ public class TabsTracker : IDisposable
Context.API.LogException(ClassName, "Exception", ex);
}
}
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.ChildRemoved)
{
var eventRuntimeId = TabsCache.RuntimeIdToKey(e.GetRuntimeId());
foreach (var window in _browserWindowsTracked)
{
var windowRuntimeId = TabsCache.RuntimeIdToKey(window);
if (windowRuntimeId != null && eventRuntimeId.StartsWith(windowRuntimeId))
{
_walker.RescanTabsForContainer(window);
}
}
}
}
private void OnWindowClosed(object sender, AutomationEventArgs e)
{
var element = sender as AutomationElement;
if (element == null)
return;
UnsubscribeStructureChangedForWindow(element);
}
private void UnsubscribeStructureChangedForWindow(AutomationElement wnd)
{
if (_browserWindowsTracked.Contains(wnd))
{
Context.API.LogDebug(ClassName, "Unsubscribe window from StructureChanged events");
Automation.RemoveStructureChangedEventHandler(wnd, OnStructureChanged);
_browserWindowsTracked.Remove(wnd);
_walker?.RemoveAllTabs(wnd);
}
}
}

View file

@ -79,6 +79,7 @@ internal class TabsWalker
// 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());
}
@ -91,7 +92,7 @@ internal class TabsWalker
var tab = tabs[i];
if (!_cache.Contains(tab))
{
Context.API.LogDebug(ClassName, $"FOUND NEW TAB: name={tab.Current.Name}, className={tab.Current.ClassName}");
Context.API.LogDebug(ClassName, $"FOUND A NEW TAB: name={tab.Current.Name}, className={tab.Current.ClassName}");
_cache.Add(tab);
return InitiateTab(process, tab);
}
@ -113,4 +114,16 @@ internal class TabsWalker
}
return null;
}
internal void RescanTabsForContainer(AutomationElement browserWindow)
{
Context.API.LogDebug(ClassName, "Rescaning tabs in order to find removed tabs");
_cache.RemoveAllNonExistentTabs(browserWindow, FindAllValidTabs(browserWindow));
}
internal void RemoveAllTabs(AutomationElement browserWindow)
{
Context.API.LogDebug(ClassName, "Removing all tabs in a window");
_cache.RemoveAllNonExistentTabs(browserWindow, []);
}
}