Add initial Speak Words implementation for Mozilla Labs.

This commit is contained in:
Edward Lee 2010-10-14 12:47:06 -07:00
Коммит 8828c1b44f
2 изменённых файлов: 387 добавлений и 0 удалений

363
speakWords/bootstrap.js поставляемый Normal file
Просмотреть файл

@ -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());
}

24
speakWords/install.rdf Normal file
Просмотреть файл

@ -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>