gavin%gavinsharp.com 0505cc0ed4 Bug 339479: Autocomplete controller isn't updated properly in all cases, r=jhughes, r=mconnor
git-svn-id: svn://10.0.0.236/trunk@198712 18797224-902f-48f8-a5cc-f745e15eee43
2006-05-31 01:19:58 +00:00

662 lines
26 KiB
XML

<?xml version="1.0"?>
# -*- Mode: HTML -*-
# ***** 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 mozilla.org browser.
#
# The Initial Developer of the Original Code is
# Joe Hewitt.
# Portions created by the Initial Developer are Copyright (C) 2003
# the Initial Developer. All Rights Reserved.
#
# Contributor(s):
# Pierre Chanial (v2) <p_ch@verizon.net>
# Gavin Sharp (v3) <gavin@gavinsharp.com>
# Ben Goodger <beng@google.com>
# Pamela Greene <pamg.bugs@gmail.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 *****
<!DOCTYPE bindings SYSTEM "chrome://browser/locale/searchbar.dtd">
<bindings id="SearchBindings"
xmlns="http://www.mozilla.org/xbl"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:xbl="http://www.mozilla.org/xbl">
<binding id="searchbar">
<resources>
<stylesheet src="chrome://browser/content/search/searchbarBindings.css"/>
<stylesheet src="chrome://browser/skin/searchbar.css"/>
</resources>
<content>
<xul:stringbundle src="chrome://browser/locale/search.properties"
anonid="searchbar-stringbundle"/>
<xul:box class="searchbar-left" flex="1">
<xul:box class="searchbar-right" flex="1">
<xul:textbox class="searchbar-textbox"
anonid="searchbar-textbox"
type="autocomplete"
flex="1"
autocompletepopup="PopupAutoComplete"
autocompletesearch="search-autocomplete"
autocompletesearchparam="searchbar-history"
completeselectedindex="true"
showcommentcolumn="true"
tabscrolling="true"
xbl:inherits="disableautocomplete,searchengine,src">
<xul:image align="center"
class="searchbar-engine-image"
anonid="searchbar-engine-image"
xbl:inherits="src"/>
<xul:hbox class="search-go-button-container">
# XXX We'd like to add a context="_child" property to this toolbarbutton, but doing
# so causes the context menu to show up for the textbox too, and subsequently crashes
# the app on shutdown. See bug 336662.
#
# XXX We'd also like to specify the menupopup position as after_end, as well as
# probably adjusting its length to match that of the searchbar so the search
# engine icons line up, but that's prevented by inconsistent behavior of the
# popup position within a toolbarbutton. See bug 336868.
<xul:toolbarbutton class="search-go-button"
anonid="search-go-button"
type="menu-button">
<xul:menupopup class="searchbar-popup"
anonid="searchbar-popup"
position="after_start">
<xul:menuseparator/>
<xul:menuitem class="open-engine-manager"
anonid="open-engine-manager"
label="&cmd_engineManager.label;"
accesskey="&cmd_engineManager.accesskey;"/>
</xul:menupopup>
</xul:toolbarbutton>
</xul:hbox>
</xul:textbox>
</xul:box>
</xul:box>
</content>
<implementation implements="nsIObserver">
<constructor><![CDATA[
setTimeout(function (a) { a.init(); }, 0, this);
]]></constructor>
<method name="init">
<body><![CDATA[
this._popup = document.getAnonymousElementByAttribute(this, "anonid",
"searchbar-popup");
// Refresh the display (updating icon, etc)
this.rebuildPopup();
this.updateDisplay();
this._textbox._displayCurrentEngine();
var os =
Components.classes["@mozilla.org/observer-service;1"]
.getService(Components.interfaces.nsIObserverService);
os.addObserver(this, "browser-search-engine-modified", false);
]]></body>
</method>
<destructor><![CDATA[
var os = Components.classes["@mozilla.org/observer-service;1"]
.getService(Components.interfaces.nsIObserverService);
os.removeObserver(this, "browser-search-engine-modified");
// Make sure to break the cycle from _texbox to us. Otherwise we leak
// the world.
this._textbox.mController.input = null;
]]></destructor>
<field name="_stringBundle">document.getAnonymousElementByAttribute(this, "anonid", "searchbar-stringbundle");</field>
<field name="_textbox">document.getAnonymousElementByAttribute(this, "anonid", "searchbar-textbox");</field>
<field name="_popup">null</field>
<field name="_ss">null</field>
<field name="_engines">null</field>
<property name="currentEngine"
onset="this.searchService.currentEngine = val; return val;">
<getter><![CDATA[
var currentEngine = this.searchService.currentEngine;
// Return a dummy engine if there is no currentEngine
return currentEngine || {name:"", uri:null};
]]></getter>
</property>
<!-- textbox is used by sanitize.js to clear the undo history when
clearing form information. -->
<property name="textbox" readonly="true"
onget="return this._textbox;"/>
<property name="searchService" readonly="true">
<getter><![CDATA[
if (!this._ss) {
const nsIBSS = Components.interfaces.nsIBrowserSearchService;
this._ss =
Components.classes["@mozilla.org/browser/search-service;1"]
.getService(nsIBSS);
}
return this._ss;
]]></getter>
</property>
<property name="value"
onget="return this._textbox.value;"
onset="this._textbox.value = val; return val;"/>
<method name="focus">
<body><![CDATA[
this._textbox.focus();
]]></body>
</method>
<method name="select">
<body><![CDATA[
this._textbox.select();
]]></body>
</method>
<method name="observe">
<parameter name="aEngine"/>
<parameter name="aTopic"/>
<parameter name="aVerb"/>
<body><![CDATA[
if (aTopic == "browser-search-engine-modified") {
switch (aVerb) {
case "engine-current":
// The current engine was changed. Rebuilding the menu appears to
// confuse its idea of whether it should be open when it's just
// been clicked, so we force it to close now.
this._popup.hidePopup();
// Fall through.
case "engine-removed":
case "engine-added":
case "engine-changed":
// An engine was removed (or hidden) or added, or an icon was
// changed.
this.rebuildPopup();
this.updateDisplay();
}
}
]]></body>
</method>
<method name="updateDisplay">
<body><![CDATA[
var uri = this.currentEngine.iconURI;
if (uri)
this.setAttribute("src", uri.spec);
else
this.setAttribute("src", "");
// Update current engine display
if (this.hasAttribute("empty"))
this._textbox._displayCurrentEngine();
var name = this.currentEngine.name;
var text = this._stringBundle.getFormattedString("searchtip", [name]);
this.setAttribute("tooltiptext", text);
]]></body>
</method>
<!-- Rebuilds the dynamic portion of the popup menu (i.e., the menu items
for new search engines that can be added to the available list). This
is called each time the popup is shown.
-->
<method name="rebuildPopupDynamic">
<body><![CDATA[
var popup = this._popup;
// Clear any addengine menuitems, including addengine-item entries and
// the addengine-separator. Work backward to avoid invalidating the
// indexes as items are removed.
var items = popup.childNodes;
for (var i = items.length - 1; i >= 0; i--) {
if (items[i].getAttribute("class").indexOf("addengine") != -1)
popup.removeChild(items[i]);
}
var addengines = getBrowser().mCurrentBrowser.engines;
if (addengines && addengines.length > 0) {
const kXULNS =
"http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
// Find the (first) separator in the remaining menu, or the first item
// if no separators are present.
var insertLocation = popup.firstChild;
while (insertLocation.nextSibling &&
insertLocation.localName != "menuseparator") {
insertLocation = insertLocation.nextSibling;
}
if (insertLocation.localName != "menuseparator")
insertLocation = popup.firstChild;
var separator = document.createElementNS(kXULNS, "menuseparator");
separator.setAttribute("class", "addengine-separator");
popup.insertBefore(separator, insertLocation);
// Insert the "add this engine" items.
for (var i = 0; i < addengines.length; i++) {
var menuitem = document.createElement("menuitem");
var engineInfo = addengines[i];
var labelStr =
this._stringBundle.getFormattedString("cmd_addFoundEngine",
[engineInfo.title]);
menuitem = document.createElementNS(kXULNS, "menuitem");
menuitem.setAttribute("class", "menuitem-iconic addengine-item");
menuitem.setAttribute("label", labelStr);
menuitem.setAttribute("tooltiptext", engineInfo.uri);
menuitem.setAttribute("uri", engineInfo.uri);
if (engineInfo.icon)
menuitem.setAttribute("src", engineInfo.icon);
menuitem.setAttribute("title", engineInfo.title);
popup.insertBefore(menuitem, insertLocation);
}
}
]]></body>
</method>
<!-- Rebuilds the list of visible search engines in the menu. Does not remove
or update any dynamic entries (i.e., "Add this engine" items) nor the
Manage Engines item. This is called by the observer when the list of
visible engines, or the currently selected engine, has changed.
-->
<method name="rebuildPopup">
<body><![CDATA[
var popup = this._popup;
// Clear the popup, down to the first separator
while (popup.firstChild && popup.firstChild.localName != "menuseparator")
popup.removeChild(popup.firstChild);
const kXULNS =
"http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
// Prepend visible engines
this._engines = this.searchService.getVisibleEngines({ });
for (var i = this._engines.length - 1; i >= 0; --i) {
var menuitem = document.createElementNS(kXULNS, "menuitem");
var name = this._engines[i].name;
menuitem.setAttribute("label", name);
menuitem.setAttribute("id", name);
menuitem.setAttribute("class", "menuitem-iconic searchbar-engine-menuitem");
// Since this menu is rebuilt by the observer method whenever a new
// engine is selected, the "selected" attribute does not need to be
// explicitly cleared anywhere.
if (this._engines[i] == this.currentEngine)
menuitem.setAttribute("selected", "true");
var tooltip = this._stringBundle.getFormattedString("searchtip", [name]);
menuitem.setAttribute("tooltiptext", tooltip);
if (this._engines[i].iconURI)
menuitem.setAttribute("src", this._engines[i].iconURI.spec);
popup.insertBefore(menuitem, popup.firstChild);
menuitem.engine = this._engines[i];
}
]]></body>
</method>
<method name="openManager">
<parameter name="aEvent"/>
<body><![CDATA[
var wm =
Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator);
var window = wm.getMostRecentWindow("Browser:SearchManager");
if (window)
window.focus()
else {
setTimeout(function () {
openDialog("chrome://browser/content/search/engineManager.xul",
"_blank", "chrome,dialog,modal,centerscreen");
}, 0);
}
]]></body>
</method>
<!-- Because this may be called from a command handler, where event.target is
not the correct target for the command, it takes a target rather than an
event as a parameter.
-->
<method name="onEnginePopupCommand">
<parameter name="aTarget"/>
<body><![CDATA[
if (aTarget.getAttribute("class").indexOf("addengine-item") != -1) {
var searchService = Components
.classes["@mozilla.org/browser/search-service;1"]
.getService(Components.interfaces.nsIBrowserSearchService);
if (searchService) {
// We only detect OpenSearch files
var type = Components.interfaces.nsISearchEngine.DATA_XML;
searchService.addEngine(aTarget.getAttribute("uri"), type,
aTarget.getAttribute("src"));
}
}
else if (aTarget.engine) {
this.currentEngine = aTarget.engine;
this.focus();
this.select();
}
]]></body>
</method>
<method name="selectEngine">
<parameter name="aEvent"/>
<parameter name="isNextEngine"/>
<body><![CDATA[
// Find the new index
var newIndex = this._engines.indexOf(this.currentEngine);
newIndex += (isNextEngine) ? 1 : -1;
if (newIndex >= 0 && newIndex < this._engines.length)
this.currentEngine = this._engines[newIndex];
aEvent.preventDefault();
aEvent.stopPropagation();
]]></body>
</method>
<method name="handleSearchCommand">
<parameter name="aEvent"/>
<body><![CDATA[
var textBox = this._textbox;
var textValue = textBox.value;
// Ignore greyed-out hint text in "empty" searchboxes.
if (this.getAttribute("empty") == "true")
textValue = "";
// Save the current value in the form history
if (textValue && !textBox.hasAttribute("disableautocomplete")) {
textBox._formHistSvc.addEntry(textBox.getAttribute("autocompletesearchparam"),
textValue);
}
var newTab = textBox._prefBranch.getBoolPref("browser.search.openintab");
this.doSearch(textValue, ((aEvent && aEvent.altKey) ^ newTab));
]]></body>
</method>
<method name="doSearch">
<parameter name="aData"/>
<parameter name="aInNewTab"/>
<body><![CDATA[
var postData = { value: null };
var submission = this.currentEngine.getSubmission(aData);
if (submission) {
var url = submission.uri.spec;
postData.value = submission.postData;
}
if (aInNewTab) {
content.focus();
getBrowser().loadOneTab(url, null, null, postData.value, false, false);
if (gURLBar)
gURLBar.value = url;
} else
loadURI(url, null, postData.value, false);
content.focus();
]]></body>
</method>
</implementation>
<handlers>
<handler event="click"><![CDATA[
const target = event.originalTarget;
var anonid = target.getAttribute("anonid");
if (anonid == "searchbar-engine-image") {
this._textbox.focus();
this._textbox.select();
}
]]></handler>
<handler event="command"><![CDATA[
const target = event.originalTarget;
var anonid = target.getAttribute("anonid");
if (anonid == "button")
this.handleSearchCommand(event);
else if (anonid == "open-engine-manager")
this.openManager(event);
else if (target.getAttribute("class").indexOf("addengine-item") != -1 ||
target.engine)
this.onEnginePopupCommand(target);
]]></handler>
<handler event="popupshowing" action="this.rebuildPopupDynamic();"/>
</handlers>
</binding>
<binding id="searchbar-textbox"
extends="chrome://global/content/bindings/autocomplete.xml#autocomplete">
<implementation>
<constructor><![CDATA[
if (this._getParentSearchbar().parentNode.parentNode.localName ==
"toolbarpaletteitem")
return;
setTimeout(function(a) { a.initialize(); }, 0, this);
]]></constructor>
<destructor><![CDATA[
// Because XBL and the customize toolbar code interacts poorly,
// there may not be anything to remove here
try {
this.controllers.removeController(this.searchbarController);
} catch (ex) { }
]]></destructor>
<field name="_stringBundle"/>
<field name="_formHistSvc"/>
<field name="_prefBranch"/>
<method name="initialize">
<body><![CDATA[
this._stringBundle = this._getParentSearchbar()._stringBundle;
this._formHistSvc =
Components.classes["@mozilla.org/satchel/form-history;1"]
.getService(Components.interfaces.nsIFormHistory2);
this._prefBranch =
Components.classes["@mozilla.org/preferences-service;1"]
.getService(Components.interfaces.nsIPrefBranch);
if (this._prefBranch.getBoolPref("browser.urlbar.clickSelectsAll"))
this.setAttribute("clickSelectsAll", true);
var textBox = document.getAnonymousElementByAttribute(this,
"anonid", "textbox-input-box");
var cxmenu = document.getAnonymousElementByAttribute(textBox,
"anonid", "input-box-contextmenu");
var element = document.createElementNS(XUL_NS, "menuseparator");
cxmenu.appendChild(element);
element = document.createElementNS(XUL_NS, "menuitem");
const label = this._stringBundle.getString("cmd_clearHistory");
const akey = this._stringBundle.getString("cmd_clearHistory_accesskey");
element.setAttribute("label", label);
element.setAttribute("accesskey", akey);
element.setAttribute("cmd", "cmd_clearhistory");
cxmenu.appendChild(element);
this.controllers.appendController(this.searchbarController);
]]></body>
</method>
<method name="openSearch">
<body>
<![CDATA[
// Don't open search popup if history popup is open
if (!this.popupOpen) {
document.getAnonymousElementByAttribute(this._getParentSearchbar(),
"anonid", "search-go-button").open = true;
return false;
}
return true;
]]>
</body>
</method>
<!-- Returns the closest parent that is a searchbar element.
If no searchbar element is found, returns null.
-->
<method name="_getParentSearchbar">
<body><![CDATA[
var searchbar = this.parentNode;
while (searchbar) {
if (searchbar.nodeName == "searchbar")
break;
searchbar = searchbar.parentNode;
}
return searchbar;
]]></body>
</method>
<!-- Displays a grayed-out hint string containing the name of the
current search engine in the search text box. (It makes it gray
by setting an empty="true" attribute on the searchbox element.)
-->
<method name="_displayCurrentEngine">
<body><![CDATA[
var searchbar = this._getParentSearchbar();
var newValue = searchbar._stringBundle.getFormattedString(
"searchbox_hint", [searchbar.currentEngine.name]);
// This section is a wee bit hacky; without the timeout, the CSS
// style corresponding to the "empty" attribute doesn't kick in
// until the text has changed, leading to an unpleasant moment
// where the engine name flashes black before turning gray.
searchbar.setAttribute("empty", "true");
var searchTextbox = this;
setTimeout(function() {
if (searchbar.getAttribute("empty") == "true")
searchTextbox.value = newValue;
}, 0);
]]></body>
</method>
<!-- overload |onTextEntered| in autocomplete.xml -->
<method name="onTextEntered">
<parameter name="aEvent"/>
<body><![CDATA[
var evt = aEvent || this.mEnterEvent;
this._getParentSearchbar().handleSearchCommand(evt);
]]></body>
</method>
<!-- nsIController -->
<field name="searchbarController" readonly="true"><![CDATA[({
_self: this,
supportsCommand: function(aCommand) {
return aCommand == "cmd_clearhistory";
},
isCommandEnabled: function(aCommand) {
return this._self._formHistSvc.nameExists(
this._self.getAttribute("autocompletesearchparam"));
},
doCommand: function (aCommand) {
this._self._formHistSvc.removeEntriesForName(
this._self.getAttribute("autocompletesearchparam"));
this._self.value = "";
}
})]]></field>
<!-- DND Observer -->
<field name="searchbarDNDObserver" readonly="true"><![CDATA[({
mOuter: this,
onDrop: function (aEvent, aXferData, aDragSession) {
var data = transferUtils.retrieveURLFromData(aXferData.data,
aXferData.flavour.contentType);
if (data) {
// Remove the search bar's empty attribute, since we're setting
// a value without focusing the textbox. If it's not empty, this
// won't do anything. This can be removed if bug 280635 is fixed.
this.mOuter._getParentSearchbar().removeAttribute("empty");
this.mOuter.value = data;
this.mOuter.onTextEntered(aEvent);
}
},
getSupportedFlavours: function () {
var flavourSet = new FlavourSet();
flavourSet.appendFlavour("text/unicode");
flavourSet.appendFlavour("text/x-moz-url");
flavourSet.appendFlavour("application/x-moz-file", "nsIFile");
return flavourSet;
}
})]]></field>
</implementation>
<handlers>
<handler event="keypress" keycode="vk_up" modifiers="accel"
phase="capturing"
action="this._getParentSearchbar().selectEngine(event, false);"/>
<handler event="keypress" keycode="vk_down" modifiers="accel"
phase="capturing"
action="this._getParentSearchbar().selectEngine(event, true);"/>
<handler event="keypress" keycode="vk_down" modifiers="alt"
phase="capturing"
action="return this.openSearch();"/>
<handler event="keypress" keycode="vk_up" modifiers="alt"
phase="capturing"
action="return this.openSearch();"/>
#ifndef XP_MACOSX
<handler event="keypress" keycode="vk_f4"
phase="capturing"
action="return this.openSearch();"/>
#endif
<handler event="dragdrop" phase="capturing">
nsDragAndDrop.drop(event, this.searchbarDNDObserver);
</handler>
<handler event="focus" phase="capturing"><![CDATA[
var searchbar = this._getParentSearchbar();
if (searchbar.getAttribute("empty") == "true") {
searchbar.removeAttribute("empty");
this.value = "";
}
]]></handler>
<handler event="blur" phase="capturing"><![CDATA[
var searchbar = this._getParentSearchbar();
if (this.value == "")
this._displayCurrentEngine();
]]></handler>
</handlers>
</binding>
</bindings>