Bug 595236 - Match Tabs From Other Windows in Panorama Search [r=dietrich, a=blocking]

--HG--
rename : browser/base/content/test/tabview/browser_tabview_search.js => browser/base/content/test/tabview/browser_tabview_multiwindow_search.js
extra : rebase_source : ff634626a815445d66fb94d4e1e263a79173affc
This commit is contained in:
Sean Dunn 2010-09-21 14:56:52 -07:00
parent 282c374265
commit e08e388ec4
10 changed files with 540 additions and 44 deletions

View File

@ -101,6 +101,11 @@ let TabView = {
Services.obs.addObserver(observer, "quit-application-requested", false);
}
},
// ----------
getContentWindow: function TabView_getContentWindow() {
return this._window;
},
// ----------
isVisible: function() {

View File

@ -109,6 +109,43 @@ function scorePatternMatch(pattern, matched, offset) {
return 0.0;
}
// ##########
// Class: TabUtils
//
// A collection of helper functions for dealing with both
// <TabItem>s and <xul:tab>s without having to worry which
// one is which.
var TabUtils = {
// ---------
// Function: _nameOfTab
// Given a <TabItem> or a <xul:tab> returns the tab's name.
nameOf: function TabUtils_nameOfTab(tab) {
// We can have two types of tabs: A <TabItem> or a <xul:tab>
// because we have to deal with both tabs represented inside
// of active Panoramas as well as for windows in which
// Panorama has yet to be activated. We uses object sniffing to
// determine the type of tab and then returns its name.
return tab.label != undefined ? tab.label : tab.nameEl.innerHTML;
},
// ---------
// Function: favURLOf
// Given a <TabItem> or a <xul:tab> returns the URL of tab's favicon.
faviconURLOf: function TabUtils_faviconURLOf(tab) {
return tab.image != undefined ? tab.image : tab.favEl.src;
},
// ---------
// Function: focus
// Given a <TabItem> or a <xul:tab>, focuses it and it's window.
focus: function TabUtils_focus(tab) {
// Convert a <TabItem> to a <xul:tab>
if (tab.tab != undefined) tab = tab.tab;
tab.ownerDocument.defaultView.gBrowser.selectedTab = tab;
tab.ownerDocument.defaultView.focus();
}
};
// ##########
// Class: TabMatcher
//
@ -119,66 +156,141 @@ function TabMatcher(term) {
this.term = term;
}
TabMatcher.prototype = {
TabMatcher.prototype = {
// ---------
// Function: _filterAndSortMatches
// Given an array of <TabItem>s and <xul:tab>s returns a new array
// of tabs whose name matched the search term, sorted by lexical
// closeness.
_filterAndSortForMatches: function TabMatcher__filterAndSortForMatches(tabs) {
var self = this;
tabs = tabs.filter(function(tab){
var name = TabUtils.nameOf(tab);
return name.match(self.term, "i");
});
tabs.sort(function sorter(x, y){
var yScore = scorePatternMatch(self.term, TabUtils.nameOf(y));
var xScore = scorePatternMatch(self.term, TabUtils.nameOf(x));
return yScore - xScore;
});
return tabs;
},
// ---------
// Function: _filterForUnmatches
// Given an array of <TabItem>s returns an unsorted array of tabs whose name
// does not match the the search term.
_filterForUnmatches: function TabMatcher__filterForUnmatches(tabs) {
var self = this;
return tabs.filter(function(tab) {
var name = tab.nameEl.innerHTML;
return !name.match(self.term, "i");
});
},
// ---------
// Function: _getTabsForOtherWindows
// Returns an array of <TabItem>s and <xul:tabs>s representing that
// tabs from all windows but the currently focused window. <TabItem>s
// will be returned for windows in which Panorama has been activated at
// least once, while <xul:tab>s will be return for windows in which
// Panorama has never been activated.
_getTabsForOtherWindows: function TabMatcher__getTabsForOtherWindows(){
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator);
var enumerator = wm.getEnumerator("navigator:browser");
var currentWindow = wm.getMostRecentWindow("navigator:browser");
var allTabs = [];
while (enumerator.hasMoreElements()) {
var win = enumerator.getNext();
// This function gets tabs from other windows: not the one you currently
// have focused.
if (win != currentWindow) {
// If TabView is around iterate over all tabs, else get the currently
// shown tabs...
tvWindow = win.TabView.getContentWindow();
if (tvWindow)
allTabs = allTabs.concat( tvWindow.TabItems.getItems() );
else
// win.gBrowser.tabs isn't a proper array, so we can't use concat
for (var i=0; i<win.gBrowser.tabs.length; i++) allTabs.push( win.gBrowser.tabs[i] );
}
}
return allTabs;
},
// ----------
// Function: matchedTabsFromOtherWindows
// Returns an array of <TabItem>s and <xul:tab>s that match the search term
// from all windows but the currently focused window. <TabItem>s will be
// returned for windows in which Panorama has been activated at least once,
// while <xul:tab>s will be return for windows in which Panorama has never
// been activated.
// (new TabMatcher("app")).matchedTabsFromOtherWindows();
matchedTabsFromOtherWindows: function TabMatcher_matchedTabsFromOtherWindows(){
if (this.term.length < 2)
return [];
var tabs = this._getTabsForOtherWindows();
tabs = this._filterAndSortForMatches(tabs);
return tabs;
},
// ----------
// Function: matched
// Returns an array of <TabItem>s which match the current search term.
// If the term is less than 2 characters in length, it returns
// nothing.
matched: function matched() {
var self = this;
matched: function TabMatcher_matched() {
if (this.term.length < 2)
return [];
var tabs = TabItems.getItems();
tabs = tabs.filter(function(tab){
var name = tab.nameEl.innerHTML;
return name.match(self.term, "i");
});
tabs.sort(function sorter(x, y){
var yScore = scorePatternMatch(self.term, y.nameEl.innerHTML);
var xScore = scorePatternMatch(self.term, x.nameEl.innerHTML);
return yScore - xScore;
});
tabs = this._filterAndSortForMatches(tabs);
return tabs;
},
// ----------
// Function: unmatched
// Returns all of <TabItem>s that .matched() doesn't return.
unmatched: function unmatched() {
var self = this;
unmatched: function TabMatcher_unmatched() {
var tabs = TabItems.getItems();
if ( this.term.length < 2 )
return tabs;
return tabs.filter(function(tab) {
var name = tab.nameEl.innerHTML;
return !name.match(self.term, "i");
});
return this._filterForUnmatches(tabs);
},
// ----------
// Function: doSearch
// Performs the search. Lets you provide two functions, one that is called
// on all matched tabs, and one that is called on all unmatched tabs.
// Both functions take two parameters: A <TabItem> and its integer index
// Performs the search. Lets you provide three functions.
// The first is on all matched tabs in the window, the second on all unmatched
// tabs in the window, and the third on all matched tabs in other windows.
// The first two functions take two parameters: A <TabItem> and its integer index
// indicating the absolute rank of the <TabItem> in terms of match to
// the search term.
doSearch: function(matchFunc, unmatchFunc) {
// the search term. The last function also takes two paramaters, but can be
// passed both <TabItem>s and <xul:tab>s and the index is offset by the
// number of matched tabs inside the window.
doSearch: function TabMatcher_doSearch(matchFunc, unmatchFunc, otherFunc) {
var matches = this.matched();
var unmatched = this.unmatched();
var otherMatches = this.matchedTabsFromOtherWindows();
matches.forEach(function(tab, i) {
matchFunc(tab, i);
});
otherMatches.forEach(function(tab,i){
otherFunc(tab, i+matches.length);
});
unmatched.forEach(function(tab, i) {
unmatchFunc(tab, i);
});
});
}
};
@ -247,9 +359,14 @@ SearchEventHandlerClass.prototype = {
if (event.which == event.DOM_VK_BACK_SPACE && term.length <= 1)
hideSearch(event);
var matches = (new TabMatcher(term)).matched();
if (event.which == event.DOM_VK_RETURN && matches.length > 0) {
matches[0].zoomIn();
var matcher = new TabMatcher(term);
var matches = matcher.matched();
var others = matcher.matchedTabsFromOtherWindows();
if (event.which == event.DOM_VK_RETURN && (matches.length > 0 || others.length > 0)) {
if (matches.length > 0)
matches[0].zoomIn();
else
TabUtils.focus(others[0]);
hideSearch(event);
}
},
@ -281,24 +398,23 @@ SearchEventHandlerClass.prototype = {
var TabHandlers = {
onMatch: function(tab, index){
tab.setZ(1010);
index != 0 ? tab.addClass("notMainMatch") : tab.removeClass("notMainMatch");
// Remove any existing handlers before adding the new ones.
// If we don't do this, then we may add more handlers than
// we remove.
iQ(tab.canvasEl)
tab.addClass("onTop");
index != 0 ? tab.addClass("notMainMatch") : tab.removeClass("notMainMatch");
// Remove any existing handlers before adding the new ones.
// If we don't do this, then we may add more handlers than
// we remove.
iQ(tab.canvasEl)
.unbind("mousedown", TabHandlers._hideHandler)
.unbind("mouseup", TabHandlers._showHandler);
iQ(tab.canvasEl)
iQ(tab.canvasEl)
.mousedown(TabHandlers._hideHandler)
.mouseup(TabHandlers._showHandler);
},
onUnmatch: function(tab, index){
// TODO: Set back as value per tab. bug 593902
tab.setZ(500);
iQ(tab.container).removeClass("onTop");
tab.removeClass("notMainMatch");
iQ(tab.canvasEl)
@ -306,6 +422,30 @@ var TabHandlers = {
.unbind("mouseup", TabHandlers._showHandler);
},
onOther: function(tab, index){
// Unlike the other on* functions, in this function tab can
// either be a <TabItem> or a <xul:tab>. In other functions
// it is always a <TabItem>. Also note that index is offset
// by the number of matches within the window.
var item = iQ("<div/>")
.addClass("inlineMatch")
.click(function(){
TabUtils.focus(tab);
});
iQ("<img/>")
.attr("src", TabUtils.faviconURLOf(tab) )
.appendTo(item);
iQ("<span/>")
.text( TabUtils.nameOf(tab) )
.appendTo(item);
index != 0 ? item.addClass("notMainMatch") : item.removeClass("notMainMatch");
item.appendTo("#results");
iQ("#otherresults").show();
},
_hideHandler: function(event){
iQ("#search").fadeOut();
TabHandlers._mouseDownLocation = {x:event.clientX, y:event.clientY};
@ -358,7 +498,14 @@ function hideSearch(event){
function performSearch() {
var matcher = new TabMatcher(iQ("#searchbox").val());
matcher.doSearch(TabHandlers.onMatch, TabHandlers.onUnmatch);
// Remove any previous other-window search results and
// hide the display area.
iQ("#results").empty();
iQ("#otherresults").hide();
iQ("#otherresults>.label").text(tabviewString("search.otherWindowTabs"));
matcher.doSearch(TabHandlers.onMatch, TabHandlers.onUnmatch, TabHandlers.onOther);
}
function ensureSearchShown(event){

View File

@ -226,3 +226,22 @@ body {
top: 0;
left: 0;
}
#otherresults{
position: absolute;
opacity: 0;
overflow: hidden;
}
.onTop{
z-index: 1010 !important;
}
.inlineMatch{
display: inline-block;
}
.inlineMatch>span{
display: inline-block;
overflow: hidden;
}

View File

@ -20,6 +20,10 @@
<div id="search">
<input id="searchbox" type="text"/>
<div id="otherresults">
<span class="label"></span>
<span id="results"></span>
</div>
</div>
<script type="text/javascript;version=1.8" src="tabview.js"></script>

View File

@ -55,6 +55,7 @@ _BROWSER_FILES = \
browser_tabview_exit_button.js \
browser_tabview_group.js \
browser_tabview_launch.js \
browser_tabview_multiwindow_search.js \
browser_tabview_search.js \
browser_tabview_snapping.js \
browser_tabview_undo_group.js \

View File

@ -0,0 +1,184 @@
/* ***** 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 tabview search test.
*
* The Initial Developer of the Original Code is
* Mozilla Foundation.
* Portions created by the Initial Developer are Copyright (C) 2010
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Raymond Lee <raymond@appcoast.com>
* Ehsan Akhgari <ehsan@mozilla.com>
* Sean Dunn <seanedunn@yahoo.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 ***** */
let newWindows = [];
function test() {
waitForExplicitFinish();
let windowOne = openDialog(location, "", "chrome,all,dialog=no", "data:text/html,");
let windowTwo;
windowOne.addEventListener("load", function() {
windowOne.gBrowser.selectedBrowser.addEventListener("load", function() {
windowOne.gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
windowTwo = openDialog(location, "", "chrome,all,dialog=no", "http://mochi.test:8888/");
windowTwo.addEventListener("load", function() {
windowTwo.gBrowser.selectedBrowser.addEventListener("load", function() {
windowTwo.gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
newWindows = [ windowOne, windowTwo ];
// show the tab view
window.addEventListener("tabviewshown", onTabViewWindowLoaded, false);
ok(!TabView.isVisible(), "Tab View is hidden");
TabView.toggle();
}, true);
}, false);
}, true);
}, false);
}
function onTabViewWindowLoaded() {
window.removeEventListener("tabviewshown", onTabViewWindowLoaded, false);
ok(TabView.isVisible(), "Tab View is visible");
let contentWindow = document.getElementById("tab-view").contentWindow;
let search = contentWindow.document.getElementById("search");
let searchButton = contentWindow.document.getElementById("searchbutton");
ok(searchButton, "Search button exists");
let onSearchEnabled = function() {
ok(search.style.display != "none", "Search is enabled");
contentWindow.removeEventListener(
"tabviewsearchenabled", onSearchEnabled, false);
searchTest(contentWindow);
}
contentWindow.addEventListener("tabviewsearchenabled", onSearchEnabled, false);
// enter search mode
EventUtils.sendMouseEvent({ type: "mousedown" }, searchButton, contentWindow);
}
// conveniently combine local and other window tab results from a query
function getMatchResults(contentWindow, query) {
let matcher = new contentWindow.TabMatcher(query);
let localMatchResults = matcher.matched();
let otherMatchResults = matcher.matchedTabsFromOtherWindows();
return localMatchResults.concat(otherMatchResults);
}
function searchTest(contentWindow) {
let searchBox = contentWindow.document.getElementById("searchbox");
let matcher = null;
let matchResults = [];
// get the titles of tabs.
let tabNames = [];
let tabItems = contentWindow.TabItems.getItems();
is(tabItems.length, 1, "Have only one tab in the current window's tab items");
tabItems.forEach(function(tab) {
tabNames.push(tab.nameEl.innerHTML);
});
newWindows.forEach(function(win) {
for(var i=0; i<win.gBrowser.tabs.length; ++i) {
tabNames.push(win.gBrowser.tabs[i].label);
}
});
ok(tabNames[0] && tabNames[0].length > 2,
"The title of tab item is longer than 2 chars")
// empty string
searchBox.setAttribute("value", "");
matchResults = getMatchResults(contentWindow, searchBox.getAttribute("value"));
ok(matchResults.length == 0, "Match nothing if it's an empty string");
// one char
searchBox.setAttribute("value", tabNames[0].charAt(0));
matchResults = getMatchResults(contentWindow, searchBox.getAttribute("value"));
ok(matchResults.length == 0,
"Match nothing if the length of search term is less than 2");
// the full title
searchBox.setAttribute("value", tabNames[2]);
matchResults = getMatchResults(contentWindow, searchBox.getAttribute("value"));
is(matchResults.length, 1,
"Match something when the whole title exists");
// part of titled
searchBox.setAttribute("value", tabNames[0].substr(1));
contentWindow.performSearch();
matchResults = getMatchResults(contentWindow, searchBox.getAttribute("value"));
is(matchResults.length, 1,
"Match something when a part of title exists");
cleanup(contentWindow);
}
function cleanup(contentWindow) {
contentWindow.hideSearch(null);
let onTabViewHidden = function() {
window.removeEventListener("tabviewhidden", onTabViewHidden, false);
ok(!TabView.isVisible(), "Tab View is hidden");
let numToClose = newWindows.length;
newWindows.forEach(function(win) {
whenWindowObservesOnce(win, "domwindowclosed", function() {
--numToClose;
if(numToClose==0) {
finish();
}
});
win.close();
});
}
window.addEventListener("tabviewhidden", onTabViewHidden, false);
EventUtils.synthesizeKey("VK_ENTER", {});
}
function whenWindowObservesOnce(win, topic, func) {
let windowWatcher = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
.getService(Components.interfaces.nsIWindowWatcher);
let origWin = win;
let origTopic = topic;
let origFunc = func;
function windowObserver(aSubject, aTopic, aData) {
let theWin = aSubject.QueryInterface(Ci.nsIDOMWindow);
if (origWin && theWin != origWin)
return;
if(aTopic == origTopic) {
windowWatcher.unregisterNotification(windowObserver);
origFunc.apply(this, []);
}
}
windowWatcher.registerNotification(windowObserver);
}

View File

@ -20,6 +20,7 @@
*
* Contributor(s):
* Raymond Lee <raymond@appcoast.com>
* Sean Dunn <seanedunn@yahoo.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
@ -117,6 +118,11 @@ function searchTest(contentWindow) {
searchBox.getAttribute("value")).matched().length == 2,
"Match something when a part of title exists");
cleanup(contentWindow);
}
function cleanup(contentWindow) {
contentWindow.hideSearch(null);
let onTabViewHidden = function() {
window.removeEventListener("tabviewhidden", onTabViewHidden, false);
ok(!TabView.isVisible(), "Tab View is hidden");

View File

@ -1,2 +1,3 @@
tabview.groupItem.newTabButton=New tab
tabview.groupItem.defaultName=Name this tab group…
tabview.search.otherWindowTabs=Tabs from other windows

View File

@ -449,3 +449,46 @@ input.defaultName {
.notMainMatch{
opacity: .70;
}
#otherresults{
left: 0px;
bottom: 0px;
width: 100%;
height: 30px;
background-color: rgba(0,0,0,.3);
-moz-box-shadow: 0px -1px 0px rgba(255,255,255,.1), inset 0px 2px 5px rgba(0,0,0,.3);
}
#otherresults .label{
color: #999;
line-height:30px;
margin-left:5px;
margin-right: 5px;
}
.inlineMatch{
background-color: #EBEBEB;
border-radius: 0.4em;
-moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
border: 1px solid rgba(255, 255, 255, 0.5);
padding-left: 3px;
padding-right: 3px;
height: 20px;
margin-right: 5px;
cursor: pointer;
}
.inlineMatch:hover{
opacity: 1.0;
}
.inlineMatch>img{
margin-right: 5px;
position: relative;
top: 2px;
}
.inlineMatch>span{
max-width:200px;
height: 15px;
}

View File

@ -455,3 +455,89 @@ input.defaultName {
.notMainMatch{
opacity: .70;
}
#otherresults{
left: 0px;
bottom: 0px;
width: 100%;
height: 30px;
background-color: rgba(0,0,0,.3);
-moz-box-shadow: 0px -1px 0px rgba(255,255,255,.1), inset 0px 2px 5px rgba(0,0,0,.3);
}
#otherresults .label{
color: #999;
line-height:30px;
margin-left:5px;
margin-right: 5px;
}
.inlineMatch{
background-color: #EBEBEB;
border-radius: 0.4em;
-moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
border: 1px solid rgba(255, 255, 255, 0.5);
padding-left: 3px;
padding-right: 3px;
height: 20px;
margin-right: 5px;
cursor: pointer;
}
.inlineMatch:hover{
opacity: 1.0;
}
.inlineMatch>img{
margin-right: 5px;
position: relative;
top: 2px;
}
.inlineMatch>span{
max-width:200px;
height: 15px;
}
#otherresults{
left: 0px;
bottom: 0px;
width: 100%;
height: 30px;
background-color: rgba(0,0,0,.3);
-moz-box-shadow: 0px -1px 0px rgba(255,255,255,.1), inset 0px 2px 5px rgba(0,0,0,.3);
}
#otherresults .label{
color: #999;
line-height:30px;
margin-left:5px;
margin-right: 5px;
}
.inlineMatch{
background-color: #EBEBEB;
border-radius: 0.4em;
-moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
border: 1px solid rgba(255, 255, 255, 0.5);
padding-left: 3px;
padding-right: 3px;
height: 20px;
margin-right: 5px;
cursor: pointer;
}
.inlineMatch:hover{
opacity: 1.0;
}
.inlineMatch>img{
margin-right: 5px;
position: relative;
top: 2px;
}
.inlineMatch>span{
max-width:200px;
height: 15px;
}