Merge fx-team to m-c.

This commit is contained in:
Ryan VanderMeulen 2014-01-31 21:04:30 -05:00
commit d037bb124f
117 changed files with 4688 additions and 2099 deletions

View File

@ -0,0 +1,310 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
const WIDGET_PANEL_LOG_PREFIX = 'WidgetPanel';
XPCOMUtils.defineLazyGetter(this, 'DebuggerClient', function() {
return Cu.import('resource://gre/modules/devtools/dbg-client.jsm', {}).DebuggerClient;
});
XPCOMUtils.defineLazyGetter(this, 'WebConsoleUtils', function() {
let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
return devtools.require("devtools/toolkit/webconsole/utils").Utils;
});
/**
* The Widget Panel is an on-device developer tool that displays widgets,
* showing visual debug information about apps. Each widget corresponds to a
* metric as tracked by a metric watcher (e.g. consoleWatcher).
*/
let devtoolsWidgetPanel = {
_apps: new Map(),
_urls: new Map(),
_client: null,
_webappsActor: null,
_watchers: [],
/**
* This method registers a metric watcher that will watch one or more metrics
* of apps that are being tracked. A watcher must implement the trackApp(app)
* and untrackApp(app) methods, add entries to the app.metrics map, keep them
* up-to-date, and call app.display() when values were changed.
*/
registerWatcher: function dwp_registerWatcher(watcher) {
this._watchers.unshift(watcher);
},
init: function dwp_init() {
if (this._client)
return;
if (!DebuggerServer.initialized) {
RemoteDebugger.start();
}
this._client = new DebuggerClient(DebuggerServer.connectPipe());
this._client.connect((type, traits) => {
// FIXME(Bug 962577) see below.
this._client.listTabs((res) => {
this._webappsActor = res.webappsActor;
for (let w of this._watchers) {
if (w.init) {
w.init(this._client);
}
}
Services.obs.addObserver(this, 'remote-browser-frame-pending', false);
Services.obs.addObserver(this, 'in-process-browser-or-app-frame-shown', false);
Services.obs.addObserver(this, 'message-manager-disconnect', false);
});
});
},
uninit: function dwp_uninit() {
if (!this._client)
return;
for (let manifest of this._apps.keys()) {
this.untrackApp(manifest);
}
Services.obs.removeObserver(this, 'remote-browser-frame-pending');
Services.obs.removeObserver(this, 'in-process-browser-or-app-frame-shown');
Services.obs.removeObserver(this, 'message-manager-disconnect');
this._client.close();
delete this._client;
},
/**
* This method will ask all registered watchers to track and update metrics
* on an app.
*/
trackApp: function dwp_trackApp(manifestURL) {
if (this._apps.has(manifestURL))
return;
// FIXME(Bug 962577) Factor getAppActor and watchApps out of webappsActor.
this._client.request({
to: this._webappsActor,
type: 'getAppActor',
manifestURL: manifestURL
}, (res) => {
if (res.error) {
return;
}
let app = new App(manifestURL, res.actor);
this._apps.set(manifestURL, app);
for (let w of this._watchers) {
w.trackApp(app);
}
});
},
untrackApp: function dwp_untrackApp(manifestURL) {
let app = this._apps.get(manifestURL);
if (app) {
for (let w of this._watchers) {
w.untrackApp(app);
}
// Delete the metrics and call display() to clean up the front-end.
delete app.metrics;
app.display();
this._apps.delete(manifestURL);
}
},
observe: function dwp_observe(subject, topic, data) {
if (!this._client)
return;
let manifestURL;
switch(topic) {
// listen for frame creation in OOP (device) as well as in parent process (b2g desktop)
case 'remote-browser-frame-pending':
case 'in-process-browser-or-app-frame-shown':
let frameLoader = subject;
// get a ref to the app <iframe>
frameLoader.QueryInterface(Ci.nsIFrameLoader);
manifestURL = frameLoader.ownerElement.appManifestURL;
if (!manifestURL) // Ignore all frames but apps
return;
this.trackApp(manifestURL);
this._urls.set(frameLoader.messageManager, manifestURL);
break;
// Every time an iframe is destroyed, its message manager also is
case 'message-manager-disconnect':
let mm = subject;
manifestURL = this._urls.get(mm);
if (!manifestURL)
return;
this.untrackApp(manifestURL);
this._urls.delete(mm);
break;
}
},
log: function dwp_log(message) {
dump(WIDGET_PANEL_LOG_PREFIX + ': ' + message + '\n');
}
};
/**
* An App object represents all there is to know about a Firefox OS app that is
* being tracked, e.g. its manifest information, current values of watched
* metrics, and how to update these values on the front-end.
*/
function App(manifest, actor) {
this.manifest = manifest;
this.actor = actor;
this.metrics = new Map();
}
App.prototype = {
display: function app_display() {
let data = {manifestURL: this.manifest, metrics: []};
let metrics = this.metrics;
if (metrics && metrics.size > 0) {
for (let name of metrics.keys()) {
data.metrics.push({name: name, value: metrics.get(name)});
}
}
shell.sendCustomEvent('widget-panel-update', data);
// FIXME(after bug 963239 lands) return event.isDefaultPrevented();
return false;
}
};
/**
* The Console Watcher tracks the following metrics in apps: errors, warnings,
* and reflows.
*/
let consoleWatcher = {
_apps: new Map(),
_client: null,
init: function cw_init(client) {
this._client = client;
this.consoleListener = this.consoleListener.bind(this);
client.addListener('logMessage', this.consoleListener);
client.addListener('pageError', this.consoleListener);
client.addListener('consoleAPICall', this.consoleListener);
client.addListener('reflowActivity', this.consoleListener);
},
trackApp: function cw_trackApp(app) {
app.metrics.set('reflows', 0);
app.metrics.set('warnings', 0);
app.metrics.set('errors', 0);
this._client.request({
to: app.actor.consoleActor,
type: 'startListeners',
listeners: ['LogMessage', 'PageError', 'ConsoleAPI', 'ReflowActivity']
}, (res) => {
this._apps.set(app.actor.consoleActor, app);
});
},
untrackApp: function cw_untrackApp(app) {
this._client.request({
to: app.actor.consoleActor,
type: 'stopListeners',
listeners: ['LogMessage', 'PageError', 'ConsoleAPI', 'ReflowActivity']
}, (res) => { });
this._apps.delete(app.actor.consoleActor);
},
bump: function cw_bump(app, metric) {
let metrics = app.metrics;
metrics.set(metric, metrics.get(metric) + 1);
},
consoleListener: function cw_consoleListener(type, packet) {
let app = this._apps.get(packet.from);
let output = '';
switch (packet.type) {
case 'pageError':
let pageError = packet.pageError;
if (pageError.warning || pageError.strict) {
this.bump(app, 'warnings');
output = 'warning (';
} else {
this.bump(app, 'errors');
output += 'error (';
}
let {errorMessage, sourceName, category, lineNumber, columnNumber} = pageError;
output += category + '): "' + (errorMessage.initial || errorMessage) +
'" in ' + sourceName + ':' + lineNumber + ':' + columnNumber;
break;
case 'consoleAPICall':
switch (packet.message.level) {
case 'error':
this.bump(app, 'errors');
output = 'error (console)';
break;
case 'warn':
this.bump(app, 'warnings');
output = 'warning (console)';
break;
default:
return;
}
break;
case 'reflowActivity':
this.bump(app, 'reflows');
let {start, end, sourceURL} = packet;
let duration = Math.round((end - start) * 100) / 100;
output = 'reflow: ' + duration + 'ms';
if (sourceURL) {
output += ' ' + this.formatSourceURL(packet);
}
break;
}
if (!app.display()) {
// If the information was not displayed, log it.
devtoolsWidgetPanel.log(output);
}
},
formatSourceURL: function cw_formatSourceURL(packet) {
// Abbreviate source URL
let source = WebConsoleUtils.abbreviateSourceURL(packet.sourceURL);
// Add function name and line number
let {functionName, sourceLine} = packet;
source = 'in ' + (functionName || '<anonymousFunction>') +
', ' + source + ':' + sourceLine;
return source;
}
};
devtoolsWidgetPanel.registerWatcher(consoleWatcher);

View File

@ -217,6 +217,24 @@ Components.utils.import('resource://gre/modules/ctypes.jsm');
window.navigator.mozSettings.createLock().set(setting);
})();
// =================== DevTools ====================
let devtoolsWidgetPanel;
SettingsListener.observe('devtools.overlay', false, (value) => {
if (value) {
if (!devtoolsWidgetPanel) {
let scope = {};
Services.scriptloader.loadSubScript('chrome://browser/content/devtools.js', scope);
devtoolsWidgetPanel = scope.devtoolsWidgetPanel;
}
devtoolsWidgetPanel.init();
} else {
if (devtoolsWidgetPanel) {
devtoolsWidgetPanel.uninit();
}
}
});
// =================== Debugger / ADB ====================
#ifdef MOZ_WIDGET_GONK

View File

@ -13,6 +13,7 @@ chrome.jar:
* content/settings.js (content/settings.js)
* content/shell.html (content/shell.html)
* content/shell.js (content/shell.js)
content/devtools.js (content/devtools.js)
#ifndef ANDROID
content/desktop.js (content/desktop.js)
content/screen.js (content/screen.js)

View File

@ -1118,7 +1118,7 @@ pref("devtools.debugger.pause-on-exceptions", false);
pref("devtools.debugger.ignore-caught-exceptions", true);
pref("devtools.debugger.source-maps-enabled", true);
pref("devtools.debugger.pretty-print-enabled", true);
pref("devtools.debugger.auto-pretty-print", true);
pref("devtools.debugger.auto-pretty-print", false);
pref("devtools.debugger.tracer", false);
// The default Debugger UI settings

View File

@ -896,13 +896,15 @@ chatbox:-moz-full-screen-ancestor > .chat-titlebar {
}
/* Customize mode */
#tab-view-deck {
transition-property: padding;
#navigator-toolbox > toolbar:not(#TabsToolbar),
#content-deck {
transition-property: margin-left, margin-right;
transition-duration: 150ms;
transition-timing-function: ease-out;
}
#tab-view-deck[fastcustomizeanimation] {
#tab-view-deck[fastcustomizeanimation] #navigator-toolbox > toolbar:not(#TabsToolbar),
#tab-view-deck[fastcustomizeanimation] #content-deck {
transition-duration: 1ms;
transition-timing-function: linear;
}

View File

@ -4368,8 +4368,6 @@ var TabsInTitlebar = {
this._menuObserver = new MutationObserver(this._onMenuMutate);
this._menuObserver.observe(menu, {attributes: true});
gNavToolbox.addEventListener("customization-transitionend", this);
this.onAreaReset = function(aArea) {
if (aArea == CustomizableUI.AREA_TABSTRIP || aArea == CustomizableUI.AREA_MENUBAR)
this._update(true);
@ -4416,12 +4414,6 @@ var TabsInTitlebar = {
this._readPref();
},
handleEvent: function(ev) {
if (ev.type == "customization-transitionend") {
this._update(true);
}
},
_onMenuMutate: function (aMutations) {
for (let mutation of aMutations) {
if (mutation.attributeName == "inactive" ||

View File

@ -223,7 +223,8 @@
noautofocus="true"
noautohide="true"
flip="none"
consumeoutsideclicks="false">
consumeoutsideclicks="false"
mousethrough="always">
<box id="UITourHighlight"></box>
</panel>

View File

@ -7,7 +7,7 @@
let SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService;
const URI_EXTENSION_BLOCKLIST_DIALOG = "chrome://mozapps/content/extensions/blocklist.xul";
let blocklistURL = "http://example.org/browser/browser/base/content/test/social/blocklist.xml";
let blocklistURL = "http://example.com/browser/browser/base/content/test/social/blocklist.xml";
let manifest = { // normal provider
name: "provider ok",
@ -26,6 +26,11 @@ let manifest_bad = { // normal provider
function test() {
waitForExplicitFinish();
// turn on logging for nsBlocklistService.js
Services.prefs.setBoolPref("extensions.logging.enabled", true);
registerCleanupFunction(function () {
Services.prefs.clearUserPref("extensions.logging.enabled");
});
runSocialTests(tests, undefined, undefined, function () {
resetBlocklist(finish); //restore to original pref
@ -45,7 +50,7 @@ var tests = {
});
},
testAddingNonBlockedProvider: function(next) {
function finish(isgood) {
function finishTest(isgood) {
ok(isgood, "adding non-blocked provider ok");
Services.prefs.clearUserPref("social.manifest.good");
resetBlocklist(next);
@ -57,21 +62,21 @@ var tests = {
try {
SocialService.removeProvider(provider.origin, function() {
ok(true, "added and removed provider");
finish(true);
finishTest(true);
});
} catch(e) {
ok(false, "SocialService.removeProvider threw exception: " + e);
finish(false);
finishTest(false);
}
});
} catch(e) {
ok(false, "SocialService.addProvider threw exception: " + e);
finish(false);
finishTest(false);
}
});
},
testAddingBlockedProvider: function(next) {
function finish(good) {
function finishTest(good) {
ok(good, "Unable to add blocklisted provider");
Services.prefs.clearUserPref("social.manifest.blocked");
resetBlocklist(next);
@ -80,17 +85,19 @@ var tests = {
setAndUpdateBlocklist(blocklistURL, function() {
try {
SocialService.addProvider(manifest_bad, function(provider) {
ok(false, "SocialService.addProvider should throw blocklist exception");
finish(false);
SocialService.removeProvider(provider.origin, function() {
ok(false, "SocialService.addProvider should throw blocklist exception");
finishTest(false);
});
});
} catch(e) {
ok(true, "SocialService.addProvider should throw blocklist exception: " + e);
finish(true);
finishTest(true);
}
});
},
testInstallingBlockedProvider: function(next) {
function finish(good) {
function finishTest(good) {
ok(good, "Unable to add blocklisted provider");
Services.prefs.clearUserPref("social.whitelist");
resetBlocklist(next);
@ -108,24 +115,16 @@ var tests = {
// provider
Social.installProvider(doc, manifest_bad, function(addonManifest) {
gBrowser.removeTab(tab);
finish(false);
finishTest(false);
});
} catch(e) {
gBrowser.removeTab(tab);
finish(true);
finishTest(true);
}
});
});
},
testBlockingExistingProvider: function(next) {
let windowWasClosed = false;
function finish() {
waitForCondition(function() windowWasClosed, function() {
Services.wm.removeListener(listener);
next();
}, "blocklist dialog was closed");
}
let listener = {
_window: null,
onOpenWindow: function(aXULWindow) {
@ -140,12 +139,18 @@ var tests = {
domwindow.addEventListener("unload", function _unload() {
domwindow.removeEventListener("unload", _unload, false);
info("blocklist window was closed");
windowWasClosed = true;
Services.wm.removeListener(listener);
next();
}, false);
is(domwindow.document.location.href, URI_EXTENSION_BLOCKLIST_DIALOG, "dialog opened and focused");
domwindow.close();
// wait until after load to cancel so the dialog has initalized. we
// don't want to accept here since that restarts the browser.
executeSoon(() => {
let cancelButton = domwindow.document.documentElement.getButton("cancel");
info("***** hit the cancel button\n");
cancelButton.doCommand();
});
}, false);
},
onCloseWindow: function(aXULWindow) { },
@ -167,7 +172,7 @@ var tests = {
SocialService.getProvider(provider.origin, function(p) {
ok(p == null, "blocklisted provider disabled");
Services.prefs.clearUserPref("social.manifest.blocked");
resetBlocklist(finish);
resetBlocklist();
});
});
// no callback - the act of updating should cause the listener above
@ -176,7 +181,7 @@ var tests = {
});
} catch(e) {
ok(false, "unable to add provider " + e);
finish();
next();
}
}
}

View File

@ -26,9 +26,11 @@
exitLabel="&appMenuCustomizeExit.label;"
tooltiptext="&appMenuCustomize.tooltip;"
exitTooltiptext="&appMenuCustomizeExit.tooltip;"
closemenu="none"
oncommand="gCustomizeMode.toggle();"/>
<toolbarseparator/>
<toolbarbutton id="PanelUI-help" label="&helpMenu.label;"
closemenu="none"
tooltiptext="&appMenuHelp.tooltip;"
oncommand="PanelUI.showHelpView(this.parentNode);"/>
<toolbarseparator/>

View File

@ -61,7 +61,6 @@ const PanelUI = {
}
this.helpView.addEventListener("ViewShowing", this._onHelpViewShow, false);
this.helpView.addEventListener("ViewHiding", this._onHelpViewHide, false);
this._eventListenersAdded = true;
},
@ -74,7 +73,6 @@ const PanelUI = {
this.panel.removeEventListener(event, this);
}
this.helpView.removeEventListener("ViewShowing", this._onHelpViewShow);
this.helpView.removeEventListener("ViewHiding", this._onHelpViewHide);
this.menuButton.removeEventListener("mousedown", this);
this.menuButton.removeEventListener("keypress", this);
},
@ -167,9 +165,6 @@ const PanelUI = {
handleEvent: function(aEvent) {
switch (aEvent.type) {
case "command":
this.onCommandHandler(aEvent);
break;
case "popupshowing":
// Fall through
case "popupshown":
@ -419,12 +414,6 @@ const PanelUI = {
fragment.appendChild(button);
}
items.appendChild(fragment);
this.addEventListener("command", PanelUI);
},
_onHelpViewHide: function(aEvent) {
this.removeEventListener("command", PanelUI);
},
_updateQuitTooltip: function() {

View File

@ -635,33 +635,33 @@ let CustomizableUIInternal = {
return [null, null];
},
registerMenuPanel: function(aPanel) {
registerMenuPanel: function(aPanelContents) {
if (gBuildAreas.has(CustomizableUI.AREA_PANEL) &&
gBuildAreas.get(CustomizableUI.AREA_PANEL).has(aPanel)) {
gBuildAreas.get(CustomizableUI.AREA_PANEL).has(aPanelContents)) {
return;
}
let document = aPanel.ownerDocument;
let document = aPanelContents.ownerDocument;
aPanel.toolbox = document.getElementById("navigator-toolbox");
aPanel.customizationTarget = aPanel;
aPanelContents.toolbox = document.getElementById("navigator-toolbox");
aPanelContents.customizationTarget = aPanelContents;
this.addPanelCloseListeners(aPanel);
this.addPanelCloseListeners(this._getPanelForNode(aPanelContents));
let placements = gPlacements.get(CustomizableUI.AREA_PANEL);
this.buildArea(CustomizableUI.AREA_PANEL, placements, aPanel);
for (let child of aPanel.children) {
this.buildArea(CustomizableUI.AREA_PANEL, placements, aPanelContents);
for (let child of aPanelContents.children) {
if (child.localName != "toolbarbutton") {
if (child.localName == "toolbaritem") {
this.ensureButtonContextMenu(child, aPanel);
this.ensureButtonContextMenu(child, aPanelContents);
}
continue;
}
this.ensureButtonContextMenu(child, aPanel);
this.ensureButtonContextMenu(child, aPanelContents);
child.setAttribute("wrap", "true");
}
this.registerBuildArea(CustomizableUI.AREA_PANEL, aPanel);
this.registerBuildArea(CustomizableUI.AREA_PANEL, aPanelContents);
},
onWidgetAdded: function(aWidgetId, aArea, aPosition) {
@ -1314,11 +1314,20 @@ let CustomizableUIInternal = {
}
}
if (aEvent.target.getAttribute("closemenu") == "none" ||
aEvent.target.getAttribute("widget-type") == "view") {
if (aEvent.originalTarget.getAttribute("closemenu") == "none" ||
aEvent.originalTarget.getAttribute("widget-type") == "view") {
return;
}
if (aEvent.originalTarget.getAttribute("closemenu") == "single") {
let panel = this._getPanelForNode(aEvent.originalTarget);
let multiview = panel.querySelector("panelmultiview");
if (multiview.showingSubView) {
multiview.showMainView();
return;
}
}
// If we get here, we can actually hide the popup:
this.hidePanelForNode(aEvent.target);
},

View File

@ -191,11 +191,9 @@ const CustomizableWidgets = [{
windowsFragment.children[elementCount].classList.add("subviewbutton");
}
recentlyClosedWindows.appendChild(windowsFragment);
aEvent.target.addEventListener("command", win.PanelUI);
},
onViewHiding: function(aEvent) {
LOG("History view is being hidden!");
aEvent.target.removeEventListener("command", win.PanelUI);
}
}, {
id: "privatebrowsing-button",
@ -293,7 +291,6 @@ const CustomizableWidgets = [{
}
items.appendChild(fragment);
aEvent.target.addEventListener("command", win.PanelUI);
},
onViewHiding: function(aEvent) {
let doc = aEvent.target.ownerDocument;
@ -309,7 +306,6 @@ const CustomizableWidgets = [{
}
parent.appendChild(items);
aEvent.target.removeEventListener("command", win.PanelUI);
}
}, {
id: "add-ons-button",

View File

@ -174,6 +174,13 @@ CustomizeMode.prototype = {
window.PanelUI.menuButton.open = true;
window.PanelUI.beginBatchUpdate();
// The menu panel is lazy, and registers itself when the popup shows. We
// need to force the menu panel to register itself, or else customization
// is really not going to work. We pass "true" to ensureRegistered to
// indicate that we're handling calling startBatchUpdate and
// endBatchUpdate.
yield window.PanelUI.ensureReady(true);
// Hide the palette before starting the transition for increased perf.
this.visiblePalette.hidden = true;
@ -200,13 +207,6 @@ CustomizeMode.prototype = {
// Let everybody in this window know that we're about to customize.
this.dispatchToolboxEvent("customizationstarting");
// The menu panel is lazy, and registers itself when the popup shows. We
// need to force the menu panel to register itself, or else customization
// is really not going to work. We pass "true" to ensureRegistered to
// indicate that we're handling calling startBatchUpdate and
// endBatchUpdate.
yield window.PanelUI.ensureReady(true);
this._mainViewContext = mainView.getAttribute("context");
if (this._mainViewContext) {
mainView.removeAttribute("context");
@ -427,26 +427,30 @@ CustomizeMode.prototype = {
*/
_doTransition: function(aEntering) {
let deferred = Promise.defer();
let deck = this.document.getElementById("tab-view-deck");
let deck = this.document.getElementById("content-deck");
let customizeTransitionEnd = function(aEvent) {
if (aEvent != "timedout" &&
(aEvent.originalTarget != deck || aEvent.propertyName != "padding-bottom")) {
(aEvent.originalTarget != deck || aEvent.propertyName != "margin-left")) {
return;
}
this.window.clearTimeout(catchAllTimeout);
deck.removeEventListener("transitionend", customizeTransitionEnd);
// Bug 962677: We let the event loop breathe for before we do the final
// stage of the transition to improve perceived performance.
this.window.setTimeout(function () {
deck.removeEventListener("transitionend", customizeTransitionEnd);
if (!aEntering) {
this.document.documentElement.removeAttribute("customize-exiting");
this.document.documentElement.removeAttribute("customizing");
} else {
this.document.documentElement.setAttribute("customize-entered", true);
this.document.documentElement.removeAttribute("customize-entering");
}
this.dispatchToolboxEvent("customization-transitionend", aEntering);
if (!aEntering) {
this.document.documentElement.removeAttribute("customize-exiting");
this.document.documentElement.removeAttribute("customizing");
} else {
this.document.documentElement.setAttribute("customize-entered", true);
this.document.documentElement.removeAttribute("customize-entering");
}
this.dispatchToolboxEvent("customization-transitionend", aEntering);
deferred.resolve();
deferred.resolve();
}.bind(this), 0);
}.bind(this);
deck.addEventListener("transitionend", customizeTransitionEnd);

View File

@ -10,12 +10,17 @@ add_task(function() {
let menuItems = document.getElementById("menubar-items");
let navbar = document.getElementById("nav-bar");
let menubar = document.getElementById("toolbar-menubar");
// Force the menu to be shown.
const kAutohide = menubar.getAttribute("autohide");
menubar.setAttribute("autohide", "false");
simulateItemDrag(menuItems, navbar.customizationTarget);
is(getAreaWidgetIds("nav-bar").indexOf("menubar-items"), -1, "Menu bar shouldn't be in the navbar.");
ok(!navbar.querySelector("#menubar-items"), "Shouldn't find menubar items in the navbar.");
ok(menubar.querySelector("#menubar-items"), "Should find menubar items in the menubar.");
isnot(getAreaWidgetIds("toolbar-menubar").indexOf("menubar-items"), -1,
"Menubar items shouldn't be missing from the navbar.");
menubar.setAttribute("autohide", kAutohide);
yield endCustomizing();
});

View File

@ -7,17 +7,12 @@
<!DOCTYPE html [
<!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
%htmlDTD;
<!ENTITY % netErrorDTD SYSTEM "chrome://global/locale/netError.dtd">
%netErrorDTD;
<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
%globalDTD;
<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
%brandDTD;
<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
%browserDTD;
#ifdef XP_MACOSX
<!ENTITY basePBMenu.label "&fileMenu.label;">
#else
<!ENTITY basePBMenu.label "<span class='appMenuButton'>&brandShortName;</span><span class='fileMenu'>&fileMenu.label;</span>">
#endif
<!ENTITY % privatebrowsingpageDTD SYSTEM "chrome://browser/locale/aboutPrivateBrowsing.dtd">
%privatebrowsingpageDTD;
]>
@ -31,12 +26,6 @@
body.private .showNormal {
display: none;
}
body.appMenuButtonVisible .fileMenu {
display: none;
}
body.appMenuButtonInvisible .appMenuButton {
display: none;
}
]]></style>
<script type="application/javascript;version=1.7"><![CDATA[
const Cc = Components.classes;
@ -82,13 +71,6 @@
let moreInfoLink = document.getElementById("moreInfoLink");
if (moreInfoLink)
moreInfoLink.setAttribute("href", moreInfoURL + "private-browsing");
// Show the correct menu structure based on whether the App Menu button is
// shown or not.
var menuBar = mainWindow.document.getElementById("toolbar-menubar");
var appMenuButtonIsVisible = menuBar.getAttribute("autohide") == "true";
document.body.classList.add(appMenuButtonIsVisible ? "appMenuButtonVisible" :
"appMenuButtonInvisible");
}, false);
function openPrivateWindow() {
@ -134,7 +116,7 @@
<!-- Footer -->
<div id="footerDesc">
<p id="footerText" class="showPrivate">&privatebrowsingpage.howToStop3;</p>
<p id="footerTextNormal" class="showNormal">&privatebrowsingpage.howToStart3;</p>
<p id="footerTextNormal" class="showNormal">&privatebrowsingpage.howToStart4;</p>
</div>
<!-- More Info -->

View File

@ -233,5 +233,5 @@ let SessionWorker = (function () {
AsyncShutdown.profileBeforeChange.addBlocker(
"SessionFile: Finish writing the latest sessionstore.js",
function() {
return SessionFile._latestWrite;
return SessionFileInternal._latestWrite;
});

View File

@ -1872,7 +1872,14 @@ VariableBubbleView.prototype = {
messages: [textContent],
messagesClass: className,
containerClass: "plain"
});
}, [{
label: L10N.getStr('addWatchExpressionButton'),
className: "dbg-expression-button",
command: () => {
DebuggerView.VariableBubble.hideContents();
DebuggerView.WatchExpressions.addExpression(evalPrefix, true);
}
}]);
} else {
this._tooltip.setVariableContent(objectActor, {
searchPlaceholder: L10N.getStr("emptyPropertiesFilterText"),
@ -1894,7 +1901,14 @@ VariableBubbleView.prototype = {
window.emit(EVENTS.FETCHED_BUBBLE_PROPERTIES);
}
}
});
}, [{
label: L10N.getStr("addWatchExpressionButton"),
className: "dbg-expression-button",
command: () => {
DebuggerView.VariableBubble.hideContents();
DebuggerView.WatchExpressions.addExpression(evalPrefix, true);
}
}]);
}
this._tooltip.show(this._markedText.anchor);
@ -2031,8 +2045,11 @@ WatchExpressionsView.prototype = Heritage.extend(WidgetMethods, {
*
* @param string aExpression [optional]
* An optional initial watch expression text.
* @param boolean aSkipUserInput [optional]
* Pass true to avoid waiting for additional user input
* on the watch expression.
*/
addExpression: function(aExpression = "") {
addExpression: function(aExpression = "", aSkipUserInput = false) {
// Watch expressions are UI elements which benefit from visible panes.
DebuggerView.showInstrumentsPane();
@ -2049,10 +2066,18 @@ WatchExpressionsView.prototype = Heritage.extend(WidgetMethods, {
}
});
// Automatically focus the new watch expression input.
expressionItem.attachment.view.inputNode.select();
expressionItem.attachment.view.inputNode.focus();
DebuggerView.Variables.parentNode.scrollTop = 0;
// Automatically focus the new watch expression input
// if additional user input is desired.
if (!aSkipUserInput) {
expressionItem.attachment.view.inputNode.select();
expressionItem.attachment.view.inputNode.focus();
DebuggerView.Variables.parentNode.scrollTop = 0;
}
// Otherwise, add and evaluate the new watch expression immediately.
else {
this.toggleContents(false);
this._onBlur({ target: expressionItem.attachment.view.inputNode });
}
},
/**

View File

@ -306,7 +306,8 @@
flex="1">
<toolbar id="debugger-toolbar"
class="devtools-toolbar">
<hbox id="debugger-controls">
<hbox id="debugger-controls"
class="devtools-toolbarbutton-group">
<toolbarbutton id="resume"
class="devtools-toolbarbutton"
tabindex="0"/>
@ -354,7 +355,8 @@
<tabpanel id="sources-tabpanel">
<vbox id="sources" flex="1"/>
<toolbar id="sources-toolbar" class="devtools-toolbar">
<hbox id="sources-controls">
<hbox id="sources-controls"
class="devtools-toolbarbutton-group">
<toolbarbutton id="black-box"
class="devtools-toolbarbutton"
tooltiptext="&debuggerUI.sources.blackBoxTooltip;"

View File

@ -64,6 +64,7 @@ support-files =
doc_step-out.html
doc_tracing-01.html
doc_watch-expressions.html
doc_watch-expression-button.html
doc_with-frame.html
head.js
sjs_random-javascript.sjs
@ -244,6 +245,8 @@ support-files =
[browser_dbg_variables-view-popup-08.js]
[browser_dbg_variables-view-popup-09.js]
[browser_dbg_variables-view-popup-10.js]
[browser_dbg_variables-view-popup-11.js]
[browser_dbg_variables-view-popup-12.js]
[browser_dbg_variables-view-reexpand-01.js]
[browser_dbg_variables-view-reexpand-02.js]
[browser_dbg_variables-view-webidl.js]

View File

@ -11,6 +11,9 @@ let gEditor, gSources, gPrefs, gOptions, gView;
let gFirstSourceLabel = "code_ugly-5.js";
let gSecondSourceLabel = "code_ugly-6.js";
let gOriginalPref = Services.prefs.getBoolPref("devtools.debugger.auto-pretty-print");
Services.prefs.setBoolPref("devtools.debugger.auto-pretty-print", true);
function test(){
initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
gTab = aTab;
@ -104,4 +107,5 @@ registerCleanupFunction(function() {
gOptions = null;
gPrefs = null;
gView = null;
});
Services.prefs.setBoolPref("devtools.debugger.auto-pretty-print", gOriginalPref);
});

View File

@ -15,6 +15,9 @@ let gEditor, gSources, gPrefs, gOptions, gView;
let gFirstSourceLabel = "code_ugly-6.js";
let gSecondSourceLabel = "code_ugly-7.js";
let gOriginalPref = Services.prefs.getBoolPref("devtools.debugger.auto-pretty-print");
Services.prefs.setBoolPref("devtools.debugger.auto-pretty-print", true);
function test(){
initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
gTab = aTab;
@ -107,4 +110,5 @@ registerCleanupFunction(function() {
gOptions = null;
gPrefs = null;
gView = null;
Services.prefs.setBoolPref("devtools.debugger.auto-pretty-print", gOriginalPref);
});

View File

@ -0,0 +1,78 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests that the watch expression button is added in variable view popup.
*/
const TAB_URL = EXAMPLE_URL + "doc_watch-expression-button.html";
function test() {
Task.spawn(function() {
let [tab, debuggee, panel] = yield initDebugger(TAB_URL);
let win = panel.panelWin;
let events = win.EVENTS;
let watch = win.DebuggerView.WatchExpressions;
let bubble = win.DebuggerView.VariableBubble;
let tooltip = bubble._tooltip.panel;
let label = win.L10N.getStr("addWatchExpressionButton");
let className = "dbg-expression-button";
function testExpressionButton(aLabel, aClassName, aExpression) {
ok(tooltip.querySelector("button"),
"There should be a button available in variable view popup.");
is(tooltip.querySelector("button").label, aLabel,
"The button available is labeled correctly.");
is(tooltip.querySelector("button").className, aClassName,
"The button available is styled correctly.");
tooltip.querySelector("button").click();
ok(!tooltip.querySelector("button"),
"There should be no button available in variable view popup.");
ok(watch.getItemAtIndex(0),
"The expression at index 0 should be available.");
is(watch.getItemAtIndex(0).attachment.initialExpression, aExpression,
"The expression at index 0 is correct.");
}
// Allow this generator function to yield first.
executeSoon(() => debuggee.start());
yield waitForSourceAndCaretAndScopes(panel, ".html", 19);
// Inspect primitive value variable.
yield openVarPopup(panel, { line: 15, ch: 12 });
let popupHiding = once(tooltip, "popuphiding");
let expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS);
testExpressionButton(label, className, "a");
yield promise.all([popupHiding, expressionsEvaluated]);
ok(true, "The new watch expressions were re-evaluated and the panel got hidden (1).");
// Inspect non primitive value variable.
let expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS);
yield openVarPopup(panel, { line: 16, ch: 12 }, true);
yield expressionsEvaluated;
ok(true, "The watch expressions were re-evaluated when a new panel opened (1).");
let popupHiding = once(tooltip, "popuphiding");
let expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS);
testExpressionButton(label, className, "b");
yield promise.all([popupHiding, expressionsEvaluated]);
ok(true, "The new watch expressions were re-evaluated and the panel got hidden (2).");
// Inspect property of an object.
let expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS);
yield openVarPopup(panel, { line: 17, ch: 10 });
yield expressionsEvaluated;
ok(true, "The watch expressions were re-evaluated when a new panel opened (2).");
let popupHiding = once(tooltip, "popuphiding");
let expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS);
testExpressionButton(label, className, "b.a");
yield promise.all([popupHiding, expressionsEvaluated]);
ok(true, "The new watch expressions were re-evaluated and the panel got hidden (3).");
yield resumeDebuggerThenCloseAndFinish(panel);
});
}

View File

@ -0,0 +1,71 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests that the clicking "Watch" button twice, for the same expression, only adds it
* once.
*/
const TAB_URL = EXAMPLE_URL + "doc_watch-expression-button.html";
function test() {
Task.spawn(function() {
let [tab, debuggee, panel] = yield initDebugger(TAB_URL);
let win = panel.panelWin;
let events = win.EVENTS;
let watch = win.DebuggerView.WatchExpressions;
let bubble = win.DebuggerView.VariableBubble;
let tooltip = bubble._tooltip.panel;
function verifyContent(aExpression, aItemCount) {
ok(watch.getItemAtIndex(0),
"The expression at index 0 should be available.");
is(watch.getItemAtIndex(0).attachment.initialExpression, aExpression,
"The expression at index 0 is correct.");
is(watch.itemCount, aItemCount,
"The expression count is correct.");
}
// Allow this generator function to yield first.
executeSoon(() => debuggee.start());
yield waitForSourceAndCaretAndScopes(panel, ".html", 19);
// Inspect primitive value variable.
yield openVarPopup(panel, { line: 15, ch: 12 });
let popupHiding = once(tooltip, "popuphiding");
let expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS);
tooltip.querySelector("button").click();
verifyContent("a", 1);
yield promise.all([popupHiding, expressionsEvaluated]);
ok(true, "The new watch expressions were re-evaluated and the panel got hidden (1).");
// Inspect property of an object.
let expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS);
yield openVarPopup(panel, { line: 17, ch: 10 });
yield expressionsEvaluated;
ok(true, "The watch expressions were re-evaluated when a new panel opened (1).");
let popupHiding = once(tooltip, "popuphiding");
let expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS);
tooltip.querySelector("button").click();
verifyContent("b.a", 2);
yield promise.all([popupHiding, expressionsEvaluated]);
ok(true, "The new watch expressions were re-evaluated and the panel got hidden (2).");
// Re-inspect primitive value variable.
let expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS);
yield openVarPopup(panel, { line: 15, ch: 12 });
yield expressionsEvaluated;
ok(true, "The watch expressions were re-evaluated when a new panel opened (2).");
let popupHiding = once(tooltip, "popuphiding");
let expressionsEvaluated = waitForDebuggerEvents(panel, events.FETCHED_WATCH_EXPRESSIONS);
tooltip.querySelector("button").click();
verifyContent("b.a", 2);
yield promise.all([popupHiding, expressionsEvaluated]);
ok(true, "The new watch expressions were re-evaluated and the panel got hidden (3).");
yield resumeDebuggerThenCloseAndFinish(panel);
});
}

View File

@ -0,0 +1,31 @@
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>Debugger test page</title>
</head>
<body>
<button onclick="start()">Click me!</button>
<script type="text/javascript">
function test() {
var a = 1;
var b = { a: a };
b.a = 2;
debugger;
}
function start() {
var e = eval('test();');
}
var button = document.querySelector("button");
var buttonAsProto = Object.create(button);
</script>
</body>
</html>

View File

@ -24,7 +24,8 @@
class="devtools-responsive-container theme-body">
<vbox class="profiler-sidebar theme-sidebar">
<toolbar class="devtools-toolbar">
<hbox id="profiler-controls">
<hbox id="profiler-controls"
class="devtools-toolbarbutton-group">
<toolbarbutton id="profiler-start"
tooltiptext="&startProfiler.tooltip;"
class="devtools-toolbarbutton"

View File

@ -414,7 +414,14 @@ Tooltip.prototype = {
* @param {boolean} isAlertTooltip [optional]
* Pass true to add an alert image for your tooltip.
*/
setTextContent: function({ messages, messagesClass, containerClass, isAlertTooltip }) {
setTextContent: function(
{
messages,
messagesClass,
containerClass,
isAlertTooltip
},
extraButtons = []) {
messagesClass = messagesClass || "default-tooltip-simple-text-colors";
containerClass = containerClass || "default-tooltip-simple-text-colors";
@ -430,6 +437,14 @@ Tooltip.prototype = {
vbox.appendChild(description);
}
for (let { label, className, command } of extraButtons) {
let button = this.doc.createElement("button");
button.className = className;
button.setAttribute("label", label);
button.addEventListener("command", command);
vbox.appendChild(button);
}
if (isAlertTooltip) {
let hbox = this.doc.createElement("hbox");
hbox.setAttribute("align", "start");
@ -467,31 +482,33 @@ Tooltip.prototype = {
viewOptions = {},
controllerOptions = {},
relayEvents = {},
reuseCachedWidget = true) {
extraButtons = []) {
if (reuseCachedWidget && this._cachedVariablesView) {
var [vbox, widget] = this._cachedVariablesView;
} else {
var vbox = this.doc.createElement("vbox");
vbox.className = "devtools-tooltip-variables-view-box";
vbox.setAttribute("flex", "1");
let vbox = this.doc.createElement("vbox");
vbox.className = "devtools-tooltip-variables-view-box";
vbox.setAttribute("flex", "1");
let innerbox = this.doc.createElement("vbox");
innerbox.className = "devtools-tooltip-variables-view-innerbox";
innerbox.setAttribute("flex", "1");
vbox.appendChild(innerbox);
let innerbox = this.doc.createElement("vbox");
innerbox.className = "devtools-tooltip-variables-view-innerbox";
innerbox.setAttribute("flex", "1");
vbox.appendChild(innerbox);
var widget = new VariablesView(innerbox, viewOptions);
// Analyzing state history isn't useful with transient object inspectors.
widget.commitHierarchy = () => {};
for (let e in relayEvents) widget.on(e, relayEvents[e]);
VariablesViewController.attach(widget, controllerOptions);
this._cachedVariablesView = [vbox, widget];
for (let { label, className, command } of extraButtons) {
let button = this.doc.createElement("button");
button.className = className;
button.setAttribute("label", label);
button.addEventListener("command", command);
vbox.appendChild(button);
}
let widget = new VariablesView(innerbox, viewOptions);
// Analyzing state history isn't useful with transient object inspectors.
widget.commitHierarchy = () => {};
for (let e in relayEvents) widget.on(e, relayEvents[e]);
VariablesViewController.attach(widget, controllerOptions);
// Some of the view options are allowed to change between uses.
widget.searchPlaceholder = viewOptions.searchPlaceholder;
widget.searchEnabled = viewOptions.searchEnabled;

View File

@ -1,4 +1,4 @@
This is the pdf.js project output, https://github.com/mozilla/pdf.js
Current extension version is: 0.8.934
Current extension version is: 0.8.990

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -43,7 +43,8 @@ var NetworkManager = (function NetworkManagerClosure() {
function NetworkManager(url, args) {
this.url = url;
args = args || {};
this.httpHeaders = args.httpHeaders || {};
this.isHttp = /^https?:/i.test(url);
this.httpHeaders = (this.isHttp && args.httpHeaders) || {};
this.withCredentials = args.withCredentials || false;
this.getXhr = args.getXhr ||
function NetworkManager_getXhr() {
@ -101,7 +102,7 @@ var NetworkManager = (function NetworkManagerClosure() {
}
xhr.setRequestHeader(property, value);
}
if ('begin' in args && 'end' in args) {
if (this.isHttp && 'begin' in args && 'end' in args) {
var rangeStr = args.begin + '-' + (args.end - 1);
xhr.setRequestHeader('Range', 'bytes=' + rangeStr);
pendingRequest.expectedStatus = 206;
@ -156,7 +157,7 @@ var NetworkManager = (function NetworkManagerClosure() {
delete this.pendingRequests[xhrId];
// success status == 0 can be on ftp, file and other protocols
if (xhr.status === 0 && /^https?:/i.test(this.url)) {
if (xhr.status === 0 && this.isHttp) {
if (pendingRequest.onError) {
pendingRequest.onError(xhr.status);
}

View File

@ -35,6 +35,7 @@ input,
button,
select {
font: message-box;
outline: none;
}
.hidden {
@ -839,6 +840,7 @@ html[dir="rtl"] .secondaryToolbarButton.print::before {
.secondaryToolbarButton.bookmark {
-moz-box-sizing: border-box;
box-sizing: border-box;
outline: none;
padding-top: 4px;
text-decoration: none;
}
@ -1486,28 +1488,29 @@ html[dir='rtl'] #documentPropertiesContainer .row > * {
color: black;
}
.grab-to-pan-grab * {
.grab-to-pan-grab {
cursor: url("images/grab.cur"), move !important;
cursor: -moz-grab !important;
cursor: grab !important;
}
.grab-to-pan-grabbing,
.grab-to-pan-grabbing * {
.grab-to-pan-grab *:not(input):not(textarea):not(button):not(select):not(:link) {
cursor: inherit !important;
}
.grab-to-pan-grab:active,
.grab-to-pan-grabbing {
cursor: url("images/grabbing.cur"), move !important;
cursor: -moz-grabbing !important;
cursor: grabbing !important;
}
.grab-to-pan-grab input,
.grab-to-pan-grab textarea,
.grab-to-pan-grab button,
.grab-to-pan-grab button *,
.grab-to-pan-grab select,
.grab-to-pan-grab option {
cursor: auto !important;
}
.grab-to-pan-grab a[href],
.grab-to-pan-grab a[href] * {
cursor: pointer !important;
position: fixed;
background: transparent;
display: block;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
z-index: 50000; /* should be higher than anything else in PDF.js! */
}
@page {

View File

@ -17,7 +17,7 @@
/* globals PDFJS, PDFBug, FirefoxCom, Stats, Cache, PDFFindBar, CustomStyle,
PDFFindController, ProgressBar, TextLayerBuilder, DownloadManager,
getFileName, scrollIntoView, getPDFFileNameFromURL, PDFHistory,
Preferences, ViewHistory, PageView, ThumbnailView,
Preferences, ViewHistory, PageView, ThumbnailView, URL,
noContextMenuHandler, SecondaryToolbar, PasswordPrompt,
PresentationMode, HandTool, Promise, DocumentProperties */
@ -771,8 +771,6 @@ var PDFFindController = {
resumePageIdx: null,
resumeCallback: null,
state: null,
dirtyMatch: false,
@ -785,7 +783,7 @@ var PDFFindController = {
initialize: function(options) {
if(typeof PDFFindBar === 'undefined' || PDFFindBar === null) {
throw 'PDFFindController cannot be initialized ' +
throw 'PDFFindController cannot be initialized ' +
'without a PDFFindController instance';
}
@ -845,10 +843,8 @@ var PDFFindController = {
this.pageMatches[pageIndex] = matches;
this.updatePage(pageIndex);
if (this.resumePageIdx === pageIndex) {
var callback = this.resumeCallback;
this.resumePageIdx = null;
this.resumeCallback = null;
callback();
this.nextPageMatch();
}
},
@ -937,7 +933,6 @@ var PDFFindController = {
this.offset.pageIdx = currentPageIndex;
this.offset.matchIdx = null;
this.hadMatch = false;
this.resumeCallback = null;
this.resumePageIdx = null;
this.pageMatches = [];
var self = this;
@ -964,7 +959,7 @@ var PDFFindController = {
}
// If we're waiting on a page, we return since we can't do anything else.
if (this.resumeCallback) {
if (this.resumePageIdx) {
return;
}
@ -990,48 +985,49 @@ var PDFFindController = {
this.nextPageMatch();
},
nextPageMatch: function() {
if (this.resumePageIdx !== null)
console.error('There can only be one pending page.');
var matchesReady = function(matches) {
var offset = this.offset;
var numMatches = matches.length;
var previous = this.state.findPrevious;
if (numMatches) {
// There were matches for the page, so initialize the matchIdx.
this.hadMatch = true;
offset.matchIdx = previous ? numMatches - 1 : 0;
this.updateMatch(true);
} else {
// No matches attempt to search the next page.
this.advanceOffsetPage(previous);
if (offset.wrapped) {
offset.matchIdx = null;
if (!this.hadMatch) {
// No point in wrapping there were no matches.
this.updateMatch(false);
return;
}
matchesReady: function(matches) {
var offset = this.offset;
var numMatches = matches.length;
var previous = this.state.findPrevious;
if (numMatches) {
// There were matches for the page, so initialize the matchIdx.
this.hadMatch = true;
offset.matchIdx = previous ? numMatches - 1 : 0;
this.updateMatch(true);
// matches were found
return true;
} else {
// No matches attempt to search the next page.
this.advanceOffsetPage(previous);
if (offset.wrapped) {
offset.matchIdx = null;
if (!this.hadMatch) {
// No point in wrapping there were no matches.
this.updateMatch(false);
// while matches were not found, searching for a page
// with matches should nevertheless halt.
return true;
}
// Search the next page.
this.nextPageMatch();
}
}.bind(this);
var pageIdx = this.offset.pageIdx;
var pageMatches = this.pageMatches;
if (!pageMatches[pageIdx]) {
// The matches aren't ready setup a callback so we can be notified,
// when they are ready.
this.resumeCallback = function() {
matchesReady(pageMatches[pageIdx]);
};
this.resumePageIdx = pageIdx;
return;
// matches were not found (and searching is not done)
return false;
}
// The matches are finished already.
matchesReady(pageMatches[pageIdx]);
},
nextPageMatch: function() {
if (this.resumePageIdx !== null) {
console.error('There can only be one pending page.');
}
do {
var pageIdx = this.offset.pageIdx;
var matches = this.pageMatches[pageIdx];
if (!matches) {
// The matches don't exist yet for processing by "matchesReady",
// so set a resume point for when they do exist.
this.resumePageIdx = pageIdx;
break;
}
} while (!this.matchesReady(matches));
},
advanceOffsetPage: function(previous) {
@ -1909,16 +1905,17 @@ var GrabToPan = (function GrabToPanClosure() {
this._onmousedown = this._onmousedown.bind(this);
this._onmousemove = this._onmousemove.bind(this);
this._endPan = this._endPan.bind(this);
// This overlay will be inserted in the document when the mouse moves during
// a grab operation, to ensure that the cursor has the desired appearance.
var overlay = this.overlay = document.createElement('div');
overlay.className = 'grab-to-pan-grabbing';
}
GrabToPan.prototype = {
/**
* Class name of element which can be grabbed
*/
CSS_CLASS_GRAB: 'grab-to-pan-grab',
/**
* Class name of element which is being dragged & panned
*/
CSS_CLASS_GRABBING: 'grab-to-pan-grabbing',
/**
* Bind a mousedown event to the element to enable grab-detection.
@ -2001,7 +1998,6 @@ var GrabToPan = (function GrabToPanClosure() {
this.element.addEventListener('scroll', this._endPan, true);
event.preventDefault();
event.stopPropagation();
this.element.classList.remove(this.CSS_CLASS_GRAB);
this.document.documentElement.classList.add(this.CSS_CLASS_GRABBING);
},
@ -2011,13 +2007,16 @@ var GrabToPan = (function GrabToPanClosure() {
_onmousemove: function GrabToPan__onmousemove(event) {
this.element.removeEventListener('scroll', this._endPan, true);
if (isLeftMouseReleased(event)) {
this.document.removeEventListener('mousemove', this._onmousemove, true);
this._endPan();
return;
}
var xDiff = event.clientX - this.clientXStart;
var yDiff = event.clientY - this.clientYStart;
this.element.scrollTop = this.scrollTopStart - yDiff;
this.element.scrollLeft = this.scrollLeftStart - xDiff;
if (!this.overlay.parentNode) {
document.body.appendChild(this.overlay);
}
},
/**
@ -2027,8 +2026,9 @@ var GrabToPan = (function GrabToPanClosure() {
this.element.removeEventListener('scroll', this._endPan, true);
this.document.removeEventListener('mousemove', this._onmousemove, true);
this.document.removeEventListener('mouseup', this._endPan, true);
this.document.documentElement.classList.remove(this.CSS_CLASS_GRABBING);
this.element.classList.add(this.CSS_CLASS_GRAB);
if (this.overlay.parentNode) {
this.overlay.parentNode.removeChild(this.overlay);
}
}
};
@ -2186,7 +2186,7 @@ var DocumentProperties = {
this.fileName = getPDFFileNameFromURL(PDFView.url);
// Get the file size.
PDFView.pdfDocument.dataLoaded().then(function(data) {
PDFView.pdfDocument.getDownloadInfo().then(function(data) {
self.setFileSize(data.length);
});
@ -2681,6 +2681,11 @@ var PDFView = {
};
window.addEventListener('message', function windowMessage(e) {
if (e.source !== null) {
// The message MUST originate from Chrome code.
console.warn('Rejected untrusted message from ' + e.origin);
return;
}
var args = e.data;
if (typeof args !== 'object' || !('pdfjsLoadAction' in args))
@ -2989,7 +2994,7 @@ var PDFView = {
var errorWrapper = document.getElementById('errorWrapper');
errorWrapper.setAttribute('hidden', 'true');
pdfDocument.dataLoaded().then(function() {
pdfDocument.getDownloadInfo().then(function() {
PDFView.loadingBar.hide();
var outerContainer = document.getElementById('outerContainer');
outerContainer.classList.remove('loadingInProgress');
@ -5181,30 +5186,6 @@ window.addEventListener('hashchange', function webViewerHashchange(evt) {
}
});
window.addEventListener('change', function webViewerChange(evt) {
var files = evt.target.files;
if (!files || files.length === 0)
return;
// Read the local file into a Uint8Array.
var fileReader = new FileReader();
fileReader.onload = function webViewerChangeFileReaderOnload(evt) {
var buffer = evt.target.result;
var uint8Array = new Uint8Array(buffer);
PDFView.open(uint8Array, 0);
};
var file = files[0];
fileReader.readAsArrayBuffer(file);
PDFView.setTitleUsingUrl(file.name);
// URL does not reflect proper document location - hiding some icons.
document.getElementById('viewBookmark').setAttribute('hidden', 'true');
document.getElementById('secondaryViewBookmark').
setAttribute('hidden', 'true');
document.getElementById('download').setAttribute('hidden', 'true');
document.getElementById('secondaryDownload').setAttribute('hidden', 'true');
}, true);
function selectScaleOption(value) {
var options = document.getElementById('scaleSelect').options;
@ -5294,19 +5275,22 @@ window.addEventListener('pagechange', function pagechange(evt) {
document.getElementById('next').disabled = (page >= PDFView.pages.length);
}, true);
// Firefox specific event, so that we can prevent browser from zooming
window.addEventListener('DOMMouseScroll', function(evt) {
if (evt.ctrlKey) {
evt.preventDefault();
function handleMouseWheel(evt) {
var MOUSE_WHEEL_DELTA_FACTOR = 40;
var ticks = (evt.type === 'DOMMouseScroll') ? -evt.detail :
evt.wheelDelta / MOUSE_WHEEL_DELTA_FACTOR;
var direction = (ticks < 0) ? 'zoomOut' : 'zoomIn';
var ticks = evt.detail;
var direction = (ticks > 0) ? 'zoomOut' : 'zoomIn';
if (evt.ctrlKey) { // Only zoom the pages, not the entire viewer
evt.preventDefault();
PDFView[direction](Math.abs(ticks));
} else if (PresentationMode.active) {
var FIREFOX_DELTA_FACTOR = -40;
PDFView.mouseScroll(evt.detail * FIREFOX_DELTA_FACTOR);
PDFView.mouseScroll(ticks * MOUSE_WHEEL_DELTA_FACTOR);
}
}, false);
}
window.addEventListener('DOMMouseScroll', handleMouseWheel);
window.addEventListener('mousewheel', handleMouseWheel);
window.addEventListener('click', function click(evt) {
if (!PresentationMode.active) {

View File

@ -13,8 +13,8 @@
<!ENTITY privatebrowsingpage.openPrivateWindow.label "Open a Private Window">
<!ENTITY privatebrowsingpage.openPrivateWindow.accesskey "P">
<!-- LOCALIZATION NOTE (privatebrowsingpage.howToStart3): please leave &basePBMenu.label; intact in the translation -->
<!ENTITY privatebrowsingpage.howToStart3 "To start Private Browsing, you can also select &basePBMenu.label; &gt; &newPrivateWindow.label;.">
<!-- LOCALIZATION NOTE (privatebrowsingpage.howToStart4): please leave &newPrivateWindow.label; intact in the translation -->
<!ENTITY privatebrowsingpage.howToStart4 "To start Private Browsing, you can also select &newPrivateWindow.label; from the menu.">
<!ENTITY privatebrowsingpage.howToStop3 "To stop Private Browsing, you can close this window.">
<!ENTITY privatebrowsingpage.moreInfo "While this computer won't have a record of your browsing history, your internet service provider or employer can still track the pages you visit.">

View File

@ -526,9 +526,9 @@ slowStartup.disableNotificationButton.label = Don't Tell Me Again
slowStartup.disableNotificationButton.accesskey = A
# LOCALIZATION NOTE(tipSection.tip0): %1$S will be replaced with the text defined
# in tipSection.tip0.hint, %2$S will be replaced with brandShortName, %3$S will
# be replaced with a hyperlink containing the text defined in tipSection.tip0.learnMore.
tipSection.tip0 = %1$S: You can customize %2$S to work the way you do. Simply drag any of the above to the menu or toolbar. %3$S about customizing %2$S.
tipSection.tip0.hint = Hint
tipSection.tip0.learnMore = Learn more
# LOCALIZATION NOTE(customizeTips.tip0): %1$S will be replaced with the text defined
# in customizeTips.tip0.hint, %2$S will be replaced with brandShortName, %3$S will
# be replaced with a hyperlink containing the text defined in customizeTips.tip0.learnMore.
customizeTips.tip0 = %1$S: You can customize %2$S to work the way you do. Simply drag any of the above to the menu or toolbar. %3$S about customizing %2$S.
customizeTips.tip0.hint = Hint
customizeTips.tip0.learnMore = Learn more

View File

@ -214,6 +214,10 @@ errorLoadingText=Error loading source:\n
# watch expressions list to add a new item.
addWatchExpressionText=Add watch expression
# LOCALIZATION NOTE (addWatchExpressionButton): The button that is displayed in the
# variables view popup.
addWatchExpressionButton=Watch
# LOCALIZATION NOTE (emptyVariablesText): The text that is displayed in the
# variables pane when there are no variables to display.
emptyVariablesText=No variables to display

View File

@ -30,6 +30,17 @@
<property name="items" readonly="true" onget="return this.querySelectorAll('richgriditem[value]');"/>
<property name="itemCount" readonly="true" onget="return this.items.length;"/>
<method name="isItem">
<parameter name="anItem"/>
<body>
<![CDATA[
// only non-empty child nodes are considered items
return anItem && anItem.hasAttribute("value") &&
anItem.parentNode == this;
]]>
</body>
</method>
<!-- nsIDOMXULMultiSelectControlElement (not fully implemented) -->
<method name="clearSelection">
@ -54,6 +65,9 @@
<parameter name="anItem"/>
<body>
<![CDATA[
if (!this.isItem(anItem))
return;
let wasSelected = anItem.selected;
if ("single" == this.getAttribute("seltype")) {
this.clearSelection();
@ -72,6 +86,8 @@
<parameter name="anItem"/>
<body>
<![CDATA[
if (!this.isItem(anItem))
return;
let wasSelected = anItem.selected,
isSingleMode = ("single" == this.getAttribute("seltype"));
if (isSingleMode) {
@ -108,7 +124,7 @@
<parameter name="aEvent"/>
<body>
<![CDATA[
if (!this.isBound)
if (!(this.isBound && this.isItem(aItem)))
return;
if ("single" == this.getAttribute("seltype")) {
@ -128,7 +144,7 @@
<parameter name="aEvent"/>
<body>
<![CDATA[
if (!this.isBound || this.noContext)
if (!this.isBound || this.noContext || !this.isItem(aItem))
return;
// we'll republish this as a selectionchange event on the grid
aEvent.stopPropagation();
@ -364,7 +380,7 @@
<parameter name="aSkipArrange"/>
<body>
<![CDATA[
if (!aItem || Array.indexOf(this.items, aItem) < 0)
if (!this.isItem(aItem))
return null;
let removal = this.removeChild(aItem);
@ -387,7 +403,7 @@
<parameter name="anItem"/>
<body>
<![CDATA[
if (!anItem)
if (!this.isItem(anItem))
return -1;
return Array.indexOf(this.items, anItem);
@ -791,8 +807,8 @@
<parameter name="aEvent"/>
<body><![CDATA[
// apply the transform to the contentBox element of the item
let bendNode = 'richgriditem' == aItem.nodeName && aItem._contentBox;
if (!bendNode)
let bendNode = this.isItem(aItem) ? aItem._contentBox : null;
if (!bendNode || aItem.hasAttribute("bending"))
return;
let event = aEvent;
@ -856,6 +872,7 @@
<handler event="mouseup" button="0" action="this.unbendItem(event.target)"/>
<handler event="mouseout" button="0" action="this.unbendItem(event.target)"/>
<handler event="touchend" action="this.unbendItem(event.target)"/>
<handler event="touchcancel" action="this.unbendItem(event.target)"/>
<!-- /item bend effect handler -->
<handler event="context-action">

View File

@ -56,6 +56,12 @@ XPCOMUtils.defineLazyModuleGetter(this, "OS",
XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry",
"resource://gre/modules/UITelemetry.jsm");
#ifdef MOZ_UPDATER
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
#endif
/*
* Services
*/

View File

@ -74,10 +74,6 @@ let AboutFlyoutPanel = {
};
#ifdef MOZ_UPDATER
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource://gre/modules/DownloadUtils.jsm");
Components.utils.import("resource://gre/modules/AddonManager.jsm");
function onUnload(aEvent) {
if (!gAppUpdater) {
return;

View File

@ -616,7 +616,6 @@ var SelectionHelperUI = {
Elements.tabList.addEventListener("TabSelect", this, true);
Elements.navbar.addEventListener("transitionend", this, true);
Elements.navbar.addEventListener("MozAppbarDismissing", this, true);
this.overlay.enabled = true;
},
@ -644,7 +643,6 @@ var SelectionHelperUI = {
Elements.tabList.removeEventListener("TabSelect", this, true);
Elements.navbar.removeEventListener("transitionend", this, true);
Elements.navbar.removeEventListener("MozAppbarDismissing", this, true);
this._shutdownAllMarkers();
@ -915,31 +913,21 @@ var SelectionHelperUI = {
},
/*
* Detects when the nav bar hides or shows, so we can enable
* selection at the appropriate location once the transition is
* complete, or shutdown selection down when the nav bar is hidden.
* Detects when the nav bar transitions, so we can enable selection at the
* appropriate location once the transition is complete, or shutdown
* selection down when the nav bar is hidden.
*/
_onNavBarTransitionEvent: function _onNavBarTransitionEvent(aEvent) {
// Ignore when selection is in content
if (this.layerMode == kContentLayer) {
return;
}
if (aEvent.propertyName == "bottom" && Elements.navbar.isShowing) {
// After tansitioning up, show the monocles
if (Elements.navbar.isShowing) {
this._showAfterUpdate = true;
this._sendAsyncMessage("Browser:SelectionUpdate", {});
return;
}
if (aEvent.propertyName == "transform" && Elements.navbar.isShowing) {
this._sendAsyncMessage("Browser:SelectionUpdate", {});
this._showMonocles(ChromeSelectionHandler.hasSelection);
}
},
_onNavBarDismissEvent: function _onNavBarDismissEvent() {
if (!this.isActive || this.layerMode == kContentLayer) {
return;
}
this._hideMonocles();
},
_onKeyboardChangedEvent: function _onKeyboardChangedEvent() {
@ -1090,10 +1078,6 @@ var SelectionHelperUI = {
this._onNavBarTransitionEvent(aEvent);
break;
case "MozAppbarDismissing":
this._onNavBarDismissEvent();
break;
case "KeyboardChanged":
this._onKeyboardChangedEvent();
break;

View File

@ -0,0 +1,44 @@
// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
gTests.push({
desc: "about flyout hides navbar, clears navbar selection, doesn't leak",
run: function() {
yield showNavBar();
let edit = document.getElementById("urlbar-edit");
edit.value = "http://www.wikipedia.org/";
sendElementTap(window, edit);
yield waitForCondition(function () {
return SelectionHelperUI.isSelectionUIVisible;
});
ok(ContextUI.navbarVisible, "nav bar visible");
let promise = waitForEvent(FlyoutPanelsUI.AboutFlyoutPanel._topmostElement, "transitionend");
FlyoutPanelsUI.show('AboutFlyoutPanel');
yield promise;
yield waitForCondition(function () {
return !SelectionHelperUI.isSelectionUIVisible;
});
ok(!ContextUI.navbarVisible, "nav bar hidden");
promise = waitForEvent(FlyoutPanelsUI.AboutFlyoutPanel._topmostElement, "transitionend");
FlyoutPanelsUI.hide('AboutFlyoutPanel');
yield promise;
}
});
function test() {
if (!isLandscapeMode()) {
todo(false, "browser_selection_tests need landscape mode to run.");
return;
}
runTests();
}

View File

@ -5,7 +5,7 @@
</style>
</head>
<body style="margin: 20px 20px 20px 20px;">
<form id="form1" method="get" action="" autocomplete="on">
<form id="form1" method="get" autocomplete="on">
<datalist id="testdatalist">
<option value="one">
<option value="two">

View File

@ -60,9 +60,8 @@ gTests.push({
let input = tabDocument.getElementById("textedit1");
input.value = "hellothere";
form.action = chromeRoot + "browser_form_auto_complete.html";
loadedPromise = waitForEvent(Browser.selectedTab.browser, "DOMContentLoaded");
loadedPromise = waitForObserver("satchel-storage-changed", null, "formhistory-add");
form.submit();
yield loadedPromise;

View File

@ -174,9 +174,60 @@ gTests.push({
let promise = waitForCondition(condition);
sendElementTap(window, copy);
ok((yield promise), "copy text onto clipboard")
clearSelection(edit);
edit.blur();
}
})
gTests.push({
desc: "bug 965832 - selection monocles move with the nav bar",
run: function() {
yield showNavBar();
let originalUtils = Services.metro;
Services.metro = {
keyboardHeight: 0,
keyboardVisible: false
};
registerCleanupFunction(function() {
Services.metro = originalUtils;
});
let edit = document.getElementById("urlbar-edit");
edit.value = "http://www.wikipedia.org/";
sendElementTap(window, edit);
let promise = waitForEvent(window, "MozDeckOffsetChanged");
Services.metro.keyboardHeight = 300;
Services.metro.keyboardVisible = true;
Services.obs.notifyObservers(null, "metro_softkeyboard_shown", null);
yield promise;
yield waitForCondition(function () {
return SelectionHelperUI.isSelectionUIVisible;
});
promise = waitForEvent(window, "MozDeckOffsetChanged");
Services.metro.keyboardHeight = 0;
Services.metro.keyboardVisible = false;
Services.obs.notifyObservers(null, "metro_softkeyboard_hidden", null);
yield promise;
yield waitForCondition(function () {
return SelectionHelperUI.isSelectionUIVisible;
});
clearSelection(edit);
edit.blur();
yield waitForCondition(function () {
return !SelectionHelperUI.isSelectionUIVisible;
});
}
});
function test() {
if (!isLandscapeMode()) {
todo(false, "browser_selection_tests need landscape mode to run.");

View File

@ -500,6 +500,8 @@ gTests.push({
is(grid.itemCount, 0, "slots do not count towards itemCount");
ok(Array.every(grid.children, (node) => node.nodeName == 'richgriditem'), "slots have nodeName richgriditem");
ok(Array.every(grid.children, isNotBoundByRichGrid_Item), "slots aren't bound by the richgrid-item binding");
ok(!grid.isItem(grid.children[0]), "slot fails isItem validation");
},
tearDown: gridSlotsTearDown
});
@ -648,3 +650,26 @@ gTests.push({
},
tearDown: gridSlotsTearDown
});
gTests.push({
desc: "richgrid empty slot selection",
setUp: gridSlotsSetup,
run: function() {
let grid = this.grid;
// leave grid empty, it has 6 slots
is(grid.itemCount, 0, "Grid setup with 0 items");
is(grid.children.length, 6, "Empty grid has the expected number of slots");
info("slot is initially selected: " + grid.children[0].selected);
grid.selectItem(grid.children[0]);
info("after selectItem, slot is selected: " + grid.children[0].selected);
ok(!grid.children[0].selected, "Attempting to select an empty slot has no effect");
grid.toggleItemSelection(grid.children[0]);
ok(!grid.children[0].selected, "Attempting to toggle selection on an empty slot has no effect");
},
tearDown: gridSlotsTearDown
});

View File

@ -519,7 +519,7 @@ function waitForImageLoad(aWindow, aImageId) {
* @param aTimeoutMs the number of miliseconds to wait before giving up
* @returns a Promise that resolves to true, or to an Error
*/
function waitForObserver(aObsEvent, aTimeoutMs) {
function waitForObserver(aObsEvent, aTimeoutMs, aObsData) {
try {
let deferred = Promise.defer();
@ -540,7 +540,8 @@ function waitForObserver(aObsEvent, aTimeoutMs) {
},
observe: function (aSubject, aTopic, aData) {
if (aTopic == aObsEvent) {
if (aTopic == aObsEvent &&
(!aObsData || (aObsData == aData))) {
this.onEvent();
}
},

View File

@ -37,6 +37,7 @@ support-files =
res/documentindesignmode.html
[browser_apzc_basic.js]
[browser_flyouts.js]
[browser_bookmarks.js]
[browser_canonizeURL.js]
[browser_circular_progress_indicator.js]

View File

@ -40,7 +40,7 @@ let CrossSlidingStateNames = [
function isSelectable(aElement) {
// placeholder logic
return aElement.nodeName == 'richgriditem';
return aElement.nodeName == 'richgriditem' && aElement.hasAttribute("value");
}
function withinCone(aLen, aHeight) {
// check pt falls within 45deg either side of the cross axis

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this file,
- You can obtain one at http://mozilla.org/MPL/2.0/. -->
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
version="1.0.0.0"
processorArchitecture="*"
name="Mozilla.FirefoxCEH"
type="win32"
/>
<description>Firefox Launcher</description>
<ms_asmv3:trustInfo xmlns:ms_asmv3="urn:schemas-microsoft-com:asm.v3">
<ms_asmv3:security>
<ms_asmv3:requestedPrivileges>
<ms_asmv3:requestedExecutionLevel level="asInvoker" uiAccess="false" />
</ms_asmv3:requestedPrivileges>
</ms_asmv3:security>
</ms_asmv3:trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
</application>
</compatibility>
</assembly>

View File

@ -0,0 +1,6 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
1 24 "CommandExecuteHandler.exe.manifest"

View File

@ -5,6 +5,7 @@
include $(topsrcdir)/config/config.mk
DIST_PROGRAM = CommandExecuteHandler$(BIN_SUFFIX)
RCINCLUDE = CommandExecuteHandler.rc
# Don't link against mozglue.dll
MOZ_GLUE_LDFLAGS =

View File

@ -54,38 +54,55 @@
pointer-events: none;
}
#tabs > .tabs-scrollbox > .scrollbutton-down:-moz-locale-dir(rtl),
#tabs > .tabs-scrollbox > .scrollbutton-up {
list-style-image: url("images/tab-arrows.png") !important;
-moz-image-region: rect(15px 58px 63px 14px) !important;
padding-right: 15px;
width: @tabs_scrollarrow_width@;
}
#tabs > .tabs-scrollbox > .scrollbutton-down:hover:-moz-locale-dir(rtl),
#tabs > .tabs-scrollbox > .scrollbutton-up:hover {
-moz-image-region: rect(14px 102px 62px 58px) !important;
}
#tabs > .tabs-scrollbox > .scrollbutton-down:active:-moz-locale-dir(rtl),
#tabs > .tabs-scrollbox > .scrollbutton-up:active {
-moz-image-region: rect(14px 152px 62px 108px) !important;
}
#tabs > .tabs-scrollbox > .scrollbutton-down[disabled]:-moz-locale-dir(rtl),
#tabs > .tabs-scrollbox > .scrollbutton-up[disabled] {
-moz-image-region: rect(15px 196px 63px 152px) !important;
}
#tabs > .tabs-scrollbox > .scrollbutton-up:-moz-locale-dir(rtl),
#tabs > .tabs-scrollbox > .scrollbutton-down {
list-style-image: url("images/tab-arrows.png") !important;
-moz-image-region: rect(73px 58px 121px 14px) !important;
padding-left: 15px;
width: @tabs_scrollarrow_width@;
}
#tabs > .tabs-scrollbox > .scrollbutton-up:hover:-moz-locale-dir(rtl),
#tabs > .tabs-scrollbox > .scrollbutton-down:hover {
-moz-image-region: rect(72px 102px 120px 58px) !important;
}
#tabs > .tabs-scrollbox > .scrollbutton-up:active:-moz-locale-dir(rtl),
#tabs > .tabs-scrollbox > .scrollbutton-down:active {
-moz-image-region: rect(72px 152px 120px 108px) !important;
}
#tabs > .tabs-scrollbox > .scrollbutton-up[disabled]:-moz-locale-dir(rtl),
#tabs > .tabs-scrollbox > .scrollbutton-down[disabled] {
-moz-image-region: rect(73px 196px 121px 152px) !important;
}
.tabs-scrollbox > .scrollbutton-up:not([disabled]):not([collapsed]):-moz-locale-dir(rtl)::after {
right: calc(@tabs_scrollarrow_width@ + @metro_spacing_normal@);
}
.tabs-scrollbox > .scrollbutton-down:not([disabled]):not([collapsed]):-moz-locale-dir(rtl)::before {
right: auto;
left: calc(@tabs_scrollarrow_width@ + @newtab_button_width@);
}
.tabs-scrollbox > .scrollbutton-up:not([disabled]):not([collapsed])::after {
content: "";
visibility: visible;

View File

@ -443,6 +443,9 @@ this.BrowserUITelemetry = {
menuBar && Services.appinfo.OS != "Darwin"
&& menuBar.getAttribute("autohide") != "true";
// Determine if the titlebar is currently visible.
result.titleBarEnabled = !Services.prefs.getBoolPref("browser.tabs.drawInTitlebar");
// Examine all customizable areas and see what default items
// are present and missing.
let defaultKept = [];

View File

@ -1921,10 +1921,6 @@ chatbox {
background-attachment: fixed;
}
#main-window:-moz-any([customize-entering],[customize-entered]) #tab-view-deck {
padding: 0 2em 2em;
}
#main-window[customize-entered] #navigator-toolbox > toolbar:not(#toolbar-menubar):not(#TabsToolbar),
#main-window[customize-entered] #customization-container {
border: 3px solid hsla(0,0%,0%,.1);
@ -1947,6 +1943,11 @@ chatbox {
border-left: 3px solid transparent;
}
#main-window[customizing] #TabsToolbar::after {
margin-left: 2em;
margin-right: 2em;
}
/* End customization mode */

View File

@ -424,7 +424,7 @@ toolbar .toolbarbutton-1:not([type="menu-button"]),
#nav-bar .toolbaritem-combined-buttons > .toolbarbutton-1 + .toolbarbutton-1::before {
content: "";
display: -moz-box;
position: absolute;
position: relative;
top: calc(50% - 9px);
width: 1px;
height: 18px;
@ -437,6 +437,10 @@ toolbar .toolbarbutton-1:not([type="menu-button"]),
box-shadow: 0 0 0 1px hsla(0,0%,100%,.2);
}
#nav-bar .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker {
-moz-box-orient: horizontal;
}
#nav-bar .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon {
-moz-margin-start: 10px;
}
@ -2561,6 +2565,10 @@ toolbarbutton.chevron > .toolbarbutton-menu-dropmarker {
text-shadow: @loweredShadow@;
}
.tabbrowser-tab[selected=true]:-moz-lwtheme {
text-shadow: inherit;
}
.tabbrowser-tabs[closebuttons="hidden"] > * > * > * > .tab-close-button:not([pinned]) {
display: -moz-box;
visibility: hidden;
@ -2604,7 +2612,7 @@ toolbarbutton.chevron > .toolbarbutton-menu-dropmarker {
/*
* Draw the bottom border of the tabstrip when core doesn't do it for us:
*/
#main-window:-moz-any([privatebrowsingmode=temporary],[sizemode="fullscreen"],[customizing],[customize-exiting]) #TabsToolbar::after,
#main-window:-moz-any([privatebrowsingmode=temporary],[sizemode="fullscreen"],[customize-entered]) #TabsToolbar::after,
#main-window:not([tabsintitlebar]) #TabsToolbar::after,
#TabsToolbar:-moz-lwtheme::after {
content: '';
@ -4018,8 +4026,9 @@ window > chatbox {
padding-top: 0;
}
#main-window:-moz-any([customize-entering],[customize-entered]) #tab-view-deck {
padding: 0 2em 2em;
#main-window[tabsintitlebar][customize-entered] #titlebar-content {
margin-bottom: 0px !important;
margin-top: 11px !important;
}
#main-window[customize-entered] #tab-view-deck {
@ -4066,6 +4075,12 @@ window > chatbox {
}
}
#main-window[customizing] #navigator-toolbox::after,
#main-window[customize-entered] #TabsToolbar::after {
margin-left: 2em;
margin-right: 2em;
}
/* End customization mode */
#main-window[privatebrowsingmode=temporary] {
@ -4137,4 +4152,4 @@ window > chatbox {
#UITourTooltipClose {
-moz-margin-end: -15px;
margin-top: -12px;
}
}

View File

@ -3,6 +3,16 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* Customization mode */
#main-window:-moz-any([customize-entering],[customize-entered]) #content-deck {
margin: 0 2em 2em;
}
#main-window:-moz-any([customize-entering],[customize-entered]) #navigator-toolbox > toolbar:not(#TabsToolbar) {
margin-left: 2em;
margin-right: 2em;
}
#main-window:-moz-any([customize-entering],[customize-exiting]) #tab-view-deck {
pointer-events: none;
}
@ -127,14 +137,21 @@ toolbarpaletteitem[notransition][place="panel"] {
transition: none;
}
toolbarpaletteitem > toolbarbutton > .toolbarbutton-icon {
transition: transform .3s cubic-bezier(.6, 2, .75, 1.5);
toolbarpaletteitem > toolbarbutton > .toolbarbutton-icon,
toolbarpaletteitem > toolbaritem.panel-wide-item,
toolbarpaletteitem > toolbarbutton[type="menu-button"] {
transition: transform .3s cubic-bezier(.6, 2, .75, 1.5) !important;
}
toolbarpaletteitem[mousedown] > toolbarbutton > .toolbarbutton-icon {
transform: scale(1.3);
}
toolbarpaletteitem[mousedown] > toolbaritem.panel-wide-item,
toolbarpaletteitem[mousedown] > toolbarbutton[type="menu-button"] {
transform: scale(1.1);
}
/* Override the toolkit styling for items being dragged over. */
toolbarpaletteitem[place="toolbar"] {
border-left-width: 0;

View File

@ -537,10 +537,16 @@ toolbarpaletteitem[place="palette"] > #bookmarks-menu-button > .toolbarbutton-me
display: none;
}
#search-container[cui-areatype="menu-panel"] {
#search-container[cui-areatype="menu-panel"],
#wrapper-search-container[place="panel"] {
width: @menuPanelWidth@;
}
#search-container[cui-areatype="menu-panel"] {
margin-top: 6px;
margin-bottom: 6px;
}
toolbarpaletteitem[place="palette"] > #search-container {
min-width: 7em;
width: 7em;

View File

@ -27,7 +27,7 @@
background-image: none;
background-color: transparent;
border: 0;
border-bottom: 1px solid #aaa;
border-bottom: 1px solid rgba(118, 121, 125, .5);
min-height: 3px;
height: 3px;
margin-top: -3px;
@ -39,7 +39,7 @@
background-image: none;
background-color: transparent;
border: 0;
-moz-border-end: 1px solid #aaa;
-moz-border-end: 1px solid rgba(118, 121, 125, .5);
min-width: 3px;
width: 3px;
-moz-margin-start: -3px;

View File

@ -5,7 +5,6 @@
/* Sources and breakpoints pane */
#sources-pane[selectedIndex="0"] + #sources-and-editor-splitter {
border-color: transparent;
}
@ -291,6 +290,22 @@
color: inherit;
}
.dbg-expression-button {
-moz-appearance: none;
border: none;
background: none;
cursor: pointer;
text-decoration: underline;
}
.theme-dark .dbg-expression-button {
color: #46afe3; /* Blue highlight color */
}
.theme-light .dbg-expression-button {
color: #0088cc; /* Blue highlight color */
}
/* Event listeners view */
.dbg-event-listener-type {
@ -554,31 +569,6 @@
list-style-image: url(debugger-step-out.png);
}
#debugger-controls > toolbarbutton,
#sources-controls > toolbarbutton {
margin: 0;
box-shadow: none;
border-radius: 0;
border-width: 0;
-moz-border-end-width: 1px;
outline-offset: -3px;
}
#debugger-controls > toolbarbutton:last-of-type,
#sources-controls > toolbarbutton:last-of-type {
-moz-border-end-width: 0;
}
#debugger-controls,
#sources-controls {
box-shadow: 0 1px 0 hsla(210,16%,76%,.15) inset,
0 0 0 1px hsla(210,16%,76%,.15) inset,
0 1px 0 hsla(210,16%,76%,.15);
border: 1px solid hsla(210,8%,5%,.45);
border-radius: 3px;
margin: 0 3px;
}
#instruments-pane-toggle {
background: none;
box-shadow: none;

View File

@ -307,4 +307,12 @@ div.CodeMirror span.eval-text {
border-bottom: 0;
}
.devtools-horizontal-splitter {
border-bottom: 1px solid #aaa;
}
.devtools-side-splitter {
-moz-border-end: 1px solid #aaa;
}
%include toolbars.inc.css

View File

@ -69,28 +69,6 @@
color: #ebeced;
}
#profiler-controls > toolbarbutton {
margin: 0;
box-shadow: none;
border-radius: 0;
border-width: 0;
-moz-border-end-width: 1px;
outline-offset: -3px;
}
#profiler-controls > toolbarbutton:last-of-type {
-moz-border-end-width: 0;
}
#profiler-controls {
box-shadow: 0 1px 0 hsla(210,16%,76%,.15) inset,
0 0 0 1px hsla(210,16%,76%,.15) inset,
0 1px 0 hsla(210,16%,76%,.15);
border: 1px solid hsla(210,8%,5%,.45);
border-radius: 3px;
margin: 0 3px;
}
#profiler-start {
list-style-image: url("chrome://browser/skin/devtools/profiler-stopwatch.png");
-moz-image-region: rect(0px,16px,16px,0px);

View File

@ -26,6 +26,28 @@
margin: 0 3px;
}
.devtools-menulist:-moz-focusring,
.devtools-toolbarbutton:-moz-focusring {
outline: 1px dotted hsla(210,30%,85%,0.7);
outline-offset: -4px;
}
.devtools-toolbarbutton > .toolbarbutton-icon {
margin: 0;
}
.devtools-toolbarbutton:not([label]) {
min-width: 32px;
}
.devtools-toolbarbutton:not([label]) > .toolbarbutton-text {
display: none;
}
.devtools-toolbarbutton > .toolbarbutton-menubutton-button {
-moz-box-orient: horizontal;
}
.theme-dark .devtools-menulist,
.theme-dark .devtools-toolbarbutton {
background: linear-gradient(hsla(212,7%,57%,.35), hsla(212,7%,57%,.1)) padding-box;
@ -62,28 +84,6 @@
color: #18191a;
}
.devtools-toolbarbutton > .toolbarbutton-menubutton-button {
-moz-box-orient: horizontal;
}
.devtools-menulist:-moz-focusring,
.devtools-toolbarbutton:-moz-focusring {
outline: 1px dotted hsla(210,30%,85%,0.7);
outline-offset: -4px;
}
.devtools-toolbarbutton > .toolbarbutton-icon {
margin: 0;
}
.devtools-toolbarbutton:not([label]) {
min-width: 32px;
}
.devtools-toolbarbutton:not([label]) > .toolbarbutton-text {
display: none;
}
.theme-dark .devtools-toolbarbutton:not([checked]):hover:active {
border-color: hsla(210,8%,5%,.6);
background: linear-gradient(hsla(220,6%,10%,.3), hsla(212,7%,57%,.15) 65%, hsla(212,7%,57%,.3));
@ -98,15 +98,15 @@
box-shadow: 0 1px 3px hsla(210,8%,5%,.25) inset, 0 1px 3px hsla(210,8%,5%,.25) inset, 0 1px 0 hsla(210,16%,76%,.15);
}
.devtools-toolbarbutton[checked=true] {
.theme-dark .devtools-toolbarbutton[checked=true] {
color: hsl(208,100%,60%);
}
.devtools-toolbarbutton[checked=true]:hover {
.theme-dark .devtools-toolbarbutton[checked=true]:hover {
background-color: transparent !important;
}
.devtools-toolbarbutton[checked=true]:hover:active {
.theme-dark .devtools-toolbarbutton[checked=true]:hover:active {
background-color: hsla(210,8%,5%,.2) !important;
}
@ -166,6 +166,37 @@
padding: 0 3px;
}
/* Toolbar button groups */
.theme-light .devtools-toolbarbutton-group > .devtools-toolbarbutton,
.theme-dark .devtools-toolbarbutton-group > .devtools-toolbarbutton {
margin: 0;
box-shadow: none;
border-radius: 0;
border-width: 0;
-moz-border-end-width: 1px;
outline-offset: -3px;
}
.devtools-toolbarbutton-group > .devtools-toolbarbutton:last-of-type {
-moz-border-end-width: 0;
}
.devtools-toolbarbutton-group {
border-radius: 3px;
margin: 0 3px;
}
.theme-dark .devtools-toolbarbutton-group {
box-shadow: 0 1px 0 hsla(210,16%,76%,.15) inset,
0 0 0 1px hsla(210,16%,76%,.15) inset,
0 1px 0 hsla(210,16%,76%,.15);
border: 1px solid hsla(210,8%,5%,.45);
}
.theme-light .devtools-toolbarbutton-group {
border: 1px solid #bbb;
}
/* Text input */
.devtools-textinput,
@ -273,7 +304,6 @@
overflow: hidden;
}
.devtools-sidebar-tabs > tabs > .tabs-right,
.devtools-sidebar-tabs > tabs > .tabs-left {
display: none;
@ -299,6 +329,7 @@
border-width: 0;
position: static;
-moz-margin-start: -1px;
text-shadow: none;
}
.devtools-sidebar-tabs > tabs > tab:first-of-type {

View File

@ -71,7 +71,6 @@
opacity: 0.5;
}
/* Draw shadows to indicate there is more content 'behind' scrollbuttons. */
.scrollbutton-up:-moz-locale-dir(ltr),
.scrollbutton-down:-moz-locale-dir(rtl) {
@ -116,16 +115,26 @@
overflow: hidden;
}
#breadcrumb-separator-before,
#breadcrumb-separator-after:after {
.theme-dark #breadcrumb-separator-before,
.theme-dark #breadcrumb-separator-after:after {
background: #1d4f73; /* Select Highlight Blue */
}
#breadcrumb-separator-after,
#breadcrumb-separator-before:after {
.theme-dark #breadcrumb-separator-after,
.theme-dark #breadcrumb-separator-before:after {
background: #343c45; /* Toolbars */
}
.theme-light #breadcrumb-separator-before,
.theme-light #breadcrumb-separator-after:after {
background: #4c9ed9; /* Select Highlight Blue */
}
.theme-light #breadcrumb-separator-after,
.theme-light #breadcrumb-separator-before:after {
background: #f0f1f2; /* Toolbars */
}
/* This chevron arrow cannot be replicated easily in CSS, so we are using
* a background image for it (still keeping it in a separate element so
* we can handle RTL support with a CSS transform).
@ -168,9 +177,16 @@
.breadcrumbs-widget-item[checked] {
background: -moz-element(#breadcrumb-separator-before) no-repeat 0 0;
}
.theme-dark .breadcrumbs-widget-item[checked] {
background-color: #1d4f73; /* Select Highlight Blue */
}
.theme-light .breadcrumbs-widget-item[checked] {
background-color: #4c9ed9; /* Select Highlight Blue */
}
.breadcrumbs-widget-item:first-child {
background-image: none;
}
@ -190,7 +206,7 @@
#breadcrumb-separator-before:-moz-locale-dir(rtl),
#breadcrumb-separator-after:-moz-locale-dir(rtl),
#breadcrumb-separator-normal:-moz-locale-dir(rtl) {
transform: scaleX(-1);
transform: scaleX(-1);
}
#breadcrumb-separator-before:-moz-locale-dir(rtl):after,
@ -198,58 +214,45 @@
transform: translateX(-5px) rotate(45deg);
}
.breadcrumbs-widget-item:not([checked]):hover label {
color: white;
}
.breadcrumbs-widget-item[checked] .breadcrumbs-widget-item-tag,
.breadcrumbs-widget-item[checked] .breadcrumbs-widget-item-id,
.breadcrumbs-widget-item[checked] .breadcrumbs-widget-item-tag,
.breadcrumbs-widget-item[checked] .breadcrumbs-widget-item-pseudo-classes,
.breadcrumbs-widget-item[checked] .breadcrumbs-widget-item-classes {
color: #f5f7fa; /* Foreground (Text) - Light */
}
.theme-dark .breadcrumbs-widget-item-id,
.theme-dark .breadcrumbs-widget-item-classes,
.theme-dark .breadcrumbs-widget-item[checked] .breadcrumbs-widget-item-classes {
.theme-dark .breadcrumbs-widget-item,
.theme-dark .breadcrumbs-widget-item-classes {
color: #f5f7fa; /* Foreground (Text) - Light */
}
.theme-light .breadcrumbs-widget-item,
.theme-light .breadcrumbs-widget-item-classes {
color: #18191a; /* Foreground (Text) - Dark */
}
.theme-dark .breadcrumbs-widget-item-id {
color: #b6babf; /* Foreground (Text) - Grey */
}
.theme-light .breadcrumbs-widget-item-id {
color: #585959; /* Foreground (Text) - Grey */
}
.theme-dark .breadcrumbs-widget-item-pseudo-classes {
color: #d99b28; /* Light Orange */
}
.theme-light .breadcrumbs-widget-item[checked] {
background: -moz-element(#breadcrumb-separator-before) no-repeat 0 0;
background-color: #4c9ed9; /* Select Highlight Blue */
}
.theme-light .breadcrumbs-widget-item:first-child {
background-image: none;
}
.theme-light #breadcrumb-separator-before,
.theme-light #breadcrumb-separator-after:after {
background: #4c9ed9; /* Select Highlight Blue */
}
.theme-light #breadcrumb-separator-after,
.theme-light #breadcrumb-separator-before:after {
background: #f0f1f2; /* Toolbars */
}
.theme-light .breadcrumbs-widget-item,
.theme-light .breadcrumbs-widget-item-id,
.theme-light .breadcrumbs-widget-item-classes {
color: #585959; /* Foreground (Text) - Grey */
}
.theme-light .breadcrumbs-widget-item-pseudo-classes {
color: #585959; /* Foreground (Text) - Grey */
color: #d97e00; /* Light Orange */
}
.theme-dark .breadcrumbs-widget-item:not([checked]):hover label {
color: white;
}
.theme-light .breadcrumbs-widget-item:not([checked]):hover label {
color: #18191a; /* Foreground (Text) - Dark */
color: black;
}
/* SimpleListWidget */

View File

@ -261,7 +261,7 @@
}
/* Need to constrain the glass fog to avoid overlapping layers, see bug 886281. */
#main-window:not([customizing]) #navigator-toolbox:not(:-moz-lwtheme) {
#navigator-toolbox:not(:-moz-lwtheme) {
overflow: -moz-hidden-unscrollable;
}

View File

@ -1479,7 +1479,7 @@ toolbarbutton[type="socialmark"] > .toolbarbutton-icon {
padding: 0;
}
#TabsToolbar:not(:-moz-lwtheme) {
#main-window:not([customizing]) #TabsToolbar:not(:-moz-lwtheme) {
background-image: linear-gradient(to top, @toolbarShadowColor@ 2px, rgba(0,0,0,.05) 2px, transparent 50%);
}
@ -2441,16 +2441,17 @@ chatbox {
background-attachment: fixed;
}
#main-window:-moz-any([customize-entering],[customize-entered]) #tab-view-deck {
padding: 0 2em 2em;
}
#customization-container {
border-left: 1px solid @toolbarShadowColor@;
border-right: 1px solid @toolbarShadowColor@;
background-clip: padding-box;
}
#main-window[customizing] #navigator-toolbox::after {
margin-left: 2em;
margin-right: 2em;
}
/* End customization mode */
#main-window[privatebrowsingmode=temporary] #TabsToolbar::after {

View File

@ -96,7 +96,7 @@ nsMenuPopupFrame::nsMenuPopupFrame(nsIPresShell* aShell, nsStyleContext* aContex
mShouldAutoPosition(true),
mInContentShell(true),
mIsMenuLocked(false),
mIsDragPopup(false),
mMouseTransparent(false),
mHFlip(false),
mVFlip(false)
{
@ -141,12 +141,6 @@ nsMenuPopupFrame::Init(nsIContent* aContent,
mPopupType = ePopupTypeTooltip;
}
if (mPopupType == ePopupTypePanel &&
aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
nsGkAtoms::drag, eIgnoreCase)) {
mIsDragPopup = true;
}
nsCOMPtr<nsIDocShellTreeItem> dsti = PresContext()->GetDocShell();
if (dsti && dsti->ItemType() == nsIDocShellTreeItem::typeChrome) {
mInContentShell = false;
@ -243,7 +237,20 @@ nsMenuPopupFrame::CreateWidgetForView(nsView* aView)
widgetData.clipSiblings = true;
widgetData.mPopupHint = mPopupType;
widgetData.mNoAutoHide = IsNoAutoHide();
widgetData.mIsDragPopup = mIsDragPopup;
if (!mInContentShell) {
// A drag popup may be used for non-static translucent drag feedback
if (mPopupType == ePopupTypePanel &&
mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
nsGkAtoms::drag, eIgnoreCase)) {
widgetData.mIsDragPopup = true;
}
// If mousethrough="always" is set directly on the popup, then the widget
// should ignore mouse events, passing them through to the content behind.
mMouseTransparent = GetStateBits() & NS_FRAME_MOUSE_THROUGH_ALWAYS;
widgetData.mMouseTransparent = mMouseTransparent;
}
nsAutoString title;
if (mContent && widgetData.mNoAutoHide) {
@ -1814,19 +1821,6 @@ nsMenuPopupFrame::AttachedDismissalListener()
mConsumeRollupEvent = nsIPopupBoxObject::ROLLUP_DEFAULT;
}
void
nsMenuPopupFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
const nsRect& aDirtyRect,
const nsDisplayListSet& aLists)
{
// don't pass events to drag popups
if (aBuilder->IsForEventDelivery() && mIsDragPopup) {
return;
}
nsBoxFrame::BuildDisplayList(aBuilder, aDirtyRect, aLists);
}
// helpers /////////////////////////////////////////////////////////////
NS_IMETHODIMP

View File

@ -230,7 +230,7 @@ public:
bool IsMenu() MOZ_OVERRIDE { return mPopupType == ePopupTypeMenu; }
bool IsOpen() MOZ_OVERRIDE { return mPopupState == ePopupOpen || mPopupState == ePopupOpenAndVisible; }
bool IsDragPopup() { return mIsDragPopup; }
bool IsMouseTransparent() { return mMouseTransparent; }
static nsIContent* GetTriggerContent(nsMenuPopupFrame* aMenuPopupFrame);
void ClearTriggerContent() { mTriggerContent = nullptr; }
@ -334,10 +334,6 @@ public:
// This position is in CSS pixels.
nsIntPoint ScreenPosition() const { return nsIntPoint(mScreenXPos, mScreenYPos); }
virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder,
const nsRect& aDirtyRect,
const nsDisplayListSet& aLists) MOZ_OVERRIDE;
nsIntPoint GetLastClientOffset() const { return mLastClientOffset; }
// Return the alignment of the popup
@ -480,7 +476,7 @@ protected:
bool mShouldAutoPosition; // Should SetPopupPosition be allowed to auto position popup?
bool mInContentShell; // True if the popup is in a content shell
bool mIsMenuLocked; // Should events inside this menu be ignored?
bool mIsDragPopup; // True if this is a popup used for drag feedback
bool mMouseTransparent; // True if this is a popup is transparent to mouse events
// the flip modes that were used when the popup was opened
bool mHFlip;

View File

@ -1385,21 +1385,21 @@ nsXULPopupManager::GetVisiblePopups(nsTArray<nsIFrame *>& aPopups)
{
aPopups.Clear();
// Iterate over both lists of popups
nsMenuChainItem* item = mPopups;
while (item) {
if (item->Frame()->PopupState() == ePopupOpenAndVisible)
aPopups.AppendElement(static_cast<nsIFrame*>(item->Frame()));
item = item->GetParent();
}
for (int32_t list = 0; list < 2; list++) {
while (item) {
// Skip panels which are not open and visible as well as popups that
// are transparent to mouse events.
if (item->Frame()->PopupState() == ePopupOpenAndVisible &&
!item->Frame()->IsMouseTransparent()) {
aPopups.AppendElement(item->Frame());
}
item = mNoHidePanels;
while (item) {
// skip panels which are not open and visible as well as draggable popups,
// as those don't respond to events.
if (item->Frame()->PopupState() == ePopupOpenAndVisible && !item->Frame()->IsDragPopup()) {
aPopups.AppendElement(static_cast<nsIFrame*>(item->Frame()));
item = item->GetParent();
}
item = item->GetParent();
item = mNoHidePanels;
}
}

View File

@ -520,6 +520,15 @@ abstract public class BrowserApp extends GeckoApp
}
});
mBrowserToolbar.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (isHomePagerVisible()) {
mHomePager.onToolbarFocusChange(hasFocus);
}
}
});
// Intercept key events for gamepad shortcuts
mBrowserToolbar.setOnKeyListener(this);

View File

@ -7,6 +7,7 @@ package org.mozilla.gecko.home;
import org.mozilla.gecko.R;
import org.mozilla.gecko.db.BrowserContract.Bookmarks;
import org.mozilla.gecko.db.BrowserContract.URLColumns;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.home.BookmarksListAdapter.FolderInfo;
import org.mozilla.gecko.home.BookmarksListAdapter.OnRefreshFolderListener;
@ -65,6 +66,22 @@ public class BookmarksPanel extends HomeFragment {
mList = (BookmarksListView) view.findViewById(R.id.bookmarks_list);
mList.setContextMenuInfoFactory(new HomeListView.ContextMenuInfoFactory() {
@Override
public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) {
final int type = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks.TYPE));
if (type == Bookmarks.TYPE_FOLDER) {
// We don't show a context menu for folders
return null;
}
final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id);
info.url = cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.URL));
info.title = cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.TITLE));
info.bookmarkId = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks._ID));
return info;
}
});
return view;
}

View File

@ -97,13 +97,13 @@ public class HomeBanner extends LinearLayout
GeckoAppShell.getEventDispatcher().unregisterEventListener("HomeBanner:Data", this);
}
public void showBanner() {
public void show() {
if (!mDismissed) {
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HomeBanner:Get", null));
}
}
public void hideBanner() {
public void hide() {
animateDown();
}

View File

@ -0,0 +1,51 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.home;
import org.mozilla.gecko.db.BrowserContract.Combined;
import org.mozilla.gecko.util.StringUtils;
import android.text.TextUtils;
import android.view.View;
import android.widget.AdapterView.AdapterContextMenuInfo;
/**
* A ContextMenuInfo for HomeListView
*/
public class HomeContextMenuInfo extends AdapterContextMenuInfo {
public String url;
public String title;
public boolean isFolder = false;
public boolean inReadingList = false;
public int display = Combined.DISPLAY_NORMAL;
public int historyId = -1;
public int bookmarkId = -1;
public HomeContextMenuInfo(View targetView, int position, long id) {
super(targetView, position, id);
}
public boolean hasBookmarkId() {
return bookmarkId > -1;
}
public boolean hasHistoryId() {
return historyId > -1;
}
public boolean isInReadingList() {
return inReadingList;
}
public String getDisplayTitle() {
if (!TextUtils.isEmpty(title)) {
return title;
}
return StringUtils.stripCommonSubdomains(StringUtils.stripScheme(url, StringUtils.UrlFlags.STRIP_HTTPS));
}
}

View File

@ -15,7 +15,7 @@ import org.mozilla.gecko.ReaderModeUtils;
import org.mozilla.gecko.Tabs;
import org.mozilla.gecko.db.BrowserContract.Combined;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.home.HomeListView.HomeContextMenuInfo;
import org.mozilla.gecko.home.HomeContextMenuInfo;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.util.UiAsyncTask;
@ -89,12 +89,12 @@ abstract class HomeFragment extends Fragment {
// Hide the "Edit" menuitem if this item isn't a bookmark,
// or if this is a reading list item.
if (info.bookmarkId < 0 || info.inReadingList) {
if (!info.hasBookmarkId() || info.isInReadingList()) {
menu.findItem(R.id.home_edit_bookmark).setVisible(false);
}
// Hide the "Remove" menuitem if this item doesn't have a bookmark or history ID.
if (info.bookmarkId < 0 && info.historyId < 0) {
if (!info.hasBookmarkId() && !info.hasHistoryId()) {
menu.findItem(R.id.home_remove).setVisible(false);
}
@ -152,7 +152,7 @@ abstract class HomeFragment extends Fragment {
if (item.getItemId() == R.id.home_open_private_tab)
flags |= Tabs.LOADURL_PRIVATE;
final String url = (info.inReadingList ? ReaderModeUtils.getAboutReaderForUrl(info.url) : info.url);
final String url = (info.isInReadingList() ? ReaderModeUtils.getAboutReaderForUrl(info.url) : info.url);
Tabs.getInstance().loadUrl(url, flags);
Toast.makeText(context, R.string.new_tab_opened, Toast.LENGTH_SHORT).show();
return true;
@ -172,15 +172,13 @@ abstract class HomeFragment extends Fragment {
if (itemId == R.id.home_remove) {
// Prioritize removing a history entry over a bookmark in the case of a combined item.
final int historyId = info.historyId;
if (historyId > -1) {
new RemoveHistoryTask(context, historyId).execute();
if (info.hasHistoryId()) {
new RemoveHistoryTask(context, info.historyId).execute();
return true;
}
final int bookmarkId = info.bookmarkId;
if (bookmarkId > -1) {
new RemoveBookmarkTask(context, bookmarkId, info.url, info.inReadingList).execute();
if (info.hasBookmarkId()) {
new RemoveBookmarkTask(context, info.bookmarkId, info.url, info.isInReadingList()).execute();
return true;
}
}

View File

@ -6,22 +6,15 @@
package org.mozilla.gecko.home;
import org.mozilla.gecko.R;
import org.mozilla.gecko.db.BrowserContract.Bookmarks;
import org.mozilla.gecko.db.BrowserContract.Combined;
import org.mozilla.gecko.db.BrowserContract.URLColumns;
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
import org.mozilla.gecko.util.StringUtils;
import android.content.Context;
import android.content.res.TypedArray;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.MotionEvent;
import android.view.View;
import android.widget.AbsListView.LayoutParams;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemLongClickListener;
import android.widget.ListView;
@ -42,6 +35,9 @@ public class HomeListView extends ListView
// Top divider
private boolean mShowTopDivider;
// ContextMenuInfo maker
private ContextMenuInfoFactory mContextMenuInfoFactory;
public HomeListView(Context context) {
this(context, null);
}
@ -87,8 +83,14 @@ public class HomeListView extends ListView
// HomeListView could hold headers too. Add a context menu info only for its children.
if (item instanceof Cursor) {
Cursor cursor = (Cursor) item;
mContextMenuInfo = new HomeContextMenuInfo(view, position, id, cursor);
if (cursor == null || mContextMenuInfoFactory == null) {
mContextMenuInfo = null;
return false;
}
mContextMenuInfo = mContextMenuInfoFactory.makeInfoForCursor(view, position, id, cursor);
return showContextMenuForChild(HomeListView.this);
} else {
mContextMenuInfo = null;
return false;
@ -114,6 +116,10 @@ public class HomeListView extends ListView
});
}
public void setContextMenuInfoFactory(final ContextMenuInfoFactory factory) {
mContextMenuInfoFactory = factory;
}
public OnUrlOpenListener getOnUrlOpenListener() {
return mUrlOpenListener;
}
@ -122,84 +128,10 @@ public class HomeListView extends ListView
mUrlOpenListener = listener;
}
/**
* A ContextMenuInfo for HomeListView that adds details from the cursor.
/*
* Interface for creating ContextMenuInfo from cursors
*/
public static class HomeContextMenuInfo extends AdapterContextMenuInfo {
public int bookmarkId;
public int historyId;
public String url;
public String title;
public int display;
public boolean isFolder;
public boolean inReadingList;
/**
* This constructor assumes that the cursor was generated from a query
* to either the combined view or the bookmarks table.
*/
public HomeContextMenuInfo(View targetView, int position, long id, Cursor cursor) {
super(targetView, position, id);
if (cursor == null) {
return;
}
final int typeCol = cursor.getColumnIndex(Bookmarks.TYPE);
if (typeCol != -1) {
isFolder = (cursor.getInt(typeCol) == Bookmarks.TYPE_FOLDER);
} else {
isFolder = false;
}
// We don't show a context menu for folders, so return early.
if (isFolder) {
return;
}
url = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.URL));
title = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.TITLE));
final int bookmarkIdCol = cursor.getColumnIndex(Combined.BOOKMARK_ID);
if (bookmarkIdCol == -1) {
// If there isn't a bookmark ID column, this must be a bookmarks cursor,
// so the regular ID column will correspond to a bookmark ID.
bookmarkId = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks._ID));
} else if (cursor.isNull(bookmarkIdCol)) {
// If this is a combined cursor, we may get a history item without a
// bookmark, in which case the bookmarks ID column value will be null.
bookmarkId = -1;
} else {
bookmarkId = cursor.getInt(bookmarkIdCol);
}
final int historyIdCol = cursor.getColumnIndex(Combined.HISTORY_ID);
if (historyIdCol != -1) {
historyId = cursor.getInt(historyIdCol);
} else {
historyId = -1;
}
// We only have the parent column in cursors from getBookmarksInFolder.
final int parentCol = cursor.getColumnIndex(Bookmarks.PARENT);
if (parentCol != -1) {
inReadingList = (cursor.getInt(parentCol) == Bookmarks.FIXED_READING_LIST_ID);
} else {
inReadingList = false;
}
final int displayCol = cursor.getColumnIndex(Combined.DISPLAY);
if (displayCol != -1) {
display = cursor.getInt(displayCol);
} else {
display = Combined.DISPLAY_NORMAL;
}
}
public String getDisplayTitle() {
return TextUtils.isEmpty(title) ?
StringUtils.stripCommonSubdomains(StringUtils.stripScheme(url, StringUtils.UrlFlags.STRIP_HTTPS)) : title;
}
public interface ContextMenuInfoFactory {
public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor);
}
}

View File

@ -288,9 +288,9 @@ public class HomePager extends ViewPager {
}
if (mHomeBanner != null) {
if (item == mDefaultPanelIndex) {
mHomeBanner.showBanner();
mHomeBanner.show();
} else {
mHomeBanner.hideBanner();
mHomeBanner.hide();
}
}
}
@ -315,6 +315,14 @@ public class HomePager extends ViewPager {
return super.dispatchTouchEvent(event);
}
public void onToolbarFocusChange(boolean hasFocus) {
if (hasFocus) {
mHomeBanner.hide();
} else if (mDefaultPanelIndex == getCurrentItem() || getAdapter().getCount() == 0) {
mHomeBanner.show();
}
}
private void updateUiFromPanelConfigs(List<PanelConfig> panelConfigs) {
// We only care about the adapter if HomePager is currently
// loaded, which means it's visible in the activity.
@ -396,9 +404,9 @@ public class HomePager extends ViewPager {
if (mHomeBanner != null) {
if (position == mDefaultPanelIndex) {
mHomeBanner.showBanner();
mHomeBanner.show();
} else {
mHomeBanner.hideBanner();
mHomeBanner.hide();
}
}
}

View File

@ -47,7 +47,7 @@ public class LastTabsPanel extends HomeFragment {
private LastTabsAdapter mAdapter;
// The view shown by the fragment.
private ListView mList;
private HomeListView mList;
// The title for this HomeFragment panel.
private TextView mTitle;
@ -103,7 +103,7 @@ public class LastTabsPanel extends HomeFragment {
mTitle.setText(R.string.home_last_tabs_title);
}
mList = (ListView) view.findViewById(R.id.list);
mList = (HomeListView) view.findViewById(R.id.list);
mList.setTag(HomePager.LIST_TAG_LAST_TABS);
mList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@ -119,6 +119,16 @@ public class LastTabsPanel extends HomeFragment {
}
});
mList.setContextMenuInfoFactory(new HomeListView.ContextMenuInfoFactory() {
@Override
public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) {
final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id);
info.url = cursor.getString(cursor.getColumnIndexOrThrow(Combined.URL));
info.title = cursor.getString(cursor.getColumnIndexOrThrow(Combined.TITLE));
return info;
}
});
registerForContextMenu(mList);
mRestoreButton = view.findViewById(R.id.open_all_tabs_button);

View File

@ -6,7 +6,9 @@
package org.mozilla.gecko.home;
import org.mozilla.gecko.R;
import org.mozilla.gecko.db.BrowserContract.Combined;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.db.BrowserContract.Bookmarks;
import org.mozilla.gecko.db.BrowserDB.URLColumns;
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
import org.mozilla.gecko.home.TwoLinePageRow;
@ -46,7 +48,7 @@ public class MostRecentPanel extends HomeFragment {
private MostRecentAdapter mAdapter;
// The view shown by the fragment.
private ListView mList;
private HomeListView mList;
// Reference to the View to display when there are no results.
private View mEmptyView;
@ -90,7 +92,7 @@ public class MostRecentPanel extends HomeFragment {
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
mList = (ListView) view.findViewById(R.id.list);
mList = (HomeListView) view.findViewById(R.id.list);
mList.setTag(HomePager.LIST_TAG_MOST_RECENT);
mList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@ -105,6 +107,25 @@ public class MostRecentPanel extends HomeFragment {
}
});
mList.setContextMenuInfoFactory(new HomeListView.ContextMenuInfoFactory() {
@Override
public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) {
final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id);
info.url = cursor.getString(cursor.getColumnIndexOrThrow(Combined.URL));
info.title = cursor.getString(cursor.getColumnIndexOrThrow(Combined.TITLE));
info.display = cursor.getInt(cursor.getColumnIndexOrThrow(Combined.DISPLAY));
info.historyId = cursor.getInt(cursor.getColumnIndexOrThrow(Combined.HISTORY_ID));
final int bookmarkIdCol = cursor.getColumnIndexOrThrow(Combined.BOOKMARK_ID);
if (cursor.isNull(bookmarkIdCol)) {
// If this is a combined cursor, we may get a history item without a
// bookmark, in which case the bookmarks ID column value will be null.
info.bookmarkId = -1;
} else {
info.bookmarkId = cursor.getInt(bookmarkIdCol);
}
return info;
}
});
registerForContextMenu(mList);
}

View File

@ -37,7 +37,7 @@ public class PanelGridItemView extends FrameLayout {
}
public PanelGridItemView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
this(context, attrs, R.attr.panelGridItemViewStyle);
}
public PanelGridItemView(Context context, AttributeSet attrs, int defStyle) {
@ -45,7 +45,6 @@ public class PanelGridItemView extends FrameLayout {
LayoutInflater.from(context).inflate(R.layout.panel_grid_item_view, this);
mThumbnailView = (ImageView) findViewById(R.id.image);
mThumbnailView.setBackgroundColor(Color.rgb(255, 148, 0));
}
public void updateFromCursor(Cursor cursor) { }

View File

@ -23,7 +23,7 @@ public class PanelGridView extends GridView implements DatasetBacked {
private final PanelGridViewAdapter mAdapter;
public PanelGridView(Context context, ViewConfig viewConfig) {
super(context, null, R.attr.homeGridViewStyle);
super(context, null, R.attr.panelGridViewStyle);
mAdapter = new PanelGridViewAdapter(context);
setAdapter(mAdapter);
setNumColumns(AUTO_FIT);

View File

@ -45,7 +45,7 @@ public class ReadingListPanel extends HomeFragment {
private ReadingListAdapter mAdapter;
// The view shown by the fragment
private ListView mList;
private HomeListView mList;
// Reference to the View to display when there are no results.
private View mEmptyView;
@ -89,7 +89,7 @@ public class ReadingListPanel extends HomeFragment {
mTopView = view;
mList = (ListView) view.findViewById(R.id.list);
mList = (HomeListView) view.findViewById(R.id.list);
mList.setTag(HomePager.LIST_TAG_READING_LIST);
mList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@ -108,6 +108,17 @@ public class ReadingListPanel extends HomeFragment {
}
});
mList.setContextMenuInfoFactory(new HomeListView.ContextMenuInfoFactory() {
@Override
public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) {
final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id);
info.url = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.URL));
info.title = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.TITLE));
info.bookmarkId = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks._ID));
info.inReadingList = true;
return info;
}
});
registerForContextMenu(mList);
}

View File

@ -18,7 +18,6 @@ import org.mozilla.gecko.db.BrowserDB.TopSitesCursorWrapper;
import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.gfx.BitmapUtils;
import org.mozilla.gecko.home.HomeListView.HomeContextMenuInfo;
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
import org.mozilla.gecko.home.PinSiteDialog.OnSiteSelectedListener;
import org.mozilla.gecko.home.TopSitesGridView.OnEditPinnedSiteListener;

View File

@ -221,6 +221,7 @@ gbjar.sources += [
'home/HomeConfig.java',
'home/HomeConfigLoader.java',
'home/HomeConfigPrefsBackend.java',
'home/HomeContextMenuInfo.java',
'home/HomeFragment.java',
'home/HomeListView.java',
'home/HomePager.java',
@ -362,6 +363,7 @@ gbjar.sources += [
'widget/GeckoActionProvider.java',
'widget/GeckoPopupMenu.java',
'widget/IconTabWidget.java',
'widget/SquaredImageView.java',
'widget/TabRow.java',
'widget/ThumbnailView.java',
'widget/TwoWayView.java',

View File

@ -6,9 +6,7 @@
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:gecko="http://schemas.android.com/apk/res-auto">
<ImageView android:id="@+id/image"
android:layout_width="fill_parent"
android:layout_height="80dp"
android:layout_marginRight="5dp" />
<org.mozilla.gecko.widget.SquaredImageView android:id="@+id/image"
style="@style/Widget.PanelGridItemImageView" />
</merge>

View File

@ -47,7 +47,8 @@
<item name="bookmarksListViewStyle">@style/Widget.BookmarksListView</item>
<item name="topSitesGridItemViewStyle">@style/Widget.TopSitesGridItemView</item>
<item name="topSitesGridViewStyle">@style/Widget.TopSitesGridView</item>
<item name="homeGridViewStyle">@style/Widget.HomeGridView</item>
<item name="panelGridViewStyle">@style/Widget.PanelGridView</item>
<item name="panelGridItemViewStyle">@style/Widget.PanelGridItemView</item>
<item name="topSitesThumbnailViewStyle">@style/Widget.TopSitesThumbnailView</item>
<item name="homeListViewStyle">@style/Widget.HomeListView</item>
<item name="geckoMenuListViewStyle">@style/Widget.GeckoMenuListView</item>

View File

@ -12,5 +12,6 @@
<dimen name="tabs_panel_indicator_width">60dp</dimen>
<dimen name="tabs_panel_list_padding">8dip</dimen>
<dimen name="history_tab_widget_width">270dp</dimen>
<dimen name="panel_grid_view_column_width">250dp</dimen>
</resources>

View File

@ -38,6 +38,12 @@
<!-- Default style for the HomeGridView -->
<attr name="homeGridViewStyle" format="reference" />
<!-- Style for the PanelGridView -->
<attr name="panelGridViewStyle" format="reference" />
<!-- Default style for the PanelGridItemView -->
<attr name="panelGridItemViewStyle" format="reference" />
<!-- Default style for the TopSitesGridView -->
<attr name="topSitesGridViewStyle" format="reference" />

View File

@ -90,5 +90,6 @@
<color name="home_last_tab_bar_bg">#FFF5F7F9</color>
<color name="panel_grid_item_image_background">#D1D9E1</color>
</resources>

View File

@ -101,4 +101,7 @@
<!-- Icon Grid -->
<dimen name="icongrid_columnwidth">128dp</dimen>
<dimen name="icongrid_padding">16dp</dimen>
<!-- PanelGridView dimensions -->
<dimen name="panel_grid_view_column_width">180dp</dimen>
</resources>

View File

@ -145,6 +145,29 @@
<item name="android:orientation">vertical</item>
</style>
<style name="Widget.PanelGridView" parent="Widget.GridView">
<item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">fill_parent</item>
<item name="android:paddingTop">0dp</item>
<item name="android:stretchMode">columnWidth</item>
<item name="android:columnWidth">@dimen/panel_grid_view_column_width</item>
<item name="android:horizontalSpacing">2dp</item>
<item name="android:verticalSpacing">2dp</item>
</style>
<style name="Widget.PanelGridItemView">
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_width">wrap_content</item>
</style>
<style name="Widget.PanelGridItemImageView">
<item name="android:layout_height">@dimen/panel_grid_view_column_width</item>
<item name="android:layout_width">fill_parent</item>
<item name="android:scaleType">centerCrop</item>
<item name="android:adjustViewBounds">true</item>
<item name="android:background">@color/panel_grid_item_image_background</item>
</style>
<style name="Widget.BookmarkItemView" parent="Widget.TwoLineRow"/>
<style name="Widget.BookmarksListView" parent="Widget.HomeListView"/>

View File

@ -80,7 +80,8 @@
<item name="bookmarksListViewStyle">@style/Widget.BookmarksListView</item>
<item name="topSitesGridItemViewStyle">@style/Widget.TopSitesGridItemView</item>
<item name="topSitesGridViewStyle">@style/Widget.TopSitesGridView</item>
<item name="homeGridViewStyle">@style/Widget.HomeGridView</item>
<item name="panelGridViewStyle">@style/Widget.PanelGridView</item>
<item name="panelGridItemViewStyle">@style/Widget.PanelGridItemView</item>
<item name="topSitesThumbnailViewStyle">@style/Widget.TopSitesThumbnailView</item>
<item name="homeListViewStyle">@style/Widget.HomeListView</item>
<item name="geckoMenuListViewStyle">@style/Widget.GeckoMenuListView</item>

View File

@ -142,6 +142,7 @@ public class BrowserToolbar extends GeckoRelativeLayout
private OnFilterListener mFilterListener;
private OnStartEditingListener mStartEditingListener;
private OnStopEditingListener mStopEditingListener;
private OnFocusChangeListener mFocusChangeListener;
final private BrowserApp mActivity;
private boolean mHasSoftMenuButton;
@ -315,6 +316,9 @@ public class BrowserToolbar extends GeckoRelativeLayout
@Override
public void onFocusChange(View v, boolean hasFocus) {
setSelected(hasFocus);
if (mFocusChangeListener != null) {
mFocusChangeListener.onFocusChange(v, hasFocus);
}
}
});
@ -793,6 +797,10 @@ public class BrowserToolbar extends GeckoRelativeLayout
mStopEditingListener = listener;
}
public void setOnFocusChangeListener(OnFocusChangeListener listener) {
mFocusChangeListener = listener;
}
private void showUrlEditLayout() {
setUrlEditLayoutVisibility(true, null);
}

View File

@ -0,0 +1,21 @@
package org.mozilla.gecko.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.ImageView;
final class SquaredImageView extends ImageView {
public SquaredImageView(Context context) {
super(context);
}
public SquaredImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(getMeasuredWidth(), getMeasuredWidth());
}
}

View File

@ -95,17 +95,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "WebappManager",
});
});
// Lazily-loaded browser scripts that use observer notifcations:
var LazyNotificationGetter = {
observers: [],
shutdown: function lng_shutdown() {
this.observers.forEach(function(o) {
Services.obs.removeObserver(o, o.notification);
});
this.observers = [];
}
};
[
#ifdef MOZ_WEBRTC
["WebrtcUI", ["getUserMedia:request", "recording-device-events"], "chrome://browser/content/WebrtcUI.js"],
@ -125,14 +114,9 @@ var LazyNotificationGetter = {
return sandbox[name];
});
notifications.forEach(function (aNotification) {
let o = {
notification: aNotification,
observe: function(s, t, d) {
window[name].observe(s, t, d);
}
};
Services.obs.addObserver(o, aNotification, false);
LazyNotificationGetter.observers.push(o);
Services.obs.addObserver(function(s, t, d) {
window[name].observe(s, t, d)
}, aNotification, false);
});
});
@ -143,12 +127,9 @@ var LazyNotificationGetter = {
let [name, notifications, resource] = module;
XPCOMUtils.defineLazyModuleGetter(this, name, resource);
notifications.forEach(notification => {
let o = {
notification: notification,
observe: (s, t, d) => this[name].observe(s, t, d)
};
Services.obs.addObserver(o, notification, false);
LazyNotificationGetter.observers.push(o);
Services.obs.addObserver((s,t,d) => {
this[name].observe(s,t,d)
}, notification, false);
});
});

View File

@ -3,6 +3,7 @@
# You can obtain one at http://mozilla.org/MPL/2.0/.
modules := \
hawk.js \
storageservice.js \
stringbundle.js \
tokenserverclient.js \

201
services/common/hawk.js Normal file
View File

@ -0,0 +1,201 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/*
* HAWK is an HTTP authentication scheme using a message authentication code
* (MAC) algorithm to provide partial HTTP request cryptographic verification.
*
* For details, see: https://github.com/hueniverse/hawk
*
* With HAWK, it is essential that the clocks on clients and server not have an
* absolute delta of greater than one minute, as the HAWK protocol uses
* timestamps to reduce the possibility of replay attacks. However, it is
* likely that some clients' clocks will be more than a little off, especially
* in mobile devices, which would break HAWK-based services (like sync and
* firefox accounts) for those clients.
*
* This library provides a stateful HAWK client that calculates (roughly) the
* clock delta on the client vs the server. The library provides an interface
* for deriving HAWK credentials and making HAWK-authenticated REST requests to
* a single remote server. Therefore, callers who want to interact with
* multiple HAWK services should instantiate one HawkClient per service.
*/
this.EXPORTED_SYMBOLS = ["HawkClient"];
const {interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/FxAccountsCommon.js");
Cu.import("resource://services-common/utils.js");
Cu.import("resource://services-crypto/utils.js");
Cu.import("resource://services-common/rest.js");
Cu.import("resource://gre/modules/Promise.jsm");
/*
* A general purpose client for making HAWK authenticated requests to a single
* host. Keeps track of the clock offset between the client and the host for
* computation of the timestamp in the HAWK Authorization header.
*
* Clients should create one HawkClient object per each server they wish to
* interact with.
*
* @param host
* The url of the host
*/
function HawkClient(host) {
this.host = host;
// Clock offset in milliseconds between our client's clock and the date
// reported in responses from our host.
this._localtimeOffsetMsec = 0;
}
HawkClient.prototype = {
/*
* Construct an error message for a response. Private.
*
* @param restResponse
* A RESTResponse object from a RESTRequest
*
* @param errorString
* A string describing the error
*/
_constructError: function(restResponse, errorString) {
return {
error: errorString,
message: restResponse.statusText,
code: restResponse.status,
errno: restResponse.status
};
},
/*
*
* Update clock offset by determining difference from date gives in the (RFC
* 1123) Date header of a server response. Because HAWK tolerates a window
* of one minute of clock skew (so two minutes total since the skew can be
* positive or negative), the simple method of calculating offset here is
* probably good enough. We keep the value in milliseconds to make life
* easier, even though the value will not have millisecond accuracy.
*
* @param dateString
* An RFC 1123 date string (e.g., "Mon, 13 Jan 2014 21:45:06 GMT")
*
* For HAWK clock skew and replay protection, see
* https://github.com/hueniverse/hawk#replay-protection
*/
_updateClockOffset: function(dateString) {
try {
let serverDateMsec = Date.parse(dateString);
this._localtimeOffsetMsec = serverDateMsec - this.now();
log.debug("Clock offset vs " + this.host + ": " + this._localtimeOffsetMsec);
} catch(err) {
log.warn("Bad date header in server response: " + dateString);
}
},
/*
* Get the current clock offset in milliseconds.
*
* The offset is the number of milliseconds that must be added to the client
* clock to make it equal to the server clock. For example, if the client is
* five minutes ahead of the server, the localtimeOffsetMsec will be -300000.
*/
get localtimeOffsetMsec() {
return this._localtimeOffsetMsec;
},
/*
* return current time in milliseconds
*/
now: function() {
return Date.now();
},
/* A general method for sending raw RESTRequest calls authorized using HAWK
*
* @param path
* API endpoint path
* @param method
* The HTTP request method
* @param credentials
* Hawk credentials
* @param payloadObj
* An object that can be encodable as JSON as the payload of the
* request
* @return Promise
* Returns a promise that resolves to the text response of the API call,
* or is rejected with an error. If the server response can be parsed
* as JSON and contains an 'error' property, the promise will be
* rejected with this JSON-parsed response.
*/
request: function(path, method, credentials=null, payloadObj={}, retryOK=true) {
method = method.toLowerCase();
let deferred = Promise.defer();
let uri = this.host + path;
let self = this;
function onComplete(error) {
let restResponse = this.response;
let status = restResponse.status;
log.debug("(Response) code: " + status +
" - Status text: " + restResponse.statusText,
" - Response text: " + restResponse.body);
if (error) {
// When things really blow up, reconstruct an error object that follows
// the general format of the server on error responses.
return deferred.reject(self._constructError(restResponse, error));
}
self._updateClockOffset(restResponse.headers["date"]);
if (status === 401 && retryOK) {
// Retry once if we were rejected due to a bad timestamp.
// Clock offset is adjusted already in the top of this function.
log.debug("Received 401 for " + path + ": retrying");
return deferred.resolve(
self.request(path, method, credentials, payloadObj, false));
}
// If the server returned a json error message, use it in the rejection
// of the promise.
//
// In the case of a 401, in which we are probably being rejected for a
// bad timestamp, retry exactly once, during which time clock offset will
// be adjusted.
let jsonResponse = {};
try {
jsonResponse = JSON.parse(restResponse.body);
} catch(notJSON) {}
let okResponse = (200 <= status && status < 300);
if (!okResponse || jsonResponse.error) {
if (jsonResponse.error) {
return deferred.reject(jsonResponse);
}
return deferred.reject(self._constructError(restResponse, "Request failed"));
}
// It's up to the caller to know how to decode the response.
// We just return the raw text.
deferred.resolve(this.response.body);
};
let extra = {
now: this.now(),
localtimeOffsetMsec: this.localtimeOffsetMsec,
};
let request = new HAWKAuthenticatedRESTRequest(uri, credentials, extra);
request[method](payloadObj, onComplete);
return deferred.promise;
}
}

View File

@ -9,7 +9,8 @@ const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
this.EXPORTED_SYMBOLS = [
"RESTRequest",
"RESTResponse",
"TokenAuthenticatedRESTRequest"
"TokenAuthenticatedRESTRequest",
"HAWKAuthenticatedRESTRequest",
];
#endif
@ -146,6 +147,11 @@ RESTRequest.prototype = {
COMPLETED: 4,
ABORTED: 8,
/**
* HTTP status text of response
*/
statusText: null,
/**
* Request timeout (in seconds, though decimal values can be used for
* up to millisecond granularity.)
@ -612,8 +618,7 @@ RESTResponse.prototype = {
get status() {
let status;
try {
let channel = this.request.channel.QueryInterface(Ci.nsIHttpChannel);
status = channel.responseStatus;
status = this.request.channel.responseStatus;
} catch (ex) {
this._log.debug("Caught exception fetching HTTP status code:" +
CommonUtils.exceptionStr(ex));
@ -623,14 +628,29 @@ RESTResponse.prototype = {
return this.status = status;
},
/**
* HTTP status text
*/
get statusText() {
let statusText;
try {
statusText = this.request.channel.responseStatusText;
} catch (ex) {
this._log.debug("Caught exception fetching HTTP status text:" +
CommonUtils.exceptionStr(ex));
return null;
}
delete this.statusText;
return this.statusText = statusText;
},
/**
* Boolean flag that indicates whether the HTTP status code is 2xx or not.
*/
get success() {
let success;
try {
let channel = this.request.channel.QueryInterface(Ci.nsIHttpChannel);
success = channel.requestSucceeded;
success = this.request.channel.requestSucceeded;
} catch (ex) {
this._log.debug("Caught exception fetching HTTP success flag:" +
CommonUtils.exceptionStr(ex));
@ -704,3 +724,60 @@ TokenAuthenticatedRESTRequest.prototype = {
);
},
};
/**
* Single-use HAWK-authenticated HTTP requests to RESTish resources.
*
* @param uri
* (String) URI for the RESTRequest constructor
*
* @param credentials
* (Object) Optional credentials for computing HAWK authentication
* header.
*
* @param payloadObj
* (Object) Optional object to be converted to JSON payload
*
* @param extra
* (Object) Optional extra params for HAWK header computation.
* Valid properties are:
*
* now: <current time in milliseconds>,
* localtimeOffsetMsec: <local clock offset vs server>
*
* extra.localtimeOffsetMsec is the value in milliseconds that must be added to
* the local clock to make it agree with the server's clock. For instance, if
* the local clock is two minutes ahead of the server, the time offset in
* milliseconds will be -120000.
*/
this.HAWKAuthenticatedRESTRequest =
function HawkAuthenticatedRESTRequest(uri, credentials, extra={}) {
RESTRequest.call(this, uri);
this.credentials = credentials;
this.now = extra.now || Date.now();
this.localtimeOffsetMsec = extra.localtimeOffsetMsec || 0;
this._log.trace("local time, offset: " + this.now + ", " + (this.localtimeOffsetMsec));
};
HAWKAuthenticatedRESTRequest.prototype = {
__proto__: RESTRequest.prototype,
dispatch: function dispatch(method, data, onComplete, onProgress) {
if (this.credentials) {
let options = {
now: this.now,
localtimeOffsetMsec: this.localtimeOffsetMsec,
credentials: this.credentials,
payload: data && JSON.stringify(data) || "",
contentType: "application/json; charset=utf-8",
};
let header = CryptoUtils.computeHAWK(this.uri, method, options);
this.setHeader("Authorization", header.field);
this._log.trace("hawk auth header: " + header.field);
}
return RESTRequest.prototype.dispatch.call(
this, method, data, onComplete, onProgress
);
}
};

View File

@ -0,0 +1,485 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://services-common/hawk.js");
const SECOND_MS = 1000;
const MINUTE_MS = SECOND_MS * 60;
const HOUR_MS = MINUTE_MS * 60;
const TEST_CREDS = {
id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
algorithm: "sha256"
};
initTestLogging("Trace");
add_task(function test_now() {
let client = new HawkClient("https://example.com");
do_check_true(client.now() - Date.now() < SECOND_MS);
run_next_test();
});
add_task(function test_updateClockOffset() {
let client = new HawkClient("https://example.com");
let now = new Date();
let serverDate = now.toUTCString();
// Client's clock is off
client.now = () => { return now.valueOf() + HOUR_MS; }
client._updateClockOffset(serverDate);
// Check that they're close; there will likely be a one-second rounding
// error, so checking strict equality will likely fail.
//
// localtimeOffsetMsec is how many milliseconds to add to the local clock so
// that it agrees with the server. We are one hour ahead of the server, so
// our offset should be -1 hour.
do_check_true(Math.abs(client.localtimeOffsetMsec + HOUR_MS) <= SECOND_MS);
run_next_test();
});
add_task(function test_authenticated_get_request() {
let message = "{\"msg\": \"Great Success!\"}";
let method = "GET";
let server = httpd_setup({"/foo": (request, response) => {
do_check_true(request.hasHeader("Authorization"));
response.setStatusLine(request.httpVersion, 200, "OK");
response.bodyOutputStream.write(message, message.length);
}
});
let client = new HawkClient(server.baseURI);
let response = yield client.request("/foo", method, TEST_CREDS);
let result = JSON.parse(response);
do_check_eq("Great Success!", result.msg);
yield deferredStop(server);
});
add_task(function test_authenticated_post_request() {
let method = "POST";
let server = httpd_setup({"/foo": (request, response) => {
do_check_true(request.hasHeader("Authorization"));
response.setStatusLine(request.httpVersion, 200, "OK");
response.setHeader("Content-Type", "application/json");
response.bodyOutputStream.writeFrom(request.bodyInputStream, request.bodyInputStream.available());
}
});
let client = new HawkClient(server.baseURI);
let response = yield client.request("/foo", method, TEST_CREDS, {foo: "bar"});
let result = JSON.parse(response);
do_check_eq("bar", result.foo);
yield deferredStop(server);
});
add_task(function test_credentials_optional() {
let method = "GET";
let server = httpd_setup({
"/foo": (request, response) => {
do_check_false(request.hasHeader("Authorization"));
let message = JSON.stringify({msg: "you're in the friend zone"});
response.setStatusLine(request.httpVersion, 200, "OK");
response.setHeader("Content-Type", "application/json");
response.bodyOutputStream.write(message, message.length);
}
});
let client = new HawkClient(server.baseURI);
let result = yield client.request("/foo", method); // credentials undefined
do_check_eq(JSON.parse(result).msg, "you're in the friend zone");
yield deferredStop(server);
});
add_task(function test_server_error() {
let message = "Ohai!";
let method = "GET";
let server = httpd_setup({"/foo": (request, response) => {
response.setStatusLine(request.httpVersion, 418, "I am a Teapot");
response.bodyOutputStream.write(message, message.length);
}
});
let client = new HawkClient(server.baseURI);
try {
yield client.request("/foo", method, TEST_CREDS);
} catch(err) {
do_check_eq(418, err.code);
do_check_eq("I am a Teapot", err.message);
}
yield deferredStop(server);
});
add_task(function test_server_error_json() {
let message = JSON.stringify({error: "Cannot get ye flask."});
let method = "GET";
let server = httpd_setup({"/foo": (request, response) => {
response.setStatusLine(request.httpVersion, 400, "What wouldst thou deau?");
response.bodyOutputStream.write(message, message.length);
}
});
let client = new HawkClient(server.baseURI);
try {
yield client.request("/foo", method, TEST_CREDS);
} catch(err) {
do_check_eq("Cannot get ye flask.", err.error);
}
yield deferredStop(server);
});
add_task(function test_offset_after_request() {
let message = "Ohai!";
let method = "GET";
let server = httpd_setup({"/foo": (request, response) => {
response.setStatusLine(request.httpVersion, 200, "OK");
response.bodyOutputStream.write(message, message.length);
}
});
let client = new HawkClient(server.baseURI);
let now = Date.now();
client.now = () => { return now + HOUR_MS; };
do_check_eq(client.localtimeOffsetMsec, 0);
let response = yield client.request("/foo", method, TEST_CREDS);
// Should be about an hour off
do_check_true(Math.abs(client.localtimeOffsetMsec + HOUR_MS) < SECOND_MS);
yield deferredStop(server);
});
add_task(function test_offset_in_hawk_header() {
let message = "Ohai!";
let method = "GET";
let server = httpd_setup({
"/first": function(request, response) {
response.setStatusLine(request.httpVersion, 200, "OK");
response.bodyOutputStream.write(message, message.length);
},
"/second": function(request, response) {
// We see a better date now in the ts component of the header
let delta = getTimestampDelta(request.getHeader("Authorization"));
let message = "Delta: " + delta;
// We're now within HAWK's one-minute window.
// I hope this isn't a recipe for intermittent oranges ...
if (delta < MINUTE_MS) {
response.setStatusLine(request.httpVersion, 200, "OK");
} else {
response.setStatusLine(request.httpVersion, 400, "Delta: " + delta);
}
response.bodyOutputStream.write(message, message.length);
}
});
let client = new HawkClient(server.baseURI);
function getOffset() {
return client.localtimeOffsetMsec;
}
client.now = () => {
return Date.now() + 12 * HOUR_MS;
};
// We begin with no offset
do_check_eq(client.localtimeOffsetMsec, 0);
yield client.request("/first", method, TEST_CREDS);
// After the first server response, our offset is updated to -12 hours.
// We should be safely in the window, now.
do_check_true(Math.abs(client.localtimeOffsetMsec + 12 * HOUR_MS) < MINUTE_MS);
yield client.request("/second", method, TEST_CREDS);
yield deferredStop(server);
});
add_task(function test_2xx_success() {
// Just to ensure that we're not biased toward 200 OK for success
let credentials = {
id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
algorithm: "sha256"
};
let method = "GET";
let server = httpd_setup({"/foo": (request, response) => {
response.setStatusLine(request.httpVersion, 202, "Accepted");
}
});
let client = new HawkClient(server.baseURI);
let response = yield client.request("/foo", method, credentials);
// Shouldn't be any content in a 202
do_check_eq(response, "");
yield deferredStop(server);
});
add_task(function test_retry_request_on_fail() {
let attempts = 0;
let credentials = {
id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
algorithm: "sha256"
};
let method = "GET";
let server = httpd_setup({
"/maybe": function(request, response) {
// This path should be hit exactly twice; once with a bad timestamp, and
// again when the client retries the request with a corrected timestamp.
attempts += 1;
do_check_true(attempts <= 2);
let delta = getTimestampDelta(request.getHeader("Authorization"));
// First time through, we should have a bad timestamp
if (attempts === 1) {
do_check_true(delta > MINUTE_MS);
let message = "never!!!";
response.setStatusLine(request.httpVersion, 401, "Unauthorized");
return response.bodyOutputStream.write(message, message.length);
}
// Second time through, timestamp should be corrected by client
do_check_true(delta < MINUTE_MS);
let message = "i love you!!!";
response.setStatusLine(request.httpVersion, 200, "OK");
response.bodyOutputStream.write(message, message.length);
}
});
let client = new HawkClient(server.baseURI);
function getOffset() {
return client.localtimeOffsetMsec;
}
client.now = () => {
return Date.now() + 12 * HOUR_MS;
};
// We begin with no offset
do_check_eq(client.localtimeOffsetMsec, 0);
// Request will have bad timestamp; client will retry once
let response = yield client.request("/maybe", method, credentials);
do_check_eq(response, "i love you!!!");
yield deferredStop(server);
});
add_task(function test_multiple_401_retry_once() {
// Like test_retry_request_on_fail, but always return a 401
// and ensure that the client only retries once.
let attempts = 0;
let credentials = {
id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
algorithm: "sha256"
};
let method = "GET";
let server = httpd_setup({
"/maybe": function(request, response) {
// This path should be hit exactly twice; once with a bad timestamp, and
// again when the client retries the request with a corrected timestamp.
attempts += 1;
do_check_true(attempts <= 2);
let message = "never!!!";
response.setStatusLine(request.httpVersion, 401, "Unauthorized");
response.bodyOutputStream.write(message, message.length);
}
});
let client = new HawkClient(server.baseURI);
function getOffset() {
return client.localtimeOffsetMsec;
}
client.now = () => {
return Date.now() - 12 * HOUR_MS;
};
// We begin with no offset
do_check_eq(client.localtimeOffsetMsec, 0);
// Request will have bad timestamp; client will retry once
try {
yield client.request("/maybe", method, credentials);
} catch (err) {
do_check_eq(err.code, 401);
}
do_check_eq(attempts, 2);
yield deferredStop(server);
});
add_task(function test_500_no_retry() {
// If we get a 500 error, the client should not retry (as it would with a
// 401)
let attempts = 0;
let credentials = {
id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
algorithm: "sha256"
};
let method = "GET";
let server = httpd_setup({
"/no-shutup": function() {
attempts += 1;
let message = "Cannot get ye flask.";
response.setStatusLine(request.httpVersion, 500, "Internal server error");
response.bodyOutputStream.write(message, message.length);
}
});
let client = new HawkClient(server.baseURI);
function getOffset() {
return client.localtimeOffsetMsec;
}
// Throw off the clock so the HawkClient would want to retry the request if
// it could
client.now = () => {
return Date.now() - 12 * HOUR_MS;
};
// Request will 500; no retries
try {
yield client.request("/no-shutup", method, credentials);
} catch(err) {
do_check_eq(err.code, 500);
}
do_check_eq(attempts, 1);
yield deferredStop(server);
});
add_task(function test_401_then_500() {
// Like test_multiple_401_retry_once, but return a 500 to the
// second request, ensuring that the promise is properly rejected
// in client.request.
let attempts = 0;
let credentials = {
id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
algorithm: "sha256"
};
let method = "GET";
let server = httpd_setup({
"/maybe": function(request, response) {
// This path should be hit exactly twice; once with a bad timestamp, and
// again when the client retries the request with a corrected timestamp.
attempts += 1;
do_check_true(attempts <= 2);
let delta = getTimestampDelta(request.getHeader("Authorization"));
// First time through, we should have a bad timestamp
// Client will retry
if (attempts === 1) {
do_check_true(delta > MINUTE_MS);
let message = "never!!!";
response.setStatusLine(request.httpVersion, 401, "Unauthorized");
return response.bodyOutputStream.write(message, message.length);
}
// Second time through, timestamp should be corrected by client
// And fail on the client
do_check_true(delta < MINUTE_MS);
let message = "Cannot get ye flask.";
response.setStatusLine(request.httpVersion, 500, "Internal server error");
response.bodyOutputStream.write(message, message.length);
}
});
let client = new HawkClient(server.baseURI);
function getOffset() {
return client.localtimeOffsetMsec;
}
client.now = () => {
return Date.now() - 12 * HOUR_MS;
};
// We begin with no offset
do_check_eq(client.localtimeOffsetMsec, 0);
// Request will have bad timestamp; client will retry once
try {
yield client.request("/maybe", method, credentials);
} catch(err) {
do_check_eq(err.code, 500);
}
do_check_eq(attempts, 2);
yield deferredStop(server);
});
add_task(function throw_if_not_json_body() {
do_test_pending();
let client = new HawkClient("https://example.com");
try {
yield client.request("/bogus", "GET", {}, "I am not json");
} catch(err) {
do_check_true(!!err.message);
do_test_finished();
}
});
// End of tests.
// Utility functions follow
function getTimestampDelta(authHeader, now=Date.now()) {
let tsMS = new Date(
parseInt(/ts="(\d+)"/.exec(authHeader)[1], 10) * SECOND_MS);
return Math.abs(tsMS - now);
}
function deferredStop(server) {
let deferred = Promise.defer();
server.stop(deferred.resolve);
return deferred.promise;
}
function run_test() {
initTestLogging("Trace");
run_next_test();
}

View File

@ -831,3 +831,62 @@ add_test(function test_not_sending_cookie() {
server.stop(run_next_test);
});
});
add_test(function test_hawk_authenticated_request() {
do_test_pending();
let onProgressCalled = false;
let postData = {your: "data"};
// An arbitrary date - Feb 2, 1971. It ends in a bunch of zeroes to make our
// computation with the hawk timestamp easier, since hawk throws away the
// millisecond values.
let then = 34329600000;
let clockSkew = 120000;
let timeOffset = -1 * clockSkew;
let localTime = then + clockSkew;
let credentials = {
id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
algorithm: "sha256"
};
let server = httpd_setup({
"/elysium": function(request, response) {
do_check_true(request.hasHeader("Authorization"));
// check that the header timestamp is our arbitrary system date, not
// today's date. Note that hawk header timestamps are in seconds, not
// milliseconds.
let authorization = request.getHeader("Authorization");
let tsMS = parseInt(/ts="(\d+)"/.exec(authorization)[1], 10) * 1000;
do_check_eq(tsMS, then);
let message = "yay";
response.setStatusLine(request.httpVersion, 200, "OK");
response.bodyOutputStream.write(message, message.length);
}
});
function onProgress() {
onProgressCalled = true;
}
function onComplete(error) {
do_check_eq(200, this.response.status);
do_check_eq(this.response.body, "yay");
do_check_true(onProgressCalled);
do_test_finished();
server.stop(run_next_test);
}
let url = server.baseURI + "/elysium";
let extra = {
now: localTime,
localtimeOffsetMsec: timeOffset
};
let request = new HAWKAuthenticatedRESTRequest(url, credentials, extra);
request.post(postData, onComplete, onProgress);
});

View File

@ -25,6 +25,7 @@ firefox-appdir = browser
[test_async_querySpinningly.js]
[test_bagheera_server.js]
[test_bagheera_client.js]
[test_hawk.js]
[test_observers.js]
[test_restrequest.js]
[test_tokenauthenticatedrequest.js]

View File

@ -67,6 +67,26 @@ InternalMethods = function(mock) {
}
InternalMethods.prototype = {
/**
* Return the current time in milliseconds as an integer. Allows tests to
* manipulate the date to simulate certificate expiration.
*/
now: function() {
return this.fxAccountsClient.now();
},
/**
* Return clock offset in milliseconds, as reported by the fxAccountsClient.
* This can be overridden for testing.
*
* The offset is the number of milliseconds that must be added to the client
* clock to make it equal to the server clock. For example, if the client is
* five minutes ahead of the server, the localtimeOffsetMsec will be -300000.
*/
get localtimeOffsetMsec() {
return this.fxAccountsClient.localtimeOffsetMsec;
},
/**
* Ask the server whether the user's email has been verified
*/
@ -206,9 +226,13 @@ InternalMethods.prototype = {
log.debug("getAssertionFromCert");
let payload = {};
let d = Promise.defer();
let options = {
localtimeOffsetMsec: internal.localtimeOffsetMsec,
now: internal.now()
};
// "audience" should look like "http://123done.org".
// The generated assertion will expire in two minutes.
jwcrypto.generateAssertion(cert, keyPair, audience, function(err, signed) {
jwcrypto.generateAssertion(cert, keyPair, audience, options, (err, signed) => {
if (err) {
log.error("getAssertionFromCert: " + err);
d.reject(err);
@ -228,7 +252,7 @@ InternalMethods.prototype = {
return Promise.resolve(this.cert.cert);
}
// else get our cert signed
let willBeValidUntil = this.now() + CERT_LIFETIME;
let willBeValidUntil = internal.now() + CERT_LIFETIME;
return this.getCertificateSigned(data.sessionToken,
keyPair.serializedPublicKey,
CERT_LIFETIME)
@ -255,7 +279,7 @@ InternalMethods.prototype = {
return Promise.resolve(this.keyPair.keyPair);
}
// Otherwse, create a keypair and set validity limit.
let willBeValidUntil = this.now() + KEY_LIFETIME;
let willBeValidUntil = internal.now() + KEY_LIFETIME;
let d = Promise.defer();
jwcrypto.generateKeyPair("DS160", (err, kp) => {
if (err) {

View File

@ -6,9 +6,11 @@ this.EXPORTED_SYMBOLS = ["FxAccountsClient"];
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://services-common/utils.js");
Cu.import("resource://services-common/hawk.js");
Cu.import("resource://services-crypto/utils.js");
Cu.import("resource://gre/modules/FxAccountsCommon.js");
@ -19,11 +21,17 @@ try {
} catch(keepDefault) {}
const HOST = _host;
const PREFIX_NAME = "identity.mozilla.com/picl/v1/";
const XMLHttpRequest =
Components.Constructor("@mozilla.org/xmlextras/xmlhttprequest;1");
const PROTOCOL_VERSION = "identity.mozilla.com/picl/v1/";
function KW(context) {
// This is used as a salt. It's specified by the protocol. Note that the
// value of PROTOCOL_VERSION does not refer in any wy to the version of the
// Firefox Accounts API. For this reason, it is not exposed as a pref.
//
// See:
// https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol#creating-the-account
return PROTOCOL_VERSION + context;
}
function stringToHex(str) {
let encoder = new TextEncoder("utf-8");
@ -43,9 +51,37 @@ function bytesToHex(bytes) {
this.FxAccountsClient = function(host = HOST) {
this.host = host;
// The FxA auth server expects requests to certain endpoints to be authorized
// using Hawk.
this.hawk = new HawkClient(host);
};
this.FxAccountsClient.prototype = {
/**
* Return client clock offset, in milliseconds, as determined by hawk client.
* Provided because callers should not have to know about hawk
* implementation.
*
* The offset is the number of milliseconds that must be added to the client
* clock to make it equal to the server clock. For example, if the client is
* five minutes ahead of the server, the localtimeOffsetMsec will be -300000.
*/
get localtimeOffsetMsec() {
return this.hawk.localtimeOffsetMsec;
},
/*
* Return current time in milliseconds
*
* Not used by this module, but made available to the FxAccounts.jsm
* that uses this client.
*/
now: function() {
return this.hawk.now();
},
/**
* Create a new Firefox Account and authenticate
*
@ -149,7 +185,7 @@ this.FxAccountsClient.prototype = {
let creds = this._deriveHawkCredentials(keyFetchTokenHex, "keyFetchToken");
let keyRequestKey = creds.extra.slice(0, 32);
let morecreds = CryptoUtils.hkdf(keyRequestKey, undefined,
PREFIX_NAME + "account/keys", 3 * 32);
KW("account/keys"), 3 * 32);
let respHMACKey = morecreds.slice(0, 32);
let respXORKey = morecreds.slice(32, 96);
@ -199,8 +235,10 @@ this.FxAccountsClient.prototype = {
return Promise.resolve()
.then(_ => this._request("/certificate/sign", "POST", creds, body))
.then(resp => resp.cert,
err => {dump("HAWK.signCertificate error: " + JSON.stringify(err) + "\n");
throw err;});
err => {
log.error("HAWK.signCertificate error: " + JSON.stringify(err));
throw err;
});
},
/**
@ -219,8 +257,10 @@ this.FxAccountsClient.prototype = {
// the account exists
(result) => true,
(err) => {
log.error("accountExists: error: " + JSON.stringify(err));
// the account doesn't exist
if (err.errno === 102) {
log.debug("returning false for errno 102");
return false;
}
// propogate other request errors
@ -251,7 +291,7 @@ this.FxAccountsClient.prototype = {
*/
_deriveHawkCredentials: function (tokenHex, context, size) {
let token = CommonUtils.hexToBytes(tokenHex);
let out = CryptoUtils.hkdf(token, undefined, PREFIX_NAME + context, size || 3 * 32);
let out = CryptoUtils.hkdf(token, undefined, KW(context), size || 3 * 32);
return {
algorithm: "sha256",
@ -286,63 +326,21 @@ this.FxAccountsClient.prototype = {
*/
_request: function hawkRequest(path, method, credentials, jsonPayload) {
let deferred = Promise.defer();
let xhr = new XMLHttpRequest({mozSystem: true});
let URI = this.host + path;
let payload;
xhr.mozBackgroundRequest = true;
if (jsonPayload) {
payload = JSON.stringify(jsonPayload);
}
log.debug("(HAWK request) - Path: " + path + " - Method: " + method +
" - Payload: " + payload);
xhr.open(method, URI);
xhr.channel.loadFlags = Ci.nsIChannel.LOAD_BYPASS_CACHE |
Ci.nsIChannel.INHIBIT_CACHING;
// When things really blow up, reconstruct an error object that follows the general format
// of the server on error responses.
function constructError(err) {
return { error: err, message: xhr.statusText, code: xhr.status, errno: xhr.status };
}
xhr.onerror = function() {
deferred.reject(constructError('Request failed'));
};
xhr.onload = function onload() {
try {
let response = JSON.parse(xhr.responseText);
log.debug("(Response) Code: " + xhr.status + " - Status text: " +
xhr.statusText + " - Response text: " + xhr.responseText);
if (xhr.status !== 200 || response.error) {
// In this case, the response is an object with error information.
return deferred.reject(response);
this.hawk.request(path, method, credentials, jsonPayload).then(
(responseText) => {
try {
let response = JSON.parse(responseText);
deferred.resolve(response);
} catch (err) {
deferred.reject({error: err});
}
deferred.resolve(response);
} catch (e) {
log.error("(Response) Code: " + xhr.status + " - Status text: " +
xhr.statusText);
deferred.reject(constructError(e));
},
(error) => {
deferred.reject(error);
}
};
let uri = Services.io.newURI(URI, null, null);
if (credentials) {
let header = CryptoUtils.computeHAWK(uri, method, {
credentials: credentials,
payload: payload,
contentType: "application/json"
});
xhr.setRequestHeader("authorization", header.field);
}
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(payload);
);
return deferred.promise;
},

Some files were not shown because too many files have changed in this diff Show More