diff --git a/content_scripts/link_hints.js b/content_scripts/link_hints.js index 2b8f979f1..1db1a8481 100644 --- a/content_scripts/link_hints.js +++ b/content_scripts/link_hints.js @@ -445,6 +445,10 @@ class LinkHintsMode { } this.setIndicator(); + // Ensure overlapping markers don't visually stack on top of each other. + if (Settings.get("suppressOverlappingHintMarkers")) { + this.suppressOverlappingMarkers(); + } } setOpenLinkMode(mode, shouldPropagateToOtherFrames) { @@ -607,6 +611,10 @@ class LinkHintsMode { for (const matched of linksMatched) { this.showMarker(matched, this.markerMatcher.hintKeystrokeQueue.length); } + // After visibility changes, suppress overlaps so only one marker per overlapping region shows. + if (Settings.get("suppressOverlappingHintMarkers")) { + this.suppressOverlappingMarkers(); + } } return this.setIndicator(); @@ -690,6 +698,54 @@ class LinkHintsMode { this.renderHints(); } + // Hide all but one visible marker in any overlapping group of markers. + suppressOverlappingMarkers() { + if (!this.containerEl) return; + // Consider only local, visible markers. + const visibleMarkers = this.hintMarkers.filter((m) => m.isLocalMarker() && (m.element.style.display !== "none")); + if (visibleMarkers.length <= 1) return; + + // Refresh marker rects. + for (const m of visibleMarkers) { + const rect = m.element.getClientRects()[0]; + m.markerRect = rect; + } + + // Build stacks of overlapping markers (O(n^2)), merging stacks if necessary. + let stacks = []; + for (const m of visibleMarkers) { + if (!m.markerRect) continue; + let stackForThisMarker = null; + const results = []; + for (const stack of stacks) { + const overlaps = this.markerOverlapsStack(m, stack); + if (overlaps && (stackForThisMarker == null)) { + stack.push(m); + stackForThisMarker = stack; + results.push(stack); + } else if (overlaps && (stackForThisMarker != null)) { + // Merge overlapping stacks. + stackForThisMarker.push(...stack); + // Do not push this stack into results to effectively discard it. + } else { + results.push(stack); + } + } + stacks = results; + if (stackForThisMarker == null) stacks.push([m]); + } + + // For each overlapping stack, hide all but the last marker (topmost after current ordering). + for (const stack of stacks) { + if (stack.length <= 1) continue; + for (let i = 0; i < stack.length - 1; i++) { + stack[i].element.style.display = "none"; + } + // Ensure one remains visible. + stack[stack.length - 1].element.style.display = ""; + } + } + // When only one hint remains, activate it in the appropriate way. The current frame may or may // not contain the matched link, and may or may not have the focus. The resulting four cases are // accounted for here by selectively pushing the appropriate HintCoordinator.onExit handlers. diff --git a/lib/settings.js b/lib/settings.js index 8b3db136c..cce30e5a2 100644 --- a/lib/settings.js +++ b/lib/settings.js @@ -70,6 +70,8 @@ w: https://www.wikipedia.org/w/index.php?title=Special:Search&search=%s Wikipedi waitForEnterForFilteredHints: true, helpDialog_showAdvancedCommands: false, ignoreKeyboardLayout: false, + // When true, hide overlapping link-hint markers so only one shows per overlapping region. + suppressOverlappingHintMarkers: true, }; /* diff --git a/pages/options.html b/pages/options.html index e9255aef6..dfe0ce045 100644 --- a/pages/options.html +++ b/pages/options.html @@ -172,6 +172,15 @@

keyboards. +

+ +
+ When hints overlap visually, show only one marker per overlapping region. +
+

Previous patterns

diff --git a/pages/options.js b/pages/options.js index ed243f90d..25e7a6949 100644 --- a/pages/options.js +++ b/pages/options.js @@ -17,6 +17,7 @@ const options = { previousPatterns: "string", regexFindMode: "boolean", ignoreKeyboardLayout: "boolean", + suppressOverlappingHintMarkers: "boolean", scrollStepSize: "number", smoothScroll: "boolean", grabBackFocus: "boolean",