diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TODO.md b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TODO.md deleted file mode 100644 index 439b78bc2..000000000 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TODO.md +++ /dev/null @@ -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 diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsCache.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsCache.cs index 6f9d10e88..331ba2bae 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsCache.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsCache.cs @@ -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; /// internal class TabsCache { - private readonly HashSet _knownTabs = new(); - private readonly object sync = new(); + private static readonly string ClassName = nameof(TabsCache); + private readonly HashSet _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 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 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); + } + } } diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsTracker.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsTracker.cs index 36ccdd863..5d849c2e5 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsTracker.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsTracker.cs @@ -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 _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); + } + } } diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsWalker.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsWalker.cs index 04b780be4..e264891f2 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsWalker.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsWalker.cs @@ -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, []); + } }