From f4a92974c61b3dd62ea300da5d37c0ab5c0fd4ef Mon Sep 17 00:00:00 2001 From: Dan Covill Date: Sat, 26 Apr 2025 22:39:59 -0400 Subject: [PATCH 1/2] Add linkText to AlphabetHints so it's easier to discern scroll/frame markers --- content_scripts/link_hints.js | 41 ++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/content_scripts/link_hints.js b/content_scripts/link_hints.js index e96a68e07..3a619cc29 100644 --- a/content_scripts/link_hints.js +++ b/content_scripts/link_hints.js @@ -38,8 +38,8 @@ class LocalHint { element; // The clickable element. image; // When element is an (image map), `image` is its associated image. rect; // The rectangle where the hint should shown, to avoid overlapping with other hints. - linkText; // Used in FilterHints. - showLinkText; // Used in FilterHints. + linkText; + showLinkText; // The reason that an element has a link hint when the reason isn't obvious, e.g. the body of a // frame so that the frame can be focused. This reason is shown to the user in the hint's caption. reason; @@ -792,18 +792,21 @@ class AlphabetHints { this.hintKeystrokeQueue = []; } + renderMarker(marker) { + let linkText = marker.linkText; + console.log(linkText); + const caption = marker.hintString.toUpperCase() + + (marker.localHint.showLinkText ? ": " + linkText : ""); + marker.element.innerHTML = spanWrap(caption); + } + fillInMarkers(hintMarkers) { const hintStrings = this.hintStrings(hintMarkers.length); - if (hintMarkers.length != hintStrings.length) { - // This can only happen if the user's linkHintCharacters setting is empty. - console.warn("Unable to generate link hint strings."); - } else { - for (let i = 0; i < hintMarkers.length; i++) { - const marker = hintMarkers[i]; - marker.hintString = hintStrings[i]; - if (marker.isLocalMarker()) { - marker.element.innerHTML = spanWrap(marker.hintString.toUpperCase()); - } + let i = 0; + for (const marker of hintMarkers) { + marker.hintString = hintStrings[i++]; + if (marker.isLocalMarker()) { + this.renderMarker(marker); } } } @@ -1186,13 +1189,13 @@ const LocalHints = { break; case "body": isClickable ||= (element === document.body) && !windowIsFocused() && - (globalThis.innerWidth > 3) && (globalThis.innerHeight > 3) && - ((document.body != null ? document.body.tagName.toLowerCase() : undefined) !== - "frameset") + (globalThis.innerWidth > 3) && (globalThis.innerHeight > 3) && + ((document.body != null ? document.body.tagName.toLowerCase() : undefined) !== + "frameset") ? (reason = "Frame.") : undefined; isClickable ||= (element === document.body) && windowIsFocused() && - Scroller.isScrollableElement(element) + Scroller.isScrollableElement(element) ? (reason = "Scroll.") : undefined; break; @@ -1411,10 +1414,8 @@ const LocalHints = { hint.rect.left += left; } - if (Settings.get("filterLinkHints")) { - for (const hint of nonOverlappingHints) { - Object.assign(hint, this.generateLinkText(hint)); - } + for (const hint of nonOverlappingHints) { + Object.assign(hint, this.generateLinkText(hint)); } return nonOverlappingHints; }, From dea753803f512a78195038b3d4975dd6016cac5e Mon Sep 17 00:00:00 2001 From: = <=> Date: Mon, 7 Jul 2025 21:02:04 -0600 Subject: [PATCH 2/2] Enhance link hinting with scroll marker and overlap handling - Added 'scroll-hint-marker' class for frames and scrollable areas in link hints. - Implemented overlap detection and offset adjustments for overlapping hint markers. - Updated CSS for scroll hint markers to improve visibility. --- content_scripts/link_hints.js | 92 +++++++++++++++++++++++++++++++++-- content_scripts/vimium.css | 5 ++ 2 files changed, 92 insertions(+), 5 deletions(-) diff --git a/content_scripts/link_hints.js b/content_scripts/link_hints.js index 71c585a1f..4067d7fba 100644 --- a/content_scripts/link_hints.js +++ b/content_scripts/link_hints.js @@ -38,8 +38,8 @@ class LocalHint { element; // The clickable element. image; // When element is an (image map), `image` is its associated image. rect; // The rectangle where the hint should shown, to avoid overlapping with other hints. - linkText; - showLinkText; + linkText; // Used in FilterHints. + showLinkText; // Used in FilterHints. // The reason that an element has a link hint when the reason isn't obvious, e.g. the body of a // frame so that the frame can be focused. This reason is shown to the user in the hint's caption. reason; @@ -371,6 +371,8 @@ class LinkHintsMode { this.hintMode = null; // A count of the number of Tab presses since the last non-Tab keyboard event. this.tabCount = 0; + // Track whether we've already applied overlap offsets for link hints + this.overlapOffsetsApplied = false; if (hintDescriptors.length === 0) { HUD.show("No links to select.", 2000); @@ -425,6 +427,12 @@ class LinkHintsMode { this.containerEl.appendChild(el); } + // Only apply overlap offsets to link hints once during initial setup + if (!this.overlapOffsetsApplied) { + this.applyOverlapOffsets(); + this.overlapOffsetsApplied = true; + } + // TODO(philc): 2024-03-27 Remove this hasPopoverSupport check once Firefox has popover support. // Also move this CSS into vimium.css. const hasPopoverSupport = this.containerEl.showPopover != null; @@ -484,6 +492,13 @@ class LinkHintsMode { // Note that Vimium's CSS is user-customizable. We're adding the "vimiumHintMarker" class here // for users to customize. See further comments about this in vimium.css. el.className = "vimium-reset internal-vimium-hint-marker vimiumHintMarker"; + // Add scroll-hint-marker class for frames or scrollable areas in alphabet hints mode. + if ( + (localHint.reason === "Frame." || localHint.reason === "Scroll.") && + !Settings.get("filterLinkHints") + ) { + el.classList.add("scroll-hint-marker"); + } Object.assign(marker, { element: el, localHint, @@ -497,6 +512,68 @@ class LinkHintsMode { }); } + // Add a new method to detect and handle overlapping markers + applyOverlapOffsets() { + const localMarkers = this.hintMarkers.filter(m => m.isLocalMarker() && m.element); + + // Cache marker rectangles + localMarkers.forEach(marker => { + marker.markerRect = marker.element.getBoundingClientRect(); + }); + + // Group overlapping markers into stacks + const stacks = this.groupOverlappingMarkers(localMarkers); + + // Apply position offsets to overlapping markers + stacks.forEach(stack => { + if (stack.length > 1) { + stack.forEach((marker, index) => { + if (index > 0) { + const offset = index * 2; + const currentLeft = parseInt(marker.element.style.left) || 0; + const currentTop = parseInt(marker.element.style.top) || 0; + + marker.element.style.left = (currentLeft + offset) + "px"; + marker.element.style.top = (currentTop + offset) + "px"; + } + }); + } + }); + } + + // Helper to group overlapping markers + groupOverlappingMarkers(markers) { + const stacks = []; + + for (const marker of markers) { + let assignedStack = null; + + // Find stacks that overlap with this marker + const overlappingStacks = stacks.filter(stack => + stack.some(otherMarker => Rect.intersects(marker.markerRect, otherMarker.markerRect)) + ); + + if (overlappingStacks.length === 0) { + // No overlaps, create new stack + stacks.push([marker]); + } else if (overlappingStacks.length === 1) { + // Overlaps with one stack, add to it + overlappingStacks[0].push(marker); + } else { + // Overlaps with multiple stacks, merge them + const mergedStack = [marker, ...overlappingStacks.flat()]; + // Remove old stacks and add merged stack + overlappingStacks.forEach(stack => { + const index = stacks.indexOf(stack); + if (index > -1) stacks.splice(index, 1); + }); + stacks.push(mergedStack); + } + } + + return stacks; + } + // Handles all keyboard events. onKeyDownInMode(event) { if (event.repeat) return; @@ -810,7 +887,6 @@ class AlphabetHints { renderMarker(marker) { let linkText = marker.linkText; - console.log(linkText); const caption = marker.hintString.toUpperCase() + (marker.localHint.showLinkText ? ": " + linkText : ""); marker.element.innerHTML = spanWrap(caption); @@ -818,6 +894,10 @@ class AlphabetHints { fillInMarkers(hintMarkers) { const hintStrings = this.hintStrings(hintMarkers.length); + if (hintMarkers.length != hintStrings.length) { + // This can only happen if the user's linkHintCharacters setting is empty. + console.warn("Unable to generate link hint strings."); + } let i = 0; for (const marker of hintMarkers) { marker.hintString = hintStrings[i++]; @@ -1430,8 +1510,10 @@ const LocalHints = { hint.rect.left += left; } - for (const hint of nonOverlappingHints) { - Object.assign(hint, this.generateLinkText(hint)); + if (Settings.get("filterLinkHints")) { + for (const hint of nonOverlappingHints) { + Object.assign(hint, this.generateLinkText(hint)); + } } return nonOverlappingHints; }, diff --git a/content_scripts/vimium.css b/content_scripts/vimium.css index 9182d7255..477e377fa 100644 --- a/content_scripts/vimium.css +++ b/content_scripts/vimium.css @@ -118,6 +118,11 @@ div.internal-vimium-hint-marker { z-index: 2147483647; } +div.internal-vimium-hint-marker.scroll-hint-marker { + /* This class is used for marking scrollable areas or frames */ + background: linear-gradient(to bottom, #ffb2b2 0%, #ff7a7a 100%); +} + div.internal-vimium-hint-marker span { color: #302505; font-family: Helvetica, Arial, sans-serif;