From cc80c0524c28775809566ec7afa46a153853db9d Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sun, 6 Dec 2015 17:50:32 +0100 Subject: [PATCH] Add command to click browser elements using markers Fixes #236. A note about the "hamburger menu" in the top-right corner: It is possible to _open_ it with `zF`, but not possible to use `zF` inside of it, because of `@popupPassthrough`. One _could_ an exception for that menu/popup, but that doesn't help: - `.elementFromPoint()` does not appear to find elements in popups, making it difficult to determine if the buttons inside are visible or not. - The markers appear _behind_ the popup, no matter what `z-index` is used. - The popup seems to eat ``: When it is pressed, the popup is closed, but the markers remain. The next `` press removes the markers. Not a big deal, but slightly annoying. Therefore I decided not to add support for that menu in this commit. (Perhaps we'll be able to do that some time in the future.) Instead, I recommend using the alt key shortcuts for the "regular" menubar. --- documentation/commands.md | 3 +- extension/lib/commands.coffee | 31 ++++++++++++ extension/lib/defaults.coffee | 1 + extension/lib/hints.coffee | 66 ++++++++++++++++--------- extension/locale/de/vimfx.properties | 1 + extension/locale/en-US/vimfx.properties | 1 + extension/locale/fr/vimfx.properties | 1 + extension/locale/id/vimfx.properties | 1 + extension/locale/it/vimfx.properties | 1 + extension/locale/ja/vimfx.properties | 1 + extension/locale/nl/vimfx.properties | 1 + extension/locale/pt-BR/vimfx.properties | 1 + extension/locale/ru/vimfx.properties | 1 + extension/locale/sv-SE/vimfx.properties | 1 + extension/locale/zh-CN/vimfx.properties | 1 + extension/locale/zh-TW/vimfx.properties | 1 + extension/skin/style.css | 9 ++++ 17 files changed, 97 insertions(+), 25 deletions(-) diff --git a/documentation/commands.md b/documentation/commands.md index 57264e6..7c2522f 100644 --- a/documentation/commands.md +++ b/documentation/commands.md @@ -177,6 +177,7 @@ Which elements get hints depends on the command as well: inputs (their text). - `zf`: Anything focusable—links, buttons, form controls, scrollable elements, frames. +- `zF`: Browser elements, such as toolbar buttons. It might seem simpler to match the same set of elements for _all_ of the commands. The reason that is not the case is because the fewer elements the @@ -229,7 +230,7 @@ command is implemented by running the same function as for the `f` command, passing `Infinity` as the `count` argument!) Therefore the `af` command does not accept a count itself. -The `gF`, `zf` and `yf` commands do not accept counts. +The `gF`, `zf`, `yf` and `zF` commands do not accept counts. Press `` to increase the count by one. This is useful when you’ve already entered Hints mode but realize that you want to interact with yet a marker. This diff --git a/extension/lib/commands.coffee b/extension/lib/commands.coffee index 860555c..5b9892a 100644 --- a/extension/lib/commands.coffee +++ b/extension/lib/commands.coffee @@ -429,6 +429,37 @@ commands.follow_focus = ({vim}) -> vim._focusMarkerElement(marker.wrapper.elementIndex, {select: true}) return helper_follow('follow_focus', vim, callback) +commands.click_browser_element = ({vim}) -> + markerElements = [] + + filter = (element, getElementShape) -> + document = element.ownerDocument + unless element.tabIndex > -1 and + not (element.nodeName.endsWith('box') and + element.nodeName != 'checkbox') and + element.nodeName != 'tabs' + return + return unless shape = getElementShape(element) + length = markerElements.push(element) + return {type: 'clickable', semantic: true, shape, elementIndex: length - 1} + + callback = (marker) -> + element = markerElements[marker.wrapper.elementIndex] + utils.focusElement(element) + utils.simulateClick(element) + + {wrappers, viewport} = + hints.getMarkableElementsAndViewport(vim.window, filter) + + if wrappers.length > 0 + markers = hints.injectHints(vim.window, wrappers, viewport, { + hint_chars: vim.options.hint_chars + ui: true + }) + vim.enterMode('hints', markers, callback) + else + vim.notify(translate('notification.follow.none')) + helper_follow_pattern = (type, {vim}) -> options = pattern_selector: vim.options.pattern_selector diff --git a/extension/lib/defaults.coffee b/extension/lib/defaults.coffee index 72deb32..47a3897 100644 --- a/extension/lib/defaults.coffee +++ b/extension/lib/defaults.coffee @@ -82,6 +82,7 @@ shortcuts = 'af': 'follow_multiple' 'yf': 'follow_copy' 'zf': 'follow_focus' + 'zF': 'click_browser_element' '[': 'follow_previous' ']': 'follow_next' 'gi': 'focus_text_input' diff --git a/extension/lib/hints.coffee b/extension/lib/hints.coffee index 8945e79..72cac39 100644 --- a/extension/lib/hints.coffee +++ b/extension/lib/hints.coffee @@ -107,15 +107,19 @@ injectHints = (window, wrappers, viewport, options) -> removeHints(window) # Better safe than sorry. container = window.document.createElement('box') container.id = CONTAINER_ID - window.gBrowser.mCurrentBrowser.parentNode.appendChild(container) - zoom = + zoom = 1 + + if options.ui + container.classList.add('ui') + window.document.getElementById('browser-panel').appendChild(container) + else + window.gBrowser.mCurrentBrowser.parentNode.appendChild(container) + # If “full zoom” is not used, it means that “Zoom text only” is enabled. + # If so, that “zoom” does not need to be taken into account. if window.ZoomManager.useFullZoom - window.ZoomManager.getZoomForBrowser(window.gBrowser.selectedBrowser) - else - # If “full zoom” is not used, it means that “Zoom text only” is enabled. - # If so, that “zoom” does not need to be taken into account. - 1 + zoom = + window.ZoomManager.getZoomForBrowser(window.gBrowser.selectedBrowser) for marker in markers container.appendChild(marker.markerElement) @@ -171,7 +175,7 @@ getMarkableElements = (window, viewport, wrappers, filter, parents = []) -> ) wrappers.push(wrapper) - for frame in window.frames + for frame in window.frames when frame.frameElement rect = frame.frameElement.getBoundingClientRect() # Frames only have one. continue unless isInsideViewport(rect, viewport) @@ -203,20 +207,24 @@ getAllElements = (document) -> unless document instanceof XULDocument return document.getElementsByTagName('*') - elements = [] + # Use a `Set` since this algorithm may find the same element more than once. + # Ideally we should find a way to find all elements without duplicates. + elements = new Set() getAllRegular = (element) -> - for child in element.getElementsByTagName('*') - elements.push(child) + # The first time `zF` is run `.getElementsByTagName('*')` may oddly include + # `undefined` in its result! Filter those out. + for child in element.getElementsByTagName('*') when child + elements.add(child) getAllAnonymous(child) return getAllAnonymous = (element) -> for child in document.getAnonymousNodes(element) or [] continue unless child instanceof Element - elements.push(child) + elements.add(child) getAllRegular(child) return getAllRegular(document.documentElement) - return elements + return Array.from(elements) getRects = (element, viewport) -> # `element.getClientRects()` returns a list of rectangles, usually just one, @@ -327,10 +335,8 @@ getFirstNonCoveredPoint = (window, viewport, element, elementRect, parents) -> # bullet proof: Combinations of CSS can cause this check to fail, even # though `element` isn’t covered. We don’t try to temporarily reset such CSS # because of performance. Instead we rely on that some of the attempts below - # will work. - if element.contains(elementAtPoint) or # Note that `a.contains(a) == true`! - (window.document instanceof XULDocument and - getClosestNonAnonymousParent(element) == elementAtPoint) + # will work. Note that `a.contains(a) == true`! + if normalize(element).contains(elementAtPoint) found = true # If we’re currently in a frame, there might be something on top of the # frame that covers `element`. Therefore we ensure that the frame really @@ -342,7 +348,7 @@ getFirstNonCoveredPoint = (window, viewport, element, elementRect, parents) -> elementAtPoint = parent.window.document.elementFromPoint( offset.left + x + dx, offset.top + y + dy ) - unless elementAtPoint == currentWindow.frameElement + unless frameAtPoint(currentWindow.frameElement, elementAtPoint) found = false break currentWindow = parent.window @@ -381,12 +387,24 @@ getFirstNonCoveredPoint = (window, viewport, element, elementRect, parents) -> return nonCoveredPoint -# In XUL documents there are “anonymous” elements, whose node names start with -# `xul:` or `html:`. These are never returned by `document.elementFromPoint` but -# their closest non-anonymous parents are. -getClosestNonAnonymousParent = (element) -> - element = element.parentNode while element.prefix? - return element +# In XUL documents there are “anonymous” elements. These are never returned by +# `document.elementFromPoint` but their closest non-anonymous parents are. +normalize = (element) -> + return element.ownerDocument.getBindingParent(element) or element + +# Returns whether `frameElement` corresponds to `elementAtPoint`. This is only +# complicated for the dev tools’ frame. `.elementAtPoint()` returns +# `` instead of the `