diff options
| author | Jez Ng | 2012-07-26 02:04:31 -0700 | 
|---|---|---|
| committer | Jez Ng | 2012-07-26 02:14:00 -0700 | 
| commit | 8c60e25b52933fadbf45da891bfdee91400ede68 (patch) | |
| tree | c54489ac705584e85d3753feeb095f806c44bf8d | |
| parent | 73a73ac467d14be8f9dd4a635f5a84cbc95fb630 (diff) | |
| download | vimium-8c60e25b52933fadbf45da891bfdee91400ede68.tar.bz2 | |
Upgrade to manifest version 2.
I use the beta channel by default, and it no longer allows me to run
development builds with the old manifest version.
| -rw-r--r-- | background_page.html | 822 | ||||
| -rw-r--r-- | background_scripts/main.js | 810 | ||||
| -rw-r--r-- | manifest.json | 12 | ||||
| -rw-r--r-- | options.html | 114 | ||||
| -rw-r--r-- | options.js | 109 | 
5 files changed, 932 insertions, 935 deletions
| diff --git a/background_page.html b/background_page.html deleted file mode 100644 index 8215b389..00000000 --- a/background_page.html +++ /dev/null @@ -1,822 +0,0 @@ -<html> -<head> -<script type="text/javascript" src="lib/utils.js"></script> -<script type="text/javascript" src="background_scripts/commands.js"></script> -<script type="text/javascript" src="lib/clipboard.js"></script> -<script type="text/javascript" src="background_scripts/settings.js"></script> -<script type="text/javascript" src="background_scripts/completion.js"></script> -<script type="text/javascript" charset="utf-8"> -  var currentVersion = Utils.getCurrentVersion(); - -  var tabQueue = {}; // windowId -> Array -  var openTabs = {}; // tabId -> object with various tab properties -  var keyQueue = ""; // Queue of keys typed -  var validFirstKeys = {}; -  var singleKeyCommands = []; -  var focusedFrame = null; -  var framesForTab = {}; - -  // Keys are either literal characters, or "named" - for example <a-b> (alt+b), <left> (left arrow) or <f12> -  // This regular expression captures two groups: the first is a named key, the second is the remainder of -  // the string. -  var namedKeyRegex = /^(<(?:[amc]-.|(?:[amc]-)?[a-z0-9]{2,5})>)(.*)$/; - -  // Port handler mapping -  var portHandlers = { -    keyDown:              handleKeyDown, -    returnScrollPosition: handleReturnScrollPosition, -    getCurrentTabUrl:     getCurrentTabUrl, -    settings:             handleSettings, -    filterCompleter:      filterCompleter, -  }; - -  var sendRequestHandlers = { -    getCompletionKeys: getCompletionKeysRequest, -    getLinkHintCss: getLinkHintCss, -    openUrlInNewTab: openUrlInNewTab, -    openUrlInCurrentTab: openUrlInCurrentTab, -    openOptionsPageInNewTab: openOptionsPageInNewTab, -    registerFrame: registerFrame, -    frameFocused: handleFrameFocused, -    upgradeNotificationClosed: upgradeNotificationClosed, -    updateScrollPosition: handleUpdateScrollPosition, -    copyToClipboard: copyToClipboard, -    isEnabledForUrl: isEnabledForUrl, -    saveHelpDialogSettings: saveHelpDialogSettings, -    selectSpecificTab: selectSpecificTab, -    refreshCompleter: refreshCompleter, -  }; - -  // Event handlers -  var selectionChangedHandlers = []; -  var getScrollPositionHandlers = {}; // tabId -> function(tab, scrollX, scrollY); -  var tabLoadedHandlers = {}; // tabId -> function() - -  var completionSources = { -    bookmarks: new BookmarkCompleter(), -    history: new HistoryCompleter(), -    domains: new DomainCompleter(), -    tabs: new TabCompleter() -  }; - -  var completers = { -    omni: new MultiCompleter([ -      completionSources.bookmarks, -      completionSources.history, -      completionSources.domains]), -    bookmarks: new MultiCompleter([completionSources.bookmarks]), -    tabs: new MultiCompleter([completionSources.tabs]) -  }; - -  chrome.extension.onConnect.addListener(function(port, name) { -    var senderTabId = port.sender.tab ? port.sender.tab.id : null; -    // If this is a tab we've been waiting to open, execute any "tab loaded" handlers, e.g. to restore -    // the tab's scroll position. Wait until domReady before doing this; otherwise operations like restoring -    // the scroll position will not be possible. -    if (port.name == "domReady" && senderTabId != null) { -      if (tabLoadedHandlers[senderTabId]) { -        var toCall = tabLoadedHandlers[senderTabId]; -        // Delete first to be sure there's no circular events. -        delete tabLoadedHandlers[senderTabId]; -        toCall.call(); -      } - -      // domReady is the appropriate time to show the "vimium has been upgraded" message. -      // TODO: This might be broken on pages with frames. -      if (shouldShowUpgradeMessage()) -        chrome.tabs.sendRequest(senderTabId, { name: "showUpgradeNotification", version: currentVersion }); -    } - -    if (portHandlers[port.name]) -      port.onMessage.addListener(portHandlers[port.name]); -  }); - -  chrome.extension.onRequest.addListener(function (request, sender, sendResponse) { -    var senderTabId = sender.tab ? sender.tab.id : null; -    if (sendRequestHandlers[request.handler]) -      sendResponse(sendRequestHandlers[request.handler](request, sender)); -  }); - -  function handleReturnScrollPosition(args) { -    if (getScrollPositionHandlers[args.currentTab.id]) { -      // Delete first to be sure there's no circular events. -      var toCall = getScrollPositionHandlers[args.currentTab.id]; -      delete getScrollPositionHandlers[args.currentTab.id]; -      toCall(args.currentTab, args.scrollX, args.scrollY); -    } -  } - -  /* -   * Used by the content scripts to get their full URL. This is needed for URLs like "view-source:http:// .." -   * because window.location doesn't know anything about the Chrome-specific "view-source:". -   */ -  function getCurrentTabUrl(args, port) { -    var returnPort = chrome.tabs.connect(port.tab.id, { name: "returnCurrentTabUrl" }); -    returnPort.postMessage({ url: port.tab.url }); -  } - -  /* -   * Checks the user's preferences in local storage to determine if Vimium is enabled for the given URL. -   */ -  function isEnabledForUrl(request) { -    // excludedUrls are stored as a series of URL expressions separated by newlines. -    var excludedUrls = Settings.get("excludedUrls").split("\n"); -    var isEnabled = true; -    for (var i = 0; i < excludedUrls.length; i++) { -      // The user can add "*" to the URL which means ".*" -      var regexp = new RegExp("^" + excludedUrls[i].replace(/\*/g, ".*") + "$"); -      if (request.url.match(regexp)) -        isEnabled = false; -    } -    return { isEnabledForUrl: isEnabled }; -  } - -  /* -   * Called by the popup UI. Strips leading/trailing whitespace and ignores empty strings. -   */ -  function addExcludedUrl(url) { -    url = trim(url); -    if (url === "") { return; } - -    var excludedUrls = Settings.get("excludedUrls"); -    excludedUrls += "\n" + url; -    Settings.set("excludedUrls", excludedUrls); - -    chrome.tabs.query({ windowId: chrome.windows.WINDOW_ID_CURRENT, active: true }, function(tabs) { -      updateActiveState(tabs[0].id); -    }); -  } - -  function saveHelpDialogSettings(request) { -    Settings.set("helpDialog_showAdvancedCommands", request.showAdvancedCommands); -  } - -  function showHelp(callback, frameId) { -    chrome.tabs.getSelected(null, function(tab) { -      chrome.tabs.sendRequest(tab.id, -        { name: "showHelpDialog", dialogHtml: helpDialogHtml(), frameId:frameId }); -    }); -  } - -  /* -   * Retrieves the help dialog HTML template from a file, and populates it with the latest keybindings. -   */ -  function helpDialogHtml(showUnboundCommands, showCommandNames, customTitle) { -    var commandsToKey = {}; -    for (var key in Commands.keyToCommandRegistry) { -      var command = Commands.keyToCommandRegistry[key].command; -      commandsToKey[command] = (commandsToKey[command] || []).concat(key); -    } -    var dialogHtml = fetchFileContents("help_dialog.html"); -    for (var group in Commands.commandGroups) -      dialogHtml = dialogHtml.replace("{{" + group + "}}", -          helpDialogHtmlForCommandGroup(group, commandsToKey, Commands.availableCommands, -                                        showUnboundCommands, showCommandNames)); -    dialogHtml = dialogHtml.replace("{{version}}", currentVersion); -    dialogHtml = dialogHtml.replace("{{title}}", customTitle || "Help"); -    dialogHtml = dialogHtml.replace("{{showAdvancedCommands}}", -        Settings.get("helpDialog_showAdvancedCommands")); -    return dialogHtml; -  } - -  /* -   * Generates HTML for a given set of commands. commandGroups are defined in commands.js -   */ -  function helpDialogHtmlForCommandGroup(group, commandsToKey, availableCommands, -                                         showUnboundCommands, showCommandNames) { -    var html = []; -    for (var i = 0; i < Commands.commandGroups[group].length; i++) { -      var command = Commands.commandGroups[group][i]; -      bindings = (commandsToKey[command] || [""]).join(", "); -      if (showUnboundCommands || commandsToKey[command]) { -        html.push( -          "<tr class='vimiumReset " + -              (Commands.advancedCommands.indexOf(command) >= 0 ? "advanced" : "") + "'>", -          "<td class='vimiumReset'>", Utils.escapeHtml(bindings), "</td>", -          "<td class='vimiumReset'>:</td><td class='vimiumReset'>", availableCommands[command].description); - -        if (showCommandNames) -          html.push("<span class='vimiumReset commandName'>(" + command + ")</span>"); - -        html.push("</td></tr>"); -      } -    } -    return html.join("\n"); -  } - -  /* -   * Fetches the contents of a file bundled with this extension. -   */ -  function fetchFileContents(extensionFileName) { -    var req = new XMLHttpRequest(); -    req.open("GET", chrome.extension.getURL(extensionFileName), false); // false => synchronous -    req.send(); -    return req.responseText; -  } - -  /** -   * Returns the keys that can complete a valid command given the current key queue. -   */ -  function getCompletionKeysRequest(request) { -    return { name: "refreshCompletionKeys", -             completionKeys: generateCompletionKeys(), -             validFirstKeys: validFirstKeys -           }; -  } - -  /* -    * Opens the url in the current tab. -    */ -   function openUrlInCurrentTab(request) { -     chrome.tabs.getSelected(null, function(tab) { -       chrome.tabs.update(tab.id, { url: Utils.convertToUrl(request.url) }); -     }); -   } - -  /* -   * Opens request.url in new tab and switches to it if request.selected is true. -   */ -  function openUrlInNewTab(request) { -    chrome.tabs.getSelected(null, function(tab) { -      chrome.tabs.create({ url: Utils.convertToUrl(request.url), index: tab.index + 1, selected: true }); -    }); -  } - -  function openCopiedUrlInCurrentTab(request) { openUrlInCurrentTab({ url: Clipboard.paste() }); } - -  function openCopiedUrlInNewTab(request) { openUrlInNewTab({ url: Clipboard.paste() }); } - -  /* -   * Returns the user-provided CSS overrides. -   */ -  function getLinkHintCss(request) { -    return { linkHintCss: (Settings.get("userDefinedLinkHintCss") || "") }; -  } - -  /* -   * Called when the user has clicked the close icon on the "Vimium has been updated" message. -   * We should now dismiss that message in all tabs. -   */ -  function upgradeNotificationClosed(request) { -    Settings.set("previousVersion", currentVersion); -    sendRequestToAllTabs({ name: "hideUpgradeNotification" }); -  } - -  /* -   * Copies some data (request.data) to the clipboard. -   */ -  function copyToClipboard(request) { -    Clipboard.copy(request.data); -  } - -  /** -    * Selects the tab with the ID specified in request.id -    */ -  function selectSpecificTab(request) { -    chrome.tabs.update(request.id, { selected: true }); -  } - -  /* -   * Used by the content scripts to get settings from the local storage. -   */ -  function handleSettings(args, port) { -    if (args.operation == "get") { -      var value = Settings.get(args.key); -      port.postMessage({ key: args.key, value: value }); -    } -    else { // operation == "set" -      Settings.set(args.key, args.value); -    } -  } - -  function refreshCompleter(request) { -    completers[request.name].refresh(); -  } - -  function filterCompleter(args, port) { -    var queryTerms = args.query == "" ? [] : args.query.split(" "); -    completers[args.name].filter(queryTerms, function(results) { -      port.postMessage({ id: args.id, results: results }); -    }); -  } - -  /* -   * Used by everyone to get settings from local storage. -   */ -  function getSettingFromLocalStorage(setting) { -    if (localStorage[setting] != "" && !localStorage[setting]) { -      return defaultSettings[setting]; -    } else { -      return localStorage[setting]; -    } -  } - -  function getCurrentTimeInSeconds() { Math.floor((new Date()).getTime() / 1000); } - -  chrome.tabs.onSelectionChanged.addListener(function(tabId, selectionInfo) { -    if (selectionChangedHandlers.length > 0) { selectionChangedHandlers.pop().call(); } -  }); - -  function repeatFunction(func, totalCount, currentCount, frameId) { -    if (currentCount < totalCount) -      func(function() { repeatFunction(func, totalCount, currentCount + 1, frameId); }, frameId); -  } - -  // Returns the scroll coordinates of the given tab. Pass in a callback of the form: -  //   function(tab, scrollX, scrollY) { .. } -  function getScrollPosition(tab, callback) { -    getScrollPositionHandlers[tab.id] = callback; -    var scrollPort = chrome.tabs.connect(tab.id, { name: "getScrollPosition" }); -    scrollPort.postMessage({currentTab: tab}); -  } - -  // Start action functions -  function createTab(callback) { -    chrome.tabs.create({}, function(tab) { callback(); }); -  } - -  function nextTab(callback) { selectTab(callback, "next"); } -  function previousTab(callback) { selectTab(callback, "previous"); } -  function firstTab(callback) { selectTab(callback, "first"); } -  function lastTab(callback) { selectTab(callback, "last"); } - -  /* -   * Selects a tab before or after the currently selected tab. Direction is either "next", "previous", "first" or "last". -   */ -  function selectTab(callback, direction) { -    chrome.tabs.getAllInWindow(null, function(tabs) { -      if (tabs.length <= 1) -        return; -      chrome.tabs.getSelected(null, function(currentTab) { -          switch (direction) { -            case "next": -              toSelect = tabs[(currentTab.index + 1 + tabs.length) % tabs.length]; -              break; -            case "previous": -              toSelect = tabs[(currentTab.index - 1 + tabs.length) % tabs.length]; -              break; -            case "first": -              toSelect = tabs[0]; -              break; -            case "last": -              toSelect = tabs[tabs.length - 1]; -              break; -          } -          selectionChangedHandlers.push(callback); -          chrome.tabs.update(toSelect.id, { selected: true }); -      }); -    }); -  } - -  function removeTab(callback) { -    chrome.tabs.getSelected(null, function(tab) { -      chrome.tabs.remove(tab.id); -      // We can't just call the callback here because we actually need to wait -      // for the selection to change to consider this action done. -      selectionChangedHandlers.push(callback); -    }); -  } - -  function updateOpenTabs(tab) { -    openTabs[tab.id] = { url: tab.url, positionIndex: tab.index, windowId: tab.windowId }; -    // Frames are recreated on refresh -    delete framesForTab[tab.id]; -  } - -  /* Updates the browserAction icon to indicated whether Vimium is enabled or disabled on the current page. -   * Also disables Vimium if it is currently enabled but should be disabled according to the url blacklist. -   * This lets you disable Vimium on a page without needing to reload. -   * -   * Three situations are considered: -   * 1. Active tab is disabled -> disable icon -   * 2. Active tab is enabled and should be enabled -> enable icon -   * 3. Active tab is enabled but should be disabled -> disable icon and disable vimium -   */ -  function updateActiveState(tabId) { -    // TODO(philc): Re-enable once we've restyled the browser action icon. -    return; -    var enabledIcon = "icons/icon48.png"; -    var disabledIcon = "icons/icon48disabled.png"; -    chrome.tabs.get(tabId, function(tab) { -      // Default to disabled state in case we can't connect to Vimium, primarily for the "New Tab" page. -      // TODO(philc): Re-enable once we've restyled the browser action icon. -      // chrome.browserAction.setIcon({ path: disabledIcon }); -      var returnPort = chrome.tabs.connect(tabId, { name: "getActiveState" }); -      returnPort.onMessage.addListener(function(response) { -        var isCurrentlyEnabled = response.enabled; -        var shouldBeEnabled = isEnabledForUrl({url: tab.url}).isEnabledForUrl; - -        if (isCurrentlyEnabled) { -          if (shouldBeEnabled) { -            chrome.browserAction.setIcon({ path: enabledIcon }); -          } else { -            chrome.browserAction.setIcon({ path: disabledIcon }); -            chrome.tabs.connect(tabId, { name: "disableVimium" }).postMessage(); -          } -        } else { -          chrome.browserAction.setIcon({ path: disabledIcon }); -        } -      }); -      returnPort.postMessage(); -    }); -  } - -  function handleUpdateScrollPosition(request, sender) { -    updateScrollPosition(sender.tab, request.scrollX, request.scrollY); -  } - -  function updateScrollPosition(tab, scrollX, scrollY) { -    openTabs[tab.id].scrollX = scrollX; -    openTabs[tab.id].scrollY = scrollY; -  } - - -  chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) { -    if (changeInfo.status != "loading") { return; } // only do this once per URL change -    updateOpenTabs(tab); -    updateActiveState(tabId); -  }); - -  chrome.tabs.onAttached.addListener(function(tabId, attachedInfo) { -    // We should update all the tabs in the old window and the new window. -    if (openTabs[tabId]) { -      updatePositionsAndWindowsForAllTabsInWindow(openTabs[tabId].windowId); -    } -    updatePositionsAndWindowsForAllTabsInWindow(attachedInfo.newWindowId); -  }); - -  chrome.tabs.onMoved.addListener(function(tabId, moveInfo) { -    updatePositionsAndWindowsForAllTabsInWindow(moveInfo.windowId); -  }); - -  chrome.tabs.onRemoved.addListener(function(tabId) { -    var openTabInfo = openTabs[tabId]; -    updatePositionsAndWindowsForAllTabsInWindow(openTabInfo.windowId); - -    // If we restore chrome:// pages, they'll ignore Vimium keystrokes when they reappear. -    // Pretend they never existed and adjust tab indices accordingly. -    // Could possibly expand this into a blacklist in the future -    if (/^chrome[^:]*:\/\/.*/.test(openTabInfo.url)) { -      for (var i in tabQueue[openTabInfo.windowId]) { -        if (tabQueue[openTabInfo.windowId][i].positionIndex > openTabInfo.positionIndex) -          tabQueue[openTabInfo.windowId][i].positionIndex--; -      } -      return; -    } - -    if (tabQueue[openTabInfo.windowId]) -      tabQueue[openTabInfo.windowId].push(openTabInfo); -    else -      tabQueue[openTabInfo.windowId] = [openTabInfo]; - -    delete openTabs[tabId]; -    delete framesForTab[tabId]; -  }); - -  chrome.tabs.onActiveChanged.addListener(function(tabId, selectInfo) { -    updateActiveState(tabId); -  }); - -  chrome.windows.onRemoved.addListener(function(windowId) { -    delete tabQueue[windowId]; -  }); - -  function restoreTab(callback) { -    // TODO(ilya): Should this be getLastFocused instead? -    chrome.windows.getCurrent(function(window) { -      if (tabQueue[window.id] && tabQueue[window.id].length > 0) -      { -        var tabQueueEntry = tabQueue[window.id].pop(); - -        // Clean out the tabQueue so we don't have unused windows laying about. -        if (tabQueue[window.id].length == 0) -          delete tabQueue[window.id]; - -        // We have to chain a few callbacks to set the appropriate scroll position. We can't just wait until the -        // tab is created because the content script is not available during the "loading" state. We need to -        // wait until that's over before we can call setScrollPosition. -        chrome.tabs.create({ url: tabQueueEntry.url, index: tabQueueEntry.positionIndex }, function(tab) { -          tabLoadedHandlers[tab.id] = function() { -            var scrollPort = chrome.tabs.connect(tab.id, {name: "setScrollPosition"}); -            scrollPort.postMessage({ scrollX: tabQueueEntry.scrollX, scrollY: tabQueueEntry.scrollY }); -          }; - -          callback(); -        }); -      } -    }); -  } -  // End action functions - -  function updatePositionsAndWindowsForAllTabsInWindow(windowId) { -    chrome.tabs.getAllInWindow(windowId, function (tabs) { -      for (var i = 0; i < tabs.length; i++) { -        var tab = tabs[i]; -        var openTabInfo = openTabs[tab.id]; -        if (openTabInfo) { -          openTabInfo.positionIndex = tab.index; -          openTabInfo.windowId = tab.windowId; -        } -      } -    }); -  } - -  function splitKeyIntoFirstAndSecond(key) { -    if (key.search(namedKeyRegex) == 0) -        return { first: RegExp.$1, second: RegExp.$2 }; -    else -      return { first: key[0], second: key.slice(1) }; -  } - -  function getActualKeyStrokeLength(key) { -    if (key.search(namedKeyRegex) == 0) -      return 1 + getActualKeyStrokeLength(RegExp.$2); -    else -      return key.length; -  } - -  function populateValidFirstKeys() { -    for (var key in Commands.keyToCommandRegistry) -    { -      if (getActualKeyStrokeLength(key) == 2) -        validFirstKeys[splitKeyIntoFirstAndSecond(key).first] = true; -    } -  } - -  function populateSingleKeyCommands() { -    for (var key in Commands.keyToCommandRegistry) -    { -      if (getActualKeyStrokeLength(key) == 1) -        singleKeyCommands.push(key); -    } -  } - -  function refreshCompletionKeysAfterMappingSave() { -    validFirstKeys = {}; -    singleKeyCommands = []; - -    populateValidFirstKeys(); -    populateSingleKeyCommands(); - -    sendRequestToAllTabs(getCompletionKeysRequest()); -  } - -  /* -   * Generates a list of keys that can complete a valid command given the current key queue or the one passed -   * in. -   */ -  function generateCompletionKeys(keysToCheck) { -    var splitHash = splitKeyQueue(keysToCheck || keyQueue); -    command = splitHash.command; -    count = splitHash.count; - -    var completionKeys = singleKeyCommands.slice(0); - -    if (getActualKeyStrokeLength(command) == 1) -    { -      for (var key in Commands.keyToCommandRegistry) -      { -        var splitKey = splitKeyIntoFirstAndSecond(key); -        if (splitKey.first == command) -         completionKeys.push(splitKey.second); -      } -    } - -    return completionKeys; -  } - -  function splitKeyQueue(queue) { -    var match = /([1-9][0-9]*)?(.*)/.exec(queue); -    var count = parseInt(match[1]); -    var command = match[2]; - -    return {count: count, command: command}; -  } - -  function handleKeyDown(request, port) { -    var key = request.keyChar; -    if (key == "<ESC>") { -      console.log("clearing keyQueue"); -      keyQueue = "" -    } -    else { -      console.log("checking keyQueue: [", keyQueue + key, "]"); -      keyQueue = checkKeyQueue(keyQueue + key, port.tab.id, request.frameId); -      console.log("new KeyQueue: " + keyQueue); -    } -  } - -  function checkKeyQueue(keysToCheck, tabId, frameId) { -    var refreshedCompletionKeys = false; -    var splitHash = splitKeyQueue(keysToCheck); -    command = splitHash.command; -    count = splitHash.count; - -    if (command.length == 0) { return keysToCheck; } -    if (isNaN(count)) { count = 1; } - -    if (Commands.keyToCommandRegistry[command]) { -      registryEntry = Commands.keyToCommandRegistry[command]; - -      if (!registryEntry.isBackgroundCommand) { -        var port = chrome.tabs.connect(tabId, { name: "executePageCommand" }); -        port.postMessage({ command: registryEntry.command, -                           frameId: frameId, -                           count: count, -                           passCountToFunction: registryEntry.passCountToFunction, -                           completionKeys: generateCompletionKeys("") -                         }); - -        refreshedCompletionKeys = true; -      } else { -        if(registryEntry.passCountToFunction){ -          this[registryEntry.command](count); -        } else { -          repeatFunction(this[registryEntry.command], count, 0, frameId); -        } -      } - -      newKeyQueue = ""; -    } else if (getActualKeyStrokeLength(command) > 1) { -      var splitKey = splitKeyIntoFirstAndSecond(command); - -      // The second key might be a valid command by its self. -      if (Commands.keyToCommandRegistry[splitKey.second]) -        newKeyQueue = checkKeyQueue(splitKey.second, tabId, frameId); -      else -        newKeyQueue = (validFirstKeys[splitKey.second] ? splitKey.second : ""); -    } else { -      newKeyQueue = (validFirstKeys[command] ? count.toString() + command : ""); -    } - -    // If we haven't sent the completion keys piggybacked on executePageCommand, -    // send them by themselves. -    if (!refreshedCompletionKeys) { -      chrome.tabs.sendRequest(tabId, getCompletionKeysRequest(), null); -    } - -    return newKeyQueue; -  } - -  /* -   * Message all tabs. Args should be the arguments hash used by the Chrome sendRequest API. -   */ -  function sendRequestToAllTabs(args) { -    chrome.windows.getAll({ populate: true }, function(windows) { -      for (var i = 0; i < windows.length; i++) -        for (var j = 0; j < windows[i].tabs.length; j++) -          chrome.tabs.sendRequest(windows[i].tabs[j].id, args, null); -    }); -  } - -  // Compares two version strings (e.g. "1.1" and "1.5") and returns -  // -1 if versionA is < versionB, 0 if they're equal, and 1 if versionA is > versionB. -  function compareVersions(versionA, versionB) { -    versionA = versionA.split("."); -    versionB = versionB.split("."); -    for (var i = 0; i < Math.max(versionA.length, versionB.length); i++) { -      var a = parseInt(versionA[i] || 0); -      var b = parseInt(versionB[i] || 0); -      if (a < b) return -1; -      else if (a > b) return 1; -    } -    return 0; -  } - -  /* -   * Returns true if the current extension version is greater than the previously recorded version in -   * localStorage, and false otherwise. -   */ -  function shouldShowUpgradeMessage() { -    // Avoid showing the upgrade notification when previousVersion is undefined, which is the case for new -    // installs. -    if (!Settings.get("previousVersion")) -      Settings.set("previousVersion", currentVersion); -    return compareVersions(currentVersion, Settings.get("previousVersion")) == 1; -  } - -  function openOptionsPageInNewTab() { -    chrome.tabs.getSelected(null, function(tab) { -      chrome.tabs.create({ url: chrome.extension.getURL("options.html"), index: tab.index + 1 }); -    }); -  } - -  function registerFrame(request, sender) { -    if (!framesForTab[sender.tab.id]) -      framesForTab[sender.tab.id] = { frames: [] }; - -    if (request.is_top) { -      focusedFrame = request.frameId; -      framesForTab[sender.tab.id].total = request.total; -    } - -    framesForTab[sender.tab.id].frames.push({ id: request.frameId, area: request.area }); - -    // We've seen all the frames. Time to focus the largest one. -    // NOTE: Disabled because it's buggy with iframes. -    // if (framesForTab[sender.tab.id].frames.length >= framesForTab[sender.tab.id].total) -    //  focusLargestFrame(sender.tab.id); -  } - -  function focusLargestFrame(tabId) { -    var mainFrameId = null; -    var mainFrameArea = 0; - -    for (var i = 0; i < framesForTab[tabId].frames.length; i++) { -      var currentFrame = framesForTab[tabId].frames[i]; - -      if (currentFrame.area > mainFrameArea) { -        mainFrameId = currentFrame.id; -        mainFrameArea = currentFrame.area; -      } -    } - -    chrome.tabs.sendRequest(tabId, { name: "focusFrame", frameId: mainFrameId, highlight: false }); -  } - -  function handleFrameFocused(request, sender) { -    focusedFrame = request.frameId; -  } - -  function nextFrame(count) { -    chrome.tabs.getSelected(null, function(tab) { -      var frames = framesForTab[tab.id].frames; -      var curr_index = getCurrFrameIndex(frames); - -      // TODO: Skip the "top" frame (which doesn't actually have a <frame> tag), -      // since it exists only to contain the other frames. -      var new_index = (curr_index + count) % frames.length; - -      chrome.tabs.sendRequest(tab.id, { name: "focusFrame", frameId: frames[new_index].id, highlight: true }); -    }); -  } - -  function getCurrFrameIndex(frames) { -    var index; -    for (index=0; index < frames.length; index++) { -      if (frames[index].id == focusedFrame) -          break; -    } -    return index; -  } - -  /* -   * Convenience function for trimming leading and trailing whitespace. -   */ -  function trim(str) { -    return str.replace(/^\s*/, "").replace(/\s*$/, ""); -  } - -  function init() { -    Commands.clearKeyMappingsAndSetDefaults(); - -    if (Settings.has("keyMappings")) -      Commands.parseCustomKeyMappings(Settings.get("keyMappings")); - -    // In version 1.22, we changed the mapping for "d" and "u" to be scroll page down/up instead of close -    // and restore tab. For existing users, we want to preserve existing behavior for them by adding some -    // custom key mappings on their behalf. -    if (Settings.get("previousVersion") == "1.21") { -      var customKeyMappings = Settings.get("keyMappings") || ""; -      if ((Commands.keyToCommandRegistry["d"] || {}).command == "scrollPageDown") -        customKeyMappings += "\nmap d removeTab"; -      if ((Commands.keyToCommandRegistry["u"] || {}).command == "scrollPageUp") -        customKeyMappings += "\nmap u restoreTab"; -      if (customKeyMappings != "") { -        Settings.set("keyMappings", customKeyMappings); -        Commands.parseCustomKeyMappings(customKeyMappings); -      } -    } - -    populateValidFirstKeys(); -    populateSingleKeyCommands(); -    if (shouldShowUpgradeMessage()) -      sendRequestToAllTabs({ name: "showUpgradeNotification", version: currentVersion }); - -    // Ensure that openTabs is populated when Vimium is installed. -    chrome.windows.getAll({ populate: true }, function(windows) { -      for (var i in windows) { -        for (var j in windows[i].tabs) { -          var tab = windows[i].tabs[j]; -          updateOpenTabs(tab); -          getScrollPosition(tab, function(tab, scrollX, scrollY) { -            // Not using the tab defined in the for loop because -            // it might have changed by the time this callback is activated. -            updateScrollPosition(tab, scrollX, scrollY); -          }); -        } -      } -    }); -  } -  init(); - -  /** -   * Convenience function for development use. -   */ -  function runTests() { -    open(chrome.extension.getURL('test_harnesses/automated.html')); -  } - -</script> -</head> -</html> diff --git a/background_scripts/main.js b/background_scripts/main.js new file mode 100644 index 00000000..49790b87 --- /dev/null +++ b/background_scripts/main.js @@ -0,0 +1,810 @@ +var currentVersion = Utils.getCurrentVersion(); + +var tabQueue = {}; // windowId -> Array +var openTabs = {}; // tabId -> object with various tab properties +var keyQueue = ""; // Queue of keys typed +var validFirstKeys = {}; +var singleKeyCommands = []; +var focusedFrame = null; +var framesForTab = {}; + +// Keys are either literal characters, or "named" - for example <a-b> (alt+b), <left> (left arrow) or <f12> +// This regular expression captures two groups: the first is a named key, the second is the remainder of +// the string. +var namedKeyRegex = /^(<(?:[amc]-.|(?:[amc]-)?[a-z0-9]{2,5})>)(.*)$/; + +// Port handler mapping +var portHandlers = { +  keyDown:              handleKeyDown, +  returnScrollPosition: handleReturnScrollPosition, +  getCurrentTabUrl:     getCurrentTabUrl, +  settings:             handleSettings, +  filterCompleter:      filterCompleter +}; + +var sendRequestHandlers = { +  getCompletionKeys: getCompletionKeysRequest, +  getLinkHintCss: getLinkHintCss, +  openUrlInNewTab: openUrlInNewTab, +  openUrlInCurrentTab: openUrlInCurrentTab, +  openOptionsPageInNewTab: openOptionsPageInNewTab, +  registerFrame: registerFrame, +  frameFocused: handleFrameFocused, +  upgradeNotificationClosed: upgradeNotificationClosed, +  updateScrollPosition: handleUpdateScrollPosition, +  copyToClipboard: copyToClipboard, +  isEnabledForUrl: isEnabledForUrl, +  saveHelpDialogSettings: saveHelpDialogSettings, +  selectSpecificTab: selectSpecificTab, +  refreshCompleter: refreshCompleter +}; + +// Event handlers +var selectionChangedHandlers = []; +var getScrollPositionHandlers = {}; // tabId -> function(tab, scrollX, scrollY); +var tabLoadedHandlers = {}; // tabId -> function() + +var completionSources = { +  bookmarks: new BookmarkCompleter(), +  history: new HistoryCompleter(), +  domains: new DomainCompleter(), +  tabs: new TabCompleter() +}; + +var completers = { +  omni: new MultiCompleter([ +    completionSources.bookmarks, +    completionSources.history, +    completionSources.domains]), +  bookmarks: new MultiCompleter([completionSources.bookmarks]), +  tabs: new MultiCompleter([completionSources.tabs]) +}; + +chrome.extension.onConnect.addListener(function(port, name) { +  var senderTabId = port.sender.tab ? port.sender.tab.id : null; +  // If this is a tab we've been waiting to open, execute any "tab loaded" handlers, e.g. to restore +  // the tab's scroll position. Wait until domReady before doing this; otherwise operations like restoring +  // the scroll position will not be possible. +  if (port.name === "domReady" && senderTabId !== null) { +    if (tabLoadedHandlers[senderTabId]) { +      var toCall = tabLoadedHandlers[senderTabId]; +      // Delete first to be sure there's no circular events. +      delete tabLoadedHandlers[senderTabId]; +      toCall.call(); +    } + +    // domReady is the appropriate time to show the "vimium has been upgraded" message. +    // TODO: This might be broken on pages with frames. +    if (shouldShowUpgradeMessage()) +      chrome.tabs.sendRequest(senderTabId, { name: "showUpgradeNotification", version: currentVersion }); +  } + +  if (portHandlers[port.name]) +    port.onMessage.addListener(portHandlers[port.name]); +}); + +chrome.extension.onRequest.addListener(function (request, sender, sendResponse) { +  var senderTabId = sender.tab ? sender.tab.id : null; +  if (sendRequestHandlers[request.handler]) +    sendResponse(sendRequestHandlers[request.handler](request, sender)); +}); + +function handleReturnScrollPosition(args) { +  if (getScrollPositionHandlers[args.currentTab.id]) { +    // Delete first to be sure there's no circular events. +    var toCall = getScrollPositionHandlers[args.currentTab.id]; +    delete getScrollPositionHandlers[args.currentTab.id]; +    toCall(args.currentTab, args.scrollX, args.scrollY); +  } +} + +/* + * Used by the content scripts to get their full URL. This is needed for URLs like "view-source:http:// .." + * because window.location doesn't know anything about the Chrome-specific "view-source:". + */ +function getCurrentTabUrl(args, port) { +  var returnPort = chrome.tabs.connect(port.sender.tab.id, { name: "returnCurrentTabUrl" }); +  returnPort.postMessage({ url: port.sender.tab.url }); +} + +/* + * Checks the user's preferences in local storage to determine if Vimium is enabled for the given URL. + */ +function isEnabledForUrl(request) { +  // excludedUrls are stored as a series of URL expressions separated by newlines. +  var excludedUrls = Settings.get("excludedUrls").split("\n"); +  var isEnabled = true; +  for (var i = 0; i < excludedUrls.length; i++) { +    // The user can add "*" to the URL which means ".*" +    var regexp = new RegExp("^" + excludedUrls[i].replace(/\*/g, ".*") + "$"); +    if (request.url.match(regexp)) +      isEnabled = false; +  } +  return { isEnabledForUrl: isEnabled }; +} + +/* + * Called by the popup UI. Strips leading/trailing whitespace and ignores empty strings. + */ +function addExcludedUrl(url) { +  url = trim(url); +  if (url === "") { return; } + +  var excludedUrls = Settings.get("excludedUrls"); +  excludedUrls += "\n" + url; +  Settings.set("excludedUrls", excludedUrls); + +  chrome.tabs.query({ windowId: chrome.windows.WINDOW_ID_CURRENT, active: true }, function(tabs) { +    updateActiveState(tabs[0].id); +  }); +} + +function saveHelpDialogSettings(request) { +  Settings.set("helpDialog_showAdvancedCommands", request.showAdvancedCommands); +} + +function showHelp(callback, frameId) { +  chrome.tabs.getSelected(null, function(tab) { +    chrome.tabs.sendRequest(tab.id, +      { name: "showHelpDialog", dialogHtml: helpDialogHtml(), frameId:frameId }); +  }); +} + +/* + * Retrieves the help dialog HTML template from a file, and populates it with the latest keybindings. + */ +function helpDialogHtml(showUnboundCommands, showCommandNames, customTitle) { +  var commandsToKey = {}; +  for (var key in Commands.keyToCommandRegistry) { +    var command = Commands.keyToCommandRegistry[key].command; +    commandsToKey[command] = (commandsToKey[command] || []).concat(key); +  } +  var dialogHtml = fetchFileContents("help_dialog.html"); +  for (var group in Commands.commandGroups) +    dialogHtml = dialogHtml.replace("{{" + group + "}}", +        helpDialogHtmlForCommandGroup(group, commandsToKey, Commands.availableCommands, +                                      showUnboundCommands, showCommandNames)); +  dialogHtml = dialogHtml.replace("{{version}}", currentVersion); +  dialogHtml = dialogHtml.replace("{{title}}", customTitle || "Help"); +  dialogHtml = dialogHtml.replace("{{showAdvancedCommands}}", +      Settings.get("helpDialog_showAdvancedCommands")); +  return dialogHtml; +} + +/* + * Generates HTML for a given set of commands. commandGroups are defined in commands.js + */ +function helpDialogHtmlForCommandGroup(group, commandsToKey, availableCommands, +                                       showUnboundCommands, showCommandNames) { +  var html = []; +  for (var i = 0; i < Commands.commandGroups[group].length; i++) { +    var command = Commands.commandGroups[group][i]; +    bindings = (commandsToKey[command] || [""]).join(", "); +    if (showUnboundCommands || commandsToKey[command]) { +      html.push( +        "<tr class='vimiumReset " + +            (Commands.advancedCommands.indexOf(command) >= 0 ? "advanced" : "") + "'>", +        "<td class='vimiumReset'>", Utils.escapeHtml(bindings), "</td>", +        "<td class='vimiumReset'>:</td><td class='vimiumReset'>", availableCommands[command].description); + +      if (showCommandNames) +        html.push("<span class='vimiumReset commandName'>(" + command + ")</span>"); + +      html.push("</td></tr>"); +    } +  } +  return html.join("\n"); +} + +/* + * Fetches the contents of a file bundled with this extension. + */ +function fetchFileContents(extensionFileName) { +  var req = new XMLHttpRequest(); +  req.open("GET", chrome.extension.getURL(extensionFileName), false); // false => synchronous +  req.send(); +  return req.responseText; +} + +/** + * Returns the keys that can complete a valid command given the current key queue. + */ +function getCompletionKeysRequest(request) { +  return { name: "refreshCompletionKeys", +           completionKeys: generateCompletionKeys(), +           validFirstKeys: validFirstKeys +         }; +} + +/* +  * Opens the url in the current tab. +  */ + function openUrlInCurrentTab(request) { +   chrome.tabs.getSelected(null, function(tab) { +     chrome.tabs.update(tab.id, { url: Utils.convertToUrl(request.url) }); +   }); + } + +/* + * Opens request.url in new tab and switches to it if request.selected is true. + */ +function openUrlInNewTab(request) { +  chrome.tabs.getSelected(null, function(tab) { +    chrome.tabs.create({ url: Utils.convertToUrl(request.url), index: tab.index + 1, selected: true }); +  }); +} + +function openCopiedUrlInCurrentTab(request) { openUrlInCurrentTab({ url: Clipboard.paste() }); } + +function openCopiedUrlInNewTab(request) { openUrlInNewTab({ url: Clipboard.paste() }); } + +/* + * Returns the user-provided CSS overrides. + */ +function getLinkHintCss(request) { +  return { linkHintCss: (Settings.get("userDefinedLinkHintCss") || "") }; +} + +/* + * Called when the user has clicked the close icon on the "Vimium has been updated" message. + * We should now dismiss that message in all tabs. + */ +function upgradeNotificationClosed(request) { +  Settings.set("previousVersion", currentVersion); +  sendRequestToAllTabs({ name: "hideUpgradeNotification" }); +} + +/* + * Copies some data (request.data) to the clipboard. + */ +function copyToClipboard(request) { +  Clipboard.copy(request.data); +} + +/** +  * Selects the tab with the ID specified in request.id +  */ +function selectSpecificTab(request) { +  chrome.tabs.update(request.id, { selected: true }); +} + +/* + * Used by the content scripts to get settings from the local storage. + */ +function handleSettings(args, port) { +  if (args.operation == "get") { +    var value = Settings.get(args.key); +    port.postMessage({ key: args.key, value: value }); +  } +  else { // operation == "set" +    Settings.set(args.key, args.value); +  } +} + +function refreshCompleter(request) { +  completers[request.name].refresh(); +} + +function filterCompleter(args, port) { +  var queryTerms = args.query === "" ? [] : args.query.split(" "); +  completers[args.name].filter(queryTerms, function(results) { +    port.postMessage({ id: args.id, results: results }); +  }); +} + +/* + * Used by everyone to get settings from local storage. + */ +function getSettingFromLocalStorage(setting) { +  if (localStorage[setting] !== "" && !localStorage[setting]) { +    return defaultSettings[setting]; +  } else { +    return localStorage[setting]; +  } +} + +function getCurrentTimeInSeconds() { Math.floor((new Date()).getTime() / 1000); } + +chrome.tabs.onSelectionChanged.addListener(function(tabId, selectionInfo) { +  if (selectionChangedHandlers.length > 0) { selectionChangedHandlers.pop().call(); } +}); + +function repeatFunction(func, totalCount, currentCount, frameId) { +  if (currentCount < totalCount) +    func(function() { repeatFunction(func, totalCount, currentCount + 1, frameId); }, frameId); +} + +// Returns the scroll coordinates of the given tab. Pass in a callback of the form: +//   function(tab, scrollX, scrollY) { .. } +function getScrollPosition(tab, callback) { +  getScrollPositionHandlers[tab.id] = callback; +  var scrollPort = chrome.tabs.connect(tab.id, { name: "getScrollPosition" }); +  scrollPort.postMessage({currentTab: tab}); +} + +// Start action functions +function createTab(callback) { +  chrome.tabs.create({}, function(tab) { callback(); }); +} + +function nextTab(callback) { selectTab(callback, "next"); } +function previousTab(callback) { selectTab(callback, "previous"); } +function firstTab(callback) { selectTab(callback, "first"); } +function lastTab(callback) { selectTab(callback, "last"); } + +/* + * Selects a tab before or after the currently selected tab. Direction is either "next", "previous", "first" or "last". + */ +function selectTab(callback, direction) { +  chrome.tabs.getAllInWindow(null, function(tabs) { +    if (tabs.length <= 1) +      return; +    chrome.tabs.getSelected(null, function(currentTab) { +        switch (direction) { +          case "next": +            toSelect = tabs[(currentTab.index + 1 + tabs.length) % tabs.length]; +            break; +          case "previous": +            toSelect = tabs[(currentTab.index - 1 + tabs.length) % tabs.length]; +            break; +          case "first": +            toSelect = tabs[0]; +            break; +          case "last": +            toSelect = tabs[tabs.length - 1]; +            break; +        } +        selectionChangedHandlers.push(callback); +        chrome.tabs.update(toSelect.id, { selected: true }); +    }); +  }); +} + +function removeTab(callback) { +  chrome.tabs.getSelected(null, function(tab) { +    chrome.tabs.remove(tab.id); +    // We can't just call the callback here because we actually need to wait +    // for the selection to change to consider this action done. +    selectionChangedHandlers.push(callback); +  }); +} + +function updateOpenTabs(tab) { +  openTabs[tab.id] = { url: tab.url, positionIndex: tab.index, windowId: tab.windowId }; +  // Frames are recreated on refresh +  delete framesForTab[tab.id]; +} + +/* Updates the browserAction icon to indicated whether Vimium is enabled or disabled on the current page. + * Also disables Vimium if it is currently enabled but should be disabled according to the url blacklist. + * This lets you disable Vimium on a page without needing to reload. + * + * Three situations are considered: + * 1. Active tab is disabled -> disable icon + * 2. Active tab is enabled and should be enabled -> enable icon + * 3. Active tab is enabled but should be disabled -> disable icon and disable vimium + */ +function updateActiveState(tabId) { +  // TODO(philc): Re-enable once we've restyled the browser action icon. +  return; +  var enabledIcon = "icons/icon48.png"; +  var disabledIcon = "icons/icon48disabled.png"; +  chrome.tabs.get(tabId, function(tab) { +    // Default to disabled state in case we can't connect to Vimium, primarily for the "New Tab" page. +    // TODO(philc): Re-enable once we've restyled the browser action icon. +    // chrome.browserAction.setIcon({ path: disabledIcon }); +    var returnPort = chrome.tabs.connect(tabId, { name: "getActiveState" }); +    returnPort.onMessage.addListener(function(response) { +      var isCurrentlyEnabled = response.enabled; +      var shouldBeEnabled = isEnabledForUrl({url: tab.url}).isEnabledForUrl; + +      if (isCurrentlyEnabled) { +        if (shouldBeEnabled) { +          chrome.browserAction.setIcon({ path: enabledIcon }); +        } else { +          chrome.browserAction.setIcon({ path: disabledIcon }); +          chrome.tabs.connect(tabId, { name: "disableVimium" }).postMessage(); +        } +      } else { +        chrome.browserAction.setIcon({ path: disabledIcon }); +      } +    }); +    returnPort.postMessage(); +  }); +} + +function handleUpdateScrollPosition(request, sender) { +  updateScrollPosition(sender.tab, request.scrollX, request.scrollY); +} + +function updateScrollPosition(tab, scrollX, scrollY) { +  openTabs[tab.id].scrollX = scrollX; +  openTabs[tab.id].scrollY = scrollY; +} + + +chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) { +  if (changeInfo.status != "loading") { return; } // only do this once per URL change +  updateOpenTabs(tab); +  updateActiveState(tabId); +}); + +chrome.tabs.onAttached.addListener(function(tabId, attachedInfo) { +  // We should update all the tabs in the old window and the new window. +  if (openTabs[tabId]) { +    updatePositionsAndWindowsForAllTabsInWindow(openTabs[tabId].windowId); +  } +  updatePositionsAndWindowsForAllTabsInWindow(attachedInfo.newWindowId); +}); + +chrome.tabs.onMoved.addListener(function(tabId, moveInfo) { +  updatePositionsAndWindowsForAllTabsInWindow(moveInfo.windowId); +}); + +chrome.tabs.onRemoved.addListener(function(tabId) { +  var openTabInfo = openTabs[tabId]; +  updatePositionsAndWindowsForAllTabsInWindow(openTabInfo.windowId); + +  // If we restore chrome:// pages, they'll ignore Vimium keystrokes when they reappear. +  // Pretend they never existed and adjust tab indices accordingly. +  // Could possibly expand this into a blacklist in the future +  if (/^chrome[^:]*:\/\/.*/.test(openTabInfo.url)) { +    for (var i in tabQueue[openTabInfo.windowId]) { +      if (tabQueue[openTabInfo.windowId][i].positionIndex > openTabInfo.positionIndex) +        tabQueue[openTabInfo.windowId][i].positionIndex--; +    } +    return; +  } + +  if (tabQueue[openTabInfo.windowId]) +    tabQueue[openTabInfo.windowId].push(openTabInfo); +  else +    tabQueue[openTabInfo.windowId] = [openTabInfo]; + +  delete openTabs[tabId]; +  delete framesForTab[tabId]; +}); + +chrome.tabs.onActiveChanged.addListener(function(tabId, selectInfo) { +  updateActiveState(tabId); +}); + +chrome.windows.onRemoved.addListener(function(windowId) { +  delete tabQueue[windowId]; +}); + +function restoreTab(callback) { +  // TODO(ilya): Should this be getLastFocused instead? +  chrome.windows.getCurrent(function(window) { +    if (tabQueue[window.id] && tabQueue[window.id].length > 0) +    { +      var tabQueueEntry = tabQueue[window.id].pop(); + +      // Clean out the tabQueue so we don't have unused windows laying about. +      if (tabQueue[window.id].length === 0) +        delete tabQueue[window.id]; + +      // We have to chain a few callbacks to set the appropriate scroll position. We can't just wait until the +      // tab is created because the content script is not available during the "loading" state. We need to +      // wait until that's over before we can call setScrollPosition. +      chrome.tabs.create({ url: tabQueueEntry.url, index: tabQueueEntry.positionIndex }, function(tab) { +        tabLoadedHandlers[tab.id] = function() { +          var scrollPort = chrome.tabs.connect(tab.id, {name: "setScrollPosition"}); +          scrollPort.postMessage({ scrollX: tabQueueEntry.scrollX, scrollY: tabQueueEntry.scrollY }); +        }; + +        callback(); +      }); +    } +  }); +} +// End action functions + +function updatePositionsAndWindowsForAllTabsInWindow(windowId) { +  chrome.tabs.getAllInWindow(windowId, function (tabs) { +    for (var i = 0; i < tabs.length; i++) { +      var tab = tabs[i]; +      var openTabInfo = openTabs[tab.id]; +      if (openTabInfo) { +        openTabInfo.positionIndex = tab.index; +        openTabInfo.windowId = tab.windowId; +      } +    } +  }); +} + +function splitKeyIntoFirstAndSecond(key) { +  if (key.search(namedKeyRegex) === 0) +      return { first: RegExp.$1, second: RegExp.$2 }; +  else +    return { first: key[0], second: key.slice(1) }; +} + +function getActualKeyStrokeLength(key) { +  if (key.search(namedKeyRegex) === 0) +    return 1 + getActualKeyStrokeLength(RegExp.$2); +  else +    return key.length; +} + +function populateValidFirstKeys() { +  for (var key in Commands.keyToCommandRegistry) +  { +    if (getActualKeyStrokeLength(key) == 2) +      validFirstKeys[splitKeyIntoFirstAndSecond(key).first] = true; +  } +} + +function populateSingleKeyCommands() { +  for (var key in Commands.keyToCommandRegistry) +  { +    if (getActualKeyStrokeLength(key) == 1) +      singleKeyCommands.push(key); +  } +} + +function refreshCompletionKeysAfterMappingSave() { +  validFirstKeys = {}; +  singleKeyCommands = []; + +  populateValidFirstKeys(); +  populateSingleKeyCommands(); + +  sendRequestToAllTabs(getCompletionKeysRequest()); +} + +/* + * Generates a list of keys that can complete a valid command given the current key queue or the one passed + * in. + */ +function generateCompletionKeys(keysToCheck) { +  var splitHash = splitKeyQueue(keysToCheck || keyQueue); +  command = splitHash.command; +  count = splitHash.count; + +  var completionKeys = singleKeyCommands.slice(0); + +  if (getActualKeyStrokeLength(command) == 1) +  { +    for (var key in Commands.keyToCommandRegistry) +    { +      var splitKey = splitKeyIntoFirstAndSecond(key); +      if (splitKey.first == command) +       completionKeys.push(splitKey.second); +    } +  } + +  return completionKeys; +} + +function splitKeyQueue(queue) { +  var match = /([1-9][0-9]*)?(.*)/.exec(queue); +  var count = parseInt(match[1], 10); +  var command = match[2]; + +  return {count: count, command: command}; +} + +function handleKeyDown(request, port) { +  var key = request.keyChar; +  if (key == "<ESC>") { +    console.log("clearing keyQueue"); +    keyQueue = ""; +  } +  else { +    console.log("checking keyQueue: [", keyQueue + key, "]"); +    keyQueue = checkKeyQueue(keyQueue + key, port.sender.tab.id, request.frameId); +    console.log("new KeyQueue: " + keyQueue); +  } +} + +function checkKeyQueue(keysToCheck, tabId, frameId) { +  var refreshedCompletionKeys = false; +  var splitHash = splitKeyQueue(keysToCheck); +  command = splitHash.command; +  count = splitHash.count; + +  if (command.length === 0) { return keysToCheck; } +  if (isNaN(count)) { count = 1; } + +  if (Commands.keyToCommandRegistry[command]) { +    registryEntry = Commands.keyToCommandRegistry[command]; + +    if (!registryEntry.isBackgroundCommand) { +      var port = chrome.tabs.connect(tabId, { name: "executePageCommand" }); +      port.postMessage({ command: registryEntry.command, +                         frameId: frameId, +                         count: count, +                         passCountToFunction: registryEntry.passCountToFunction, +                         completionKeys: generateCompletionKeys("") +                       }); + +      refreshedCompletionKeys = true; +    } else { +      if(registryEntry.passCountToFunction){ +        this[registryEntry.command](count); +      } else { +        repeatFunction(this[registryEntry.command], count, 0, frameId); +      } +    } + +    newKeyQueue = ""; +  } else if (getActualKeyStrokeLength(command) > 1) { +    var splitKey = splitKeyIntoFirstAndSecond(command); + +    // The second key might be a valid command by its self. +    if (Commands.keyToCommandRegistry[splitKey.second]) +      newKeyQueue = checkKeyQueue(splitKey.second, tabId, frameId); +    else +      newKeyQueue = (validFirstKeys[splitKey.second] ? splitKey.second : ""); +  } else { +    newKeyQueue = (validFirstKeys[command] ? count.toString() + command : ""); +  } + +  // If we haven't sent the completion keys piggybacked on executePageCommand, +  // send them by themselves. +  if (!refreshedCompletionKeys) { +    chrome.tabs.sendRequest(tabId, getCompletionKeysRequest(), null); +  } + +  return newKeyQueue; +} + +/* + * Message all tabs. Args should be the arguments hash used by the Chrome sendRequest API. + */ +function sendRequestToAllTabs(args) { +  chrome.windows.getAll({ populate: true }, function(windows) { +    for (var i = 0; i < windows.length; i++) +      for (var j = 0; j < windows[i].tabs.length; j++) +        chrome.tabs.sendRequest(windows[i].tabs[j].id, args, null); +  }); +} + +// Compares two version strings (e.g. "1.1" and "1.5") and returns +// -1 if versionA is < versionB, 0 if they're equal, and 1 if versionA is > versionB. +function compareVersions(versionA, versionB) { +  versionA = versionA.split("."); +  versionB = versionB.split("."); +  for (var i = 0; i < Math.max(versionA.length, versionB.length); i++) { +    var a = parseInt(versionA[i] || 0, 10); +    var b = parseInt(versionB[i] || 0, 10); +    if (a < b) return -1; +    else if (a > b) return 1; +  } +  return 0; +} + +/* + * Returns true if the current extension version is greater than the previously recorded version in + * localStorage, and false otherwise. + */ +function shouldShowUpgradeMessage() { +  // Avoid showing the upgrade notification when previousVersion is undefined, which is the case for new +  // installs. +  if (!Settings.get("previousVersion")) +    Settings.set("previousVersion", currentVersion); +  return compareVersions(currentVersion, Settings.get("previousVersion")) == 1; +} + +function openOptionsPageInNewTab() { +  chrome.tabs.getSelected(null, function(tab) { +    chrome.tabs.create({ url: chrome.extension.getURL("options.html"), index: tab.index + 1 }); +  }); +} + +function registerFrame(request, sender) { +  if (!framesForTab[sender.tab.id]) +    framesForTab[sender.tab.id] = { frames: [] }; + +  if (request.is_top) { +    focusedFrame = request.frameId; +    framesForTab[sender.tab.id].total = request.total; +  } + +  framesForTab[sender.tab.id].frames.push({ id: request.frameId, area: request.area }); + +  // We've seen all the frames. Time to focus the largest one. +  // NOTE: Disabled because it's buggy with iframes. +  // if (framesForTab[sender.tab.id].frames.length >= framesForTab[sender.tab.id].total) +  //  focusLargestFrame(sender.tab.id); +} + +function focusLargestFrame(tabId) { +  var mainFrameId = null; +  var mainFrameArea = 0; + +  for (var i = 0; i < framesForTab[tabId].frames.length; i++) { +    var currentFrame = framesForTab[tabId].frames[i]; + +    if (currentFrame.area > mainFrameArea) { +      mainFrameId = currentFrame.id; +      mainFrameArea = currentFrame.area; +    } +  } + +  chrome.tabs.sendRequest(tabId, { name: "focusFrame", frameId: mainFrameId, highlight: false }); +} + +function handleFrameFocused(request, sender) { +  focusedFrame = request.frameId; +} + +function nextFrame(count) { +  chrome.tabs.getSelected(null, function(tab) { +    var frames = framesForTab[tab.id].frames; +    var curr_index = getCurrFrameIndex(frames); + +    // TODO: Skip the "top" frame (which doesn't actually have a <frame> tag), +    // since it exists only to contain the other frames. +    var new_index = (curr_index + count) % frames.length; + +    chrome.tabs.sendRequest(tab.id, { name: "focusFrame", frameId: frames[new_index].id, highlight: true }); +  }); +} + +function getCurrFrameIndex(frames) { +  var index; +  for (index=0; index < frames.length; index++) { +    if (frames[index].id == focusedFrame) +        break; +  } +  return index; +} + +/* + * Convenience function for trimming leading and trailing whitespace. + */ +function trim(str) { +  return str.replace(/^\s*/, "").replace(/\s*$/, ""); +} + +function init() { +  Commands.clearKeyMappingsAndSetDefaults(); + +  if (Settings.has("keyMappings")) +    Commands.parseCustomKeyMappings(Settings.get("keyMappings")); + +  // In version 1.22, we changed the mapping for "d" and "u" to be scroll page down/up instead of close +  // and restore tab. For existing users, we want to preserve existing behavior for them by adding some +  // custom key mappings on their behalf. +  if (Settings.get("previousVersion") == "1.21") { +    var customKeyMappings = Settings.get("keyMappings") || ""; +    if ((Commands.keyToCommandRegistry["d"] || {}).command == "scrollPageDown") +      customKeyMappings += "\nmap d removeTab"; +    if ((Commands.keyToCommandRegistry["u"] || {}).command == "scrollPageUp") +      customKeyMappings += "\nmap u restoreTab"; +    if (customKeyMappings !== "") { +      Settings.set("keyMappings", customKeyMappings); +      Commands.parseCustomKeyMappings(customKeyMappings); +    } +  } + +  populateValidFirstKeys(); +  populateSingleKeyCommands(); +  if (shouldShowUpgradeMessage()) +    sendRequestToAllTabs({ name: "showUpgradeNotification", version: currentVersion }); + +  // Ensure that openTabs is populated when Vimium is installed. +  chrome.windows.getAll({ populate: true }, function(windows) { +    for (var i in windows) { +      for (var j in windows[i].tabs) { +        var tab = windows[i].tabs[j]; +        updateOpenTabs(tab); +        getScrollPosition(tab, function(tab, scrollX, scrollY) { +          // Not using the tab defined in the for loop because +          // it might have changed by the time this callback is activated. +          updateScrollPosition(tab, scrollX, scrollY); +        }); +      } +    } +  }); +} +init(); + +/** + * Convenience function for development use. + */ +function runTests() { +  open(chrome.extension.getURL('test_harnesses/automated.html')); +} diff --git a/manifest.json b/manifest.json index ed27c157..5c451700 100644 --- a/manifest.json +++ b/manifest.json @@ -1,11 +1,21 @@  { +  "manifest_version": 2,    "name": "Vimium",    "version": "1.37",    "description": "The Hacker's Browser. Vimium provides keyboard shortcuts for navigation and control in the spirit of Vim.",    "icons": {  "16": "icons/icon16.png",                "48": "icons/icon48.png",               "128": "icons/icon128.png" }, -  "background_page": "background_page.html", +  "background": { +    "scripts": [ +      "lib/utils.js", +      "background_scripts/commands.js", +      "lib/clipboard.js", +      "background_scripts/settings.js", +      "background_scripts/completion.js", +      "background_scripts/main.js" +    ] +  },    "options_page": "options.html",    "permissions": [      "tabs", diff --git a/options.html b/options.html index f4b17997..52dbdef2 100644 --- a/options.html +++ b/options.html @@ -84,121 +84,11 @@      </style>    <link rel="stylesheet" type="text/css" href="vimium.css" /> -  <script type="text/javascript"> -  $ = function(id) { return document.getElementById(id); }; -  var bgSettings = chrome.extension.getBackgroundPage().Settings; - -  var editableFields = ["scrollStepSize", "excludedUrls", "linkHintCharacters", "userDefinedLinkHintCss", -                        "keyMappings", "filterLinkHints", "previousPatterns", "nextPatterns", "hideHud"]; - -  var canBeEmptyFields = ["excludedUrls", "keyMappings", "userDefinedLinkHintCss"]; - -  var postSaveHooks = { -    keyMappings: function (value) { -      commands = chrome.extension.getBackgroundPage().Commands; -      commands.clearKeyMappingsAndSetDefaults(); -      commands.parseCustomKeyMappings(value); -      chrome.extension.getBackgroundPage().refreshCompletionKeysAfterMappingSave(); -    } -  }; - -  function initializeOptions() { -    populateOptions(); - -    for (var i = 0; i < editableFields.length; i++) { -      $(editableFields[i]).addEventListener("keyup", onOptionKeyup, false); -      $(editableFields[i]).addEventListener("change", enableSaveButton, false); -      $(editableFields[i]).addEventListener("change", onDataLoaded, false); -    } - -    $("advancedOptions").addEventListener("click", openAdvancedOptions, false); -    $("showCommands").addEventListener("click", function () { -      showHelpDialog( -        chrome.extension.getBackgroundPage().helpDialogHtml(true, true, "Command Listing"), frameId); -    }, false); -  } - -  function onOptionKeyup(event) { -    if (event.target.getAttribute("type") !== "checkbox" && -        event.target.getAttribute("savedValue") != event.target.value) -      enableSaveButton(); -  } - -  function onDataLoaded() { -    $("linkHintCharacters").readOnly = $("filterLinkHints").checked; -  } - -  function enableSaveButton() { $("saveOptions").removeAttribute("disabled"); } - -  // Saves options to localStorage. -  function saveOptions() { -    // If the value is unchanged from the default, delete the preference from localStorage; this gives us -    // the freedom to change the defaults in the future. -    for (var i = 0; i < editableFields.length; i++) { -      var fieldName = editableFields[i]; -      var field = $(fieldName); - -      var fieldValue; -      if (field.getAttribute("type") == "checkbox") { -        fieldValue = field.checked; -      } else { -        fieldValue = field.value.trim(); -        field.value = fieldValue; -      } - -      // If it's empty and not a field that we allow to be empty, restore to the default value -      if (!fieldValue && canBeEmptyFields.indexOf(fieldName) == -1) { -        bgSettings.clear(fieldName); -        fieldValue = bgSettings.get(fieldName); -      } else -        bgSettings.set(fieldName, fieldValue); - -      $(fieldName).value = fieldValue; -      $(fieldName).setAttribute("savedValue", fieldValue); - -      if (postSaveHooks[fieldName]) { postSaveHooks[fieldName](fieldValue); } -    } -    $("saveOptions").disabled = true; -  } - -  // Restores select box state to saved value from localStorage. -  function populateOptions() { -    for (var i = 0; i < editableFields.length; i++) { -      var val = bgSettings.get(editableFields[i]) || ""; -      setFieldValue($(editableFields[i]), val); -    } -    onDataLoaded(); -  } - -  function restoreToDefaults() { -    for (var i = 0; i < editableFields.length; i++) { -      var val = bgSettings.defaults[editableFields[i]] || ""; -      setFieldValue($(editableFields[i]), val); -    } -    onDataLoaded(); -    enableSaveButton(); -  } - -  function setFieldValue(field, value) { -    if (field.getAttribute('type') == 'checkbox') -      field.checked = value; -    else { -      field.value = value; -      field.setAttribute("savedValue", value); -    } -  } - -  function openAdvancedOptions(event) { -    var elements = document.getElementsByClassName("advancedOption"); -    for (var i = 0; i < elements.length; i++) -      elements[i].style.display = (elements[i].style.display == "table-row") ? "none" : "table-row"; -    event.preventDefault(); -  } -  </script> +  <script type="text/javascript" src="options.js"></script>    </head> -  <body onload="initializeOptions()"> +  <body>      <h1>Vimium - Options</h1>      <table style="position:relative">        <tr> diff --git a/options.js b/options.js new file mode 100644 index 00000000..ff5427a8 --- /dev/null +++ b/options.js @@ -0,0 +1,109 @@ +$ = function(id) { return document.getElementById(id); }; +var bgSettings = chrome.extension.getBackgroundPage().Settings; + +var editableFields = ["scrollStepSize", "excludedUrls", "linkHintCharacters", "userDefinedLinkHintCss", +                      "keyMappings", "filterLinkHints", "previousPatterns", "nextPatterns", "hideHud"]; + +var canBeEmptyFields = ["excludedUrls", "keyMappings", "userDefinedLinkHintCss"]; + +var postSaveHooks = { +  keyMappings: function (value) { +    commands = chrome.extension.getBackgroundPage().Commands; +    commands.clearKeyMappingsAndSetDefaults(); +    commands.parseCustomKeyMappings(value); +    chrome.extension.getBackgroundPage().refreshCompletionKeysAfterMappingSave(); +  } +}; + +document.addEventListener("DOMContentLoaded", function() { +  populateOptions(); + +  for (var i = 0; i < editableFields.length; i++) { +    $(editableFields[i]).addEventListener("keyup", onOptionKeyup, false); +    $(editableFields[i]).addEventListener("change", enableSaveButton, false); +    $(editableFields[i]).addEventListener("change", onDataLoaded, false); +  } + +  $("advancedOptions").addEventListener("click", openAdvancedOptions, false); +  $("showCommands").addEventListener("click", function () { +    showHelpDialog( +      chrome.extension.getBackgroundPage().helpDialogHtml(true, true, "Command Listing"), frameId); +  }, false); +}); + +function onOptionKeyup(event) { +  if (event.target.getAttribute("type") !== "checkbox" && +      event.target.getAttribute("savedValue") != event.target.value) +    enableSaveButton(); +} + +function onDataLoaded() { +  $("linkHintCharacters").readOnly = $("filterLinkHints").checked; +} + +function enableSaveButton() { $("saveOptions").removeAttribute("disabled"); } + +// Saves options to localStorage. +function saveOptions() { +  // If the value is unchanged from the default, delete the preference from localStorage; this gives us +  // the freedom to change the defaults in the future. +  for (var i = 0; i < editableFields.length; i++) { +    var fieldName = editableFields[i]; +    var field = $(fieldName); + +    var fieldValue; +    if (field.getAttribute("type") == "checkbox") { +      fieldValue = field.checked; +    } else { +      fieldValue = field.value.trim(); +      field.value = fieldValue; +    } + +    // If it's empty and not a field that we allow to be empty, restore to the default value +    if (!fieldValue && canBeEmptyFields.indexOf(fieldName) == -1) { +      bgSettings.clear(fieldName); +      fieldValue = bgSettings.get(fieldName); +    } else +      bgSettings.set(fieldName, fieldValue); + +    $(fieldName).value = fieldValue; +    $(fieldName).setAttribute("savedValue", fieldValue); + +    if (postSaveHooks[fieldName]) { postSaveHooks[fieldName](fieldValue); } +  } +  $("saveOptions").disabled = true; +} + +// Restores select box state to saved value from localStorage. +function populateOptions() { +  for (var i = 0; i < editableFields.length; i++) { +    var val = bgSettings.get(editableFields[i]) || ""; +    setFieldValue($(editableFields[i]), val); +  } +  onDataLoaded(); +} + +function restoreToDefaults() { +  for (var i = 0; i < editableFields.length; i++) { +    var val = bgSettings.defaults[editableFields[i]] || ""; +    setFieldValue($(editableFields[i]), val); +  } +  onDataLoaded(); +  enableSaveButton(); +} + +function setFieldValue(field, value) { +  if (field.getAttribute('type') == 'checkbox') +    field.checked = value; +  else { +    field.value = value; +    field.setAttribute("savedValue", value); +  } +} + +function openAdvancedOptions(event) { +  var elements = document.getElementsByClassName("advancedOption"); +  for (var i = 0; i < elements.length; i++) +    elements[i].style.display = (elements[i].style.display == "table-row") ? "none" : "table-row"; +  event.preventDefault(); +} | 
