aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStephen Blott2015-04-25 07:06:31 +0100
committerStephen Blott2015-04-25 07:06:31 +0100
commit9100b1d48aa9c09d792d9e2c9251e6a6c62f81bf (patch)
treee005bacd529ec8efa63243b45225051fc9af3417
parentef44d0ff49cec17a758b65c10ba4c0ecbc0c2ece (diff)
parentbfa6c88b41acac4c98d06f324f25f8bb7b328614 (diff)
downloadvimium-9100b1d48aa9c09d792d9e2c9251e6a6c62f81bf.tar.bz2
Merge branch 'vomnibar-in-main-window' into vomnibar-in-main-window-merge
Conflicts: background_scripts/main.coffee content_scripts/vimium_frontend.coffee
-rw-r--r--background_scripts/main.coffee14
-rw-r--r--content_scripts/ui_component.coffee47
-rw-r--r--content_scripts/vimium_frontend.coffee49
-rw-r--r--content_scripts/vomnibar.coffee26
-rw-r--r--lib/dom_utils.coffee6
-rw-r--r--pages/vomnibar.coffee12
6 files changed, 124 insertions, 30 deletions
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee
index 31ada357..a26fd8a8 100644
--- a/background_scripts/main.coffee
+++ b/background_scripts/main.coffee
@@ -600,6 +600,18 @@ handleFrameFocused = (request, sender) ->
if frameIdsForTab[tabId]?
frameIdsForTab[tabId] =
[request.frameId, (frameIdsForTab[tabId].filter (id) -> id != request.frameId)...]
+ # Inform all frames that a frame has received the focus.
+ chrome.tabs.sendMessage sender.tab.id,
+ name: "frameFocused"
+ focusFrameId: request.frameId
+
+# Send a message to all frames in the current tab.
+sendMessageToFrames = (request, sender) ->
+ chrome.tabs.sendMessage sender.tab.id, request.message
+
+# For debugging only. This allows content scripts to log messages to the background page's console.
+bgLog = (request, sender) ->
+ console.log "#{sender.tab.id}/#{request.frameId}", request.message
# Port handler mapping
portHandlers =
@@ -627,6 +639,8 @@ sendRequestHandlers =
createMark: Marks.create.bind(Marks)
gotoMark: Marks.goto.bind(Marks)
setIcon: setIcon
+ sendMessageToFrames: sendMessageToFrames
+ log: bgLog
# We always remove chrome.storage.local/findModeRawQueryListIncognito on startup.
chrome.storage.local.remove "findModeRawQueryListIncognito"
diff --git a/content_scripts/ui_component.coffee b/content_scripts/ui_component.coffee
index dadc84b5..ea0bf776 100644
--- a/content_scripts/ui_component.coffee
+++ b/content_scripts/ui_component.coffee
@@ -2,6 +2,7 @@ class UIComponent
iframeElement: null
iframePort: null
showing: null
+ options: null
constructor: (iframeUrl, className, @handleMessage) ->
@iframeElement = document.createElement "iframe"
@@ -14,6 +15,13 @@ class UIComponent
# Hide the iframe, but don't interfere with the focus.
@hide false
+ # If any other frame in the current tab receives the focus, then we hide the UI component.
+ # NOTE(smblott) This is correct for the vomnibar, but might be incorrect (and need to be revisited) for
+ # other UI components.
+ chrome.runtime.onMessage.addListener (request) =>
+ @postMessage "hide" if @showing and request.name == "frameFocused" and request.focusFrameId != frameId
+ false # Free up the sendResponse handler.
+
# Open a port and pass it to the iframe via window.postMessage.
openPort: ->
messageChannel = new MessageChannel()
@@ -27,8 +35,8 @@ class UIComponent
postMessage: (message) ->
@iframePort.postMessage message
- activate: (message) ->
- @postMessage message if message?
+ activate: (@options) ->
+ @postMessage @options if @options?
@show() unless @showing
@iframeElement.focus()
@@ -36,6 +44,9 @@ class UIComponent
@postMessage message if message?
@iframeElement.classList.remove "vimiumUIComponentHidden"
@iframeElement.classList.add "vimiumUIComponentShowing"
+ # The window may not have the focus. We focus it now, to prevent the "focus" listener below from firing
+ # immediately.
+ window.focus()
window.addEventListener "focus", @onFocus = (event) =>
if event.target == window
window.removeEventListener "focus", @onFocus
@@ -44,12 +55,38 @@ class UIComponent
@showing = true
hide: (focusWindow = true)->
- @iframeElement.classList.remove "vimiumUIComponentShowing"
- @iframeElement.classList.add "vimiumUIComponentHidden"
+ @refocusSourceFrame @options?.sourceFrameId if focusWindow
window.removeEventListener "focus", @onFocus if @onFocus
@onFocus = null
- window.focus() if focusWindow
+ @iframeElement.classList.remove "vimiumUIComponentShowing"
+ @iframeElement.classList.add "vimiumUIComponentHidden"
+ @options = null
@showing = false
+ # Refocus the frame from which the UI component was opened. This may be different from the current frame.
+ # After hiding the UI component, Chrome refocuses the containing frame. To avoid a race condition, we need
+ # to wait until that frame first receives the focus, before then focusing the frame which should now have
+ # the focus.
+ refocusSourceFrame: (sourceFrameId) ->
+ if @showing and sourceFrameId? and sourceFrameId != frameId
+ refocusSourceFrame = ->
+ chrome.runtime.sendMessage
+ handler: "sendMessageToFrames"
+ message:
+ name: "focusFrame"
+ frameId: sourceFrameId
+ highlight: false
+ highlightOnlyIfNotTop: true
+
+ if windowIsFocused() and false
+ # We already have the focus.
+ refocusSourceFrame()
+ else
+ # We don't yet have the focus (but we'll be getting it soon).
+ window.addEventListener "focus", handler = (event) ->
+ if event.target == window
+ window.removeEventListener "focus", handler
+ refocusSourceFrame()
+
root = exports ? window
root.UIComponent = UIComponent
diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee
index 6c09ab72..b5a5f51e 100644
--- a/content_scripts/vimium_frontend.coffee
+++ b/content_scripts/vimium_frontend.coffee
@@ -19,6 +19,13 @@ keyQueue = null
currentCompletionKeys = ""
validFirstKeys = ""
+# We track whther the current window has the focus or not.
+windowIsFocused = do ->
+ windowHasFocus = document.hasFocus()
+ window.addEventListener "focus", (event) -> windowHasFocus = true if event.target == window; true
+ window.addEventListener "blur", (event) -> windowHasFocus = false if event.target == window; true
+ -> windowHasFocus
+
# The types in <input type="..."> that we consider for focusInput command. Right now this is recalculated in
# each content script. Alternatively we could calculate it once in the background page and use a request to
# fetch it each time.
@@ -96,6 +103,11 @@ settings =
#
frameId = Math.floor(Math.random()*999999999)
+# For debugging only. This logs to the console on the background page.
+bgLog = (args...) ->
+ args = (arg.toString() for arg in args)
+ chrome.runtime.sendMessage handler: "log", frameId: frameId, message: args.join " "
+
# If an input grabs the focus before the user has interacted with the page, then grab it back (if the
# grabBackFocus option is set).
class GrabBackFocus extends Mode
@@ -167,7 +179,7 @@ initializePreDomReady = ->
requestHandlers =
showHUDforDuration: (request) -> HUD.showForDuration request.text, request.duration
toggleHelpDialog: (request) -> toggleHelpDialog(request.dialogHtml, request.frameId)
- focusFrame: (request) -> if (frameId == request.frameId) then focusThisFrame(request.highlight)
+ focusFrame: (request) -> if (frameId == request.frameId) then focusThisFrame request
refreshCompletionKeys: refreshCompletionKeys
getScrollPosition: -> scrollX: window.scrollX, scrollY: window.scrollY
setScrollPosition: (request) -> setScrollPosition request.scrollX, request.scrollY
@@ -175,14 +187,19 @@ initializePreDomReady = ->
currentKeyQueue: (request) ->
keyQueue = request.keyQueue
handlerStack.bubbleEvent "registerKeyQueue", { keyQueue: keyQueue }
+ frameFocused: -> # A frame has received the focus. We don't care, here. The Vomnibar/UI-component cares.
chrome.runtime.onMessage.addListener (request, sender, sendResponse) ->
# In the options page, we will receive requests from both content and background scripts. ignore those
# from the former.
return if sender.tab and not sender.tab.url.startsWith 'chrome-extension://'
- return unless isEnabledForUrl
+ # We handle the message if we're enabled, or if it's one of these listed message types.
+ return unless isEnabledForUrl or request.name in [ "executePageCommand" ]
# These requests are delivered to the options page, but there are no handlers there.
- return if request.handler in [ "registerFrame", "frameFocused", "unregisterFrame" ]
+ return if request.handler in [ "registerFrame", "unregisterFrame" ]
+ # We don't handle these here. They're handled elsewhere (e.g. in the vomnibar/UI component).
+ return if request.name in [ "frameFocused" ]
+ # Handle the request.
sendResponse requestHandlers[request.name](request, sender)
# Ensure the sendResponse callback is freed.
false
@@ -233,7 +250,8 @@ initializeOnDomReady = ->
# Tell the background page we're in the dom ready state.
chrome.runtime.connect({ name: "domReady" })
CursorHider.init()
- Vomnibar.init()
+ # We only initialize the vomnibar in the tab's main frame, because it's only ever opened there.
+ Vomnibar.init() if DomUtils.isTopFrame()
registerFrame = ->
# Don't register frameset containers; focusing them is no use.
@@ -247,10 +265,21 @@ unregisterFrame = ->
chrome.runtime.sendMessage
handler: "unregisterFrame"
frameId: frameId
- tab_is_closing: window.top == window.self
+ tab_is_closing: DomUtils.isTopFrame()
executePageCommand = (request) ->
- return unless frameId == request.frameId
+ # Vomnibar commands are handled in the tab's main/top frame. They are handled even if Vimium is otherwise
+ # disabled in the frame.
+ if request.command.split(".")[0] == "Vomnibar"
+ if DomUtils.isTopFrame()
+ # We pass the frameId from request. That's the frame which originated the request, so that's the frame
+ # which should receive the focus when the vomnibar closes.
+ Utils.invokeCommandString request.command, [ request.frameId ]
+ refreshCompletionKeys request
+ return
+
+ # All other commands are handled in their frame (but only if Vimium is enabled).
+ return unless frameId == request.frameId and isEnabledForUrl
if (request.passCountToFunction)
Utils.invokeCommandString(request.command, [request.count])
@@ -266,7 +295,7 @@ setScrollPosition = (scrollX, scrollY) ->
#
# Called from the backend in order to change frame focus.
#
-window.focusThisFrame = (shouldHighlight) ->
+window.focusThisFrame = (request) ->
if window.innerWidth < 3 or window.innerHeight < 3
# This frame is too small to focus. Cancel and tell the background frame to focus the next one instead.
# This affects sites like Google Inbox, which have many tiny iframes. See #1317.
@@ -274,7 +303,9 @@ window.focusThisFrame = (shouldHighlight) ->
chrome.runtime.sendMessage({ handler: "nextFrame", frameId: frameId })
return
window.focus()
- if (document.body && shouldHighlight)
+ shouldHighlight = request.highlight
+ shouldHighlight ||= request.highlightOnlyIfNotTop and not DomUtils.isTopFrame()
+ if document.body and shouldHighlight
borderWas = document.body.style.border
document.body.style.border = '5px solid yellow'
setTimeout((-> document.body.style.border = borderWas), 200)
@@ -1192,3 +1223,5 @@ root.settings = settings
root.HUD = HUD
root.handlerStack = handlerStack
root.frameId = frameId
+root.windowIsFocused = windowIsFocused
+root.bgLog = bgLog
diff --git a/content_scripts/vomnibar.coffee b/content_scripts/vomnibar.coffee
index c4cfc8b9..2529c077 100644
--- a/content_scripts/vomnibar.coffee
+++ b/content_scripts/vomnibar.coffee
@@ -4,31 +4,33 @@
Vomnibar =
vomnibarUI: null
- activate: -> @open {completer:"omni"}
- activateInNewTab: -> @open {
+ # sourceFrameId here (and below) is the ID of the frame from which this request originates, which may be different
+ # from the current frame.
+ activate: (sourceFrameId) -> @open sourceFrameId, {completer:"omni"}
+ activateInNewTab: (sourceFrameId) -> @open sourceFrameId, {
completer: "omni"
selectFirst: false
newTab: true
}
- activateTabSelection: -> @open {
+ activateTabSelection: (sourceFrameId) -> @open sourceFrameId, {
completer: "tabs"
selectFirst: true
}
- activateBookmarks: -> @open {
+ activateBookmarks: (sourceFrameId) -> @open sourceFrameId, {
completer: "bookmarks"
selectFirst: true
}
- activateBookmarksInNewTab: -> @open {
+ activateBookmarksInNewTab: (sourceFrameId) -> @open sourceFrameId, {
completer: "bookmarks"
selectFirst: true
newTab: true
}
- activateEditUrl: -> @open {
+ activateEditUrl: (sourceFrameId) -> @open sourceFrameId, {
completer: "omni"
selectFirst: false
query: window.location.href
}
- activateEditUrlInNewTab: -> @open {
+ activateEditUrlInNewTab: (sourceFrameId) -> @open sourceFrameId, {
completer: "omni"
selectFirst: false
query: window.location.href
@@ -38,9 +40,11 @@ Vomnibar =
init: ->
unless @vomnibarUI?
@vomnibarUI = new UIComponent "pages/vomnibar.html", "vomnibarFrame", (event) =>
- if event.data == "hide"
- @vomnibarUI.hide()
- @vomnibarUI.postMessage "hidden"
+ @vomnibarUI.hide() if event.data == "hide"
+ # Whenever the window receives the focus, we tell the Vomnibar UI that it has been hidden (regardless of
+ # whether it was previously visible).
+ window.addEventListener "focus", (event) =>
+ @vomnibarUI.postMessage "hidden" if event.target == window; true
# This function opens the vomnibar. It accepts options, a map with the values:
@@ -48,7 +52,7 @@ Vomnibar =
# query - Optional. Text to prefill the Vomnibar with.
# selectFirst - Optional, boolean. Whether to select the first entry.
# newTab - Optional, boolean. Whether to open the result in a new tab.
- open: (options) -> @vomnibarUI.activate options
+ open: (sourceFrameId, options) -> @vomnibarUI.activate extend options, { sourceFrameId }
root = exports ? window
root.Vomnibar = Vomnibar
diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee
index 0231f994..efb125f6 100644
--- a/lib/dom_utils.coffee
+++ b/lib/dom_utils.coffee
@@ -27,6 +27,12 @@ DomUtils =
removeElement: (el) -> el.parentNode.removeChild el
#
+ # Test whether the current frame is the top/main frame.
+ #
+ isTopFrame: ->
+ window.top == window.self
+
+ #
# Takes an array of XPath selectors, adds the necessary namespaces (currently only XHTML), and applies them
# to the document root. The namespaceResolver in evaluateXPath should be kept in sync with the namespaces
# here.
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index 06ec9ee9..b133b126 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -63,14 +63,14 @@ class VomnibarUI
# The sequence of events when the vomnibar is hidden is as follows:
# 1. Post a "hide" message to the host page.
- # 2. The host page hides the vomnibar and posts back a "hidden" message.
- # 3. Only once "hidden" message is received here is any required action (callback) invoked (in onHidden).
- # This ensures that the vomnibar is actually hidden, and avoids flicker after opening a link in a new tab
- # (see #1485).
- hide: (callback = null) ->
+ # 2. The host page hides the vomnibar.
+ # 3. When that page receives the focus, and it posts back a "hidden" message.
+ # 3. Only once the "hidden" message is received here is any required action invoked (in onHidden).
+ # This ensures that the vomnibar is actually hidden before any new tab is created, and avoids flicker after
+ # opening a link in a new tab then returning to the original tab (see #1485).
+ hide: (@postHideCallback = null) ->
UIComponentServer.postMessage "hide"
@reset()
- @postHideCallback = callback
onHidden: ->
@postHideCallback?()