зеркало из https://github.com/mozilla/prospector.git
Add initial Speak Words implementation for Mozilla Labs.
This commit is contained in:
Коммит
8828c1b44f
|
@ -0,0 +1,363 @@
|
|||
/* ***** BEGIN LICENSE BLOCK *****
|
||||
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||
*
|
||||
* The contents of this file are subject to the Mozilla Public License Version
|
||||
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
* http://www.mozilla.org/MPL/
|
||||
*
|
||||
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing rights and limitations under the
|
||||
* License.
|
||||
*
|
||||
* The Original Code is Speak Words.
|
||||
*
|
||||
* The Initial Developer of the Original Code is The Mozilla Foundation.
|
||||
* Portions created by the Initial Developer are Copyright (C) 2010
|
||||
* the Initial Developer. All Rights Reserved.
|
||||
*
|
||||
* Contributor(s):
|
||||
* Edward Lee <edilee@mozilla.com>
|
||||
*
|
||||
* Alternatively, the contents of this file may be used under the terms of
|
||||
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||
* of those above. If you wish to allow use of your version of this file only
|
||||
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||
* use your version of this file under the terms of the MPL, indicate your
|
||||
* decision by deleting the provisions above and replace them with the notice
|
||||
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||
* the provisions above, a recipient may use your version of this file under
|
||||
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||
*
|
||||
* ***** END LICENSE BLOCK ***** */
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cu = Components.utils;
|
||||
Cu.import("resource://gre/modules/AddonManager.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
// Keep an array of functions to call when shutting down
|
||||
let unloaders = [];
|
||||
|
||||
// Keep a sorted list of keywords to suggest
|
||||
let sortedKeywords = [];
|
||||
|
||||
/**
|
||||
* Lookup a keyword to suggest for the provided query
|
||||
*/
|
||||
function getKeyword(query) {
|
||||
let queryLen = query.length;
|
||||
let sortedLen = sortedKeywords.length;
|
||||
for (let i = 0; i < sortedLen; i++) {
|
||||
let keyword = sortedKeywords[i];
|
||||
if (keyword.slice(0, queryLen) == query)
|
||||
return keyword;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically suggest a keyword when typing in the location bar
|
||||
*/
|
||||
function addKeywordSuggestions(window) {
|
||||
let urlBar = window.gURLBar;
|
||||
let deleting = false;
|
||||
let suggesting = false;
|
||||
|
||||
// Look for deletes to handle them better on input
|
||||
listen(urlBar, "keypress", function(event) {
|
||||
switch (event.keyCode) {
|
||||
case event.DOM_VK_BACK_SPACE:
|
||||
case event.DOM_VK_DELETE:
|
||||
deleting = true;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Watch for urlbar value input changes to suggest keywords
|
||||
listen(urlBar, "input", function(event) {
|
||||
// Don't try suggesting a keyword when the user wants to delete
|
||||
if (deleting) {
|
||||
// Clear out the last letter (in addition to the now-removed selection)
|
||||
if (suggesting) {
|
||||
urlBar.value = urlBar.value.slice(0, -1);
|
||||
suggesting = false;
|
||||
}
|
||||
deleting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// See if we can suggest a keyword if it isn't the current query
|
||||
let query = urlBar.textValue.toLowerCase();
|
||||
let keyword = getKeyword(query);
|
||||
if (keyword == null || keyword == query) {
|
||||
suggesting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Select the end of the suggestion to allow over-typing
|
||||
urlBar.value = keyword;
|
||||
urlBar.selectTextRange(query.length, keyword.length);
|
||||
suggesting = true;
|
||||
|
||||
// Make sure the search suggestions show up
|
||||
Utils.delay(function() urlBar.controller.startSearch(urlBar.value));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically select the first location bar result on pressing enter
|
||||
*/
|
||||
function addEnterSelects(window) {
|
||||
// Remember what auto-select if enter was hit after starting a search
|
||||
let autoSelectOn;
|
||||
// Keep track of last shown result's search string
|
||||
let lastSearch;
|
||||
|
||||
// Add some helper functions to various objects
|
||||
let gURLBar = window.gURLBar;
|
||||
let popup = gURLBar.popup;
|
||||
popup.__defineGetter__("noResults", function() {
|
||||
return this._matchCount == 0;
|
||||
});
|
||||
gURLBar.__defineGetter__("trimmedSearch", function() {
|
||||
return this.value.replace(/^\s+|\s+$/g, "");
|
||||
});
|
||||
gURLBar.__defineGetter__("willHandle", function() {
|
||||
// Potentially it's a url if there's no spaces
|
||||
let search = this.trimmedSearch;
|
||||
if (search.match(/ /) == null) {
|
||||
try {
|
||||
// Quit early if the input is already a URI
|
||||
return Cc["@mozilla.org/network/io-service;1"].
|
||||
getService(Ci.nsIIOService).newURI(gURLBar.value, null, null);
|
||||
}
|
||||
catch(ex) {}
|
||||
|
||||
try {
|
||||
// Quit early if the input is domain-like (e.g., site.com/page)
|
||||
return Cc["@mozilla.org/network/effective-tld-service;1"].
|
||||
getService(Ci.nsIEffectiveTLDService).
|
||||
getBaseDomainFromHost(gURLBar.value);
|
||||
}
|
||||
catch(ex) {}
|
||||
}
|
||||
|
||||
// Check if there's an search engine registered for the first keyword
|
||||
let keyword = search.split(/\s+/)[0];
|
||||
return Cc["@mozilla.org/browser/search-service;1"].
|
||||
getService(Ci.nsIBrowserSearchService).getEngineByAlias(keyword);
|
||||
});
|
||||
|
||||
// Wait for results to get added to the popup
|
||||
let (orig = popup._appendCurrentResult) {
|
||||
popup._appendCurrentResult = function() {
|
||||
// Run the original first to get results added
|
||||
orig.apply(this, arguments);
|
||||
|
||||
// Don't bother if something is already selected
|
||||
if (popup.selectedIndex >= 0)
|
||||
return;
|
||||
|
||||
// Make sure there's results
|
||||
if (popup.noResults)
|
||||
return;
|
||||
|
||||
// Don't auto-select if we have a url
|
||||
if (gURLBar.willHandle)
|
||||
return;
|
||||
|
||||
// We passed all the checks, so pretend the user has the first result
|
||||
// selected, so this causes the UI to show the selection style
|
||||
popup.selectedIndex = 0;
|
||||
|
||||
// If the just-added result is what to auto-select, make it happen
|
||||
if (autoSelectOn == gURLBar.trimmedSearch) {
|
||||
// Clear out what to auto-select now that we've done it once
|
||||
autoSelectOn = null;
|
||||
gURLBar.controller.handleEnter(true);
|
||||
}
|
||||
|
||||
// Remember this to notice if the search changes
|
||||
lastSearch = gURLBar.trimmedSearch;
|
||||
};
|
||||
|
||||
unloaders.push(function() popup._appendCurrentResult = orig);
|
||||
}
|
||||
|
||||
listen(gURLBar, "keydown", function(aEvent) {
|
||||
let KeyEvent = aEvent;
|
||||
switch (aEvent.keyCode) {
|
||||
// For movement keys, unselect the first item to allow editing
|
||||
case KeyEvent.DOM_VK_LEFT:
|
||||
case KeyEvent.DOM_VK_RIGHT:
|
||||
case KeyEvent.DOM_VK_HOME:
|
||||
popup.selectedIndex = -1;
|
||||
return;
|
||||
|
||||
// We're interested in handling enter (return), do so below
|
||||
case KeyEvent.DOM_VK_RETURN:
|
||||
break;
|
||||
|
||||
// For anything else, just ignore
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore special key combinations
|
||||
if (aEvent.shiftKey || aEvent.ctrlKey || aEvent.metaKey)
|
||||
return;
|
||||
|
||||
// Deselect if the selected result isn't for the current search
|
||||
if (!popup.noResults && lastSearch != gURLBar.trimmedSearch) {
|
||||
popup.selectedIndex = -1;
|
||||
|
||||
// If it's not a url, we'll want to auto-select the first result
|
||||
if (!gURLBar.willHandle) {
|
||||
autoSelectOn = gURLBar.trimmedSearch;
|
||||
|
||||
// Don't load what's typed in the location bar because it's a search
|
||||
aEvent.preventDefault();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Pretend the user pressed right in the location bar which will cause
|
||||
// the selected index to be filled in. If the user has already pressed
|
||||
// down to some other selection, it'll just show the same value.
|
||||
gURLBar.controller.handleKeyNavigation(KeyEvent.DOM_VK_RIGHT);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper that adds event listeners and remembers to remove on unload
|
||||
*/
|
||||
function listen(node, event, func) {
|
||||
node.addEventListener(event, func, true);
|
||||
unloaders.push(function() node.removeEventListener(event, func, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a callback to each open and new browser windows
|
||||
*/
|
||||
function trackOpenAndNewWindows(callback) {
|
||||
// Add functionality to existing windows
|
||||
let browserWindows = Services.wm.getEnumerator("navigator:browser");
|
||||
while (browserWindows.hasMoreElements()) {
|
||||
// On restart, the browser window might not be ready yet, so wait... :(
|
||||
let browserWindow = browserWindows.getNext();
|
||||
Utils.delay(function() callback(browserWindow), 1000);
|
||||
}
|
||||
|
||||
// Watch for new browser windows opening
|
||||
function windowWatcher(subject, topic) {
|
||||
if (topic != "domwindowopened")
|
||||
return;
|
||||
|
||||
subject.addEventListener("load", function() {
|
||||
subject.removeEventListener("load", arguments.callee, false);
|
||||
|
||||
// Now that the window has loaded, only register on browser windows
|
||||
let doc = subject.document.documentElement;
|
||||
if (doc.getAttribute("windowtype") == "navigator:browser")
|
||||
callback(subject);
|
||||
}, false);
|
||||
}
|
||||
Services.ww.registerNotification(windowWatcher);
|
||||
unloaders.push(function() Services.ww.unregisterNotification(windowWatcher));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the add-on being activated on install/enable
|
||||
*/
|
||||
function startup(data) AddonManager.getAddonByID(data.id, function(addon) {
|
||||
Cu.import("resource://services-sync/util.js");
|
||||
|
||||
// Add suggestions to all windows
|
||||
trackOpenAndNewWindows(addKeywordSuggestions);
|
||||
// Add enter-selects functionality to all windows
|
||||
trackOpenAndNewWindows(addEnterSelects);
|
||||
|
||||
// Use input history to discover keywords from typed letters
|
||||
let query = "SELECT * " +
|
||||
"FROM moz_inputhistory " +
|
||||
"JOIN moz_places " +
|
||||
"ON id = place_id " +
|
||||
"WHERE input NOT NULL " +
|
||||
"ORDER BY frecency DESC";
|
||||
let cols = ["input", "url", "title"];
|
||||
let stmt = Utils.createStatement(Svc.History.DBConnection, query);
|
||||
|
||||
// Break a string into individual words separated by the splitter
|
||||
function explode(text, splitter) {
|
||||
return (text || "").toLowerCase().split(splitter).filter(function(word) {
|
||||
// Only interested in not too-short words
|
||||
return word && word.length > 3;
|
||||
});
|
||||
}
|
||||
|
||||
// Keep a nested array of array of keywords -- 2 arrays per entry
|
||||
let allKeywords = [];
|
||||
Utils.queryAsync(stmt, cols).forEach(function({input, url, title}) {
|
||||
// Add keywords for word parts that start with the input word
|
||||
let word = input.trim().toLowerCase().split(/\s+/)[0];
|
||||
word = word.replace("www.", "");
|
||||
let wordLen = word.length;
|
||||
if (wordLen == 0)
|
||||
return;
|
||||
|
||||
function addKeywords(parts) {
|
||||
allKeywords.push(parts.filter(function(part) {
|
||||
return part.slice(0, wordLen) == word;
|
||||
}));
|
||||
}
|
||||
|
||||
// Add keywords from url (ignoring protocol) and title
|
||||
addKeywords(explode(url, /[\/:.?&#=%+]+/).slice(1));
|
||||
addKeywords(explode(title, /[\s\-\/\u2010-\u202f\"',.:;?!|()]/));
|
||||
});
|
||||
|
||||
// Add in some typed subdomains/domains as potential keywords
|
||||
function addDomains(extraQuery) {
|
||||
let query = "SELECT * FROM moz_places WHERE visit_count > 1 " + extraQuery;
|
||||
let cols = ["url"];
|
||||
let stmt = Utils.createStatement(Svc.History.DBConnection, query);
|
||||
Utils.queryAsync(stmt, cols).forEach(function({url}) {
|
||||
try {
|
||||
allKeywords.push(explode(url.match(/[\/@]([^\/@:]+)[\/:]/)[1], /\./));
|
||||
}
|
||||
// Must have be some strange format url that we probably don't care about
|
||||
catch(ex) {}
|
||||
});
|
||||
}
|
||||
addDomains("AND typed = 1 ORDER BY frecency DESC");
|
||||
addDomains("ORDER BY visit_count DESC LIMIT 100");
|
||||
addDomains("ORDER BY last_visit_date DESC LIMIT 100");
|
||||
|
||||
// Do a breadth first traversal of the keywords
|
||||
do {
|
||||
// Remove any empty results and stop if there's no more
|
||||
allKeywords = allKeywords.filter(function(keywords) keywords.length > 0);
|
||||
if (allKeywords.length == 0)
|
||||
break;
|
||||
|
||||
// Get the first keyword of each result and add if it doesn't exist
|
||||
allKeywords.map(function(keywords) {
|
||||
let keyword = keywords.shift();
|
||||
if (sortedKeywords.indexOf(keyword) == -1) {
|
||||
sortedKeywords.push(keyword);
|
||||
}
|
||||
});
|
||||
} while (true);
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle the add-on being deactivated on uninstall/disable
|
||||
*/
|
||||
function shutdown(data, reason) {
|
||||
unloaders.forEach(function(unload) unload());
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<r:RDF xmlns="http://www.mozilla.org/2004/em-rdf#"
|
||||
xmlns:r="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||
<r:Description about="urn:mozilla:install-manifest">
|
||||
<creator>Mozilla Labs</creator>
|
||||
<description>Fill in the rest of words as you type them into the location bar</description>
|
||||
<homepageURL>https://mozillalabs.com/prospector</homepageURL>
|
||||
<iconURL>http://mozillalabs.com/wp-content/themes/labs_project/img/prospector-header.png</iconURL>
|
||||
<id>speak.words@prospector.labs.mozilla</id>
|
||||
<name>Mozilla Labs: Prospector - Speak Words</name>
|
||||
<version>1</version>
|
||||
|
||||
<bootstrap>true</bootstrap>
|
||||
<type>2</type>
|
||||
|
||||
<targetApplication>
|
||||
<r:Description>
|
||||
<id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</id>
|
||||
<minVersion>4.0b4</minVersion>
|
||||
<maxVersion>4.0b8pre</maxVersion>
|
||||
</r:Description>
|
||||
</targetApplication>
|
||||
</r:Description>
|
||||
</r:RDF>
|
Загрузка…
Ссылка в новой задаче