mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-02-04 13:07:52 +00:00
Merge fx-team to m-c.
This commit is contained in:
commit
92add8e36d
310
b2g/chrome/content/devtools.js
Normal file
310
b2g/chrome/content/devtools.js
Normal 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);
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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" ||
|
||||
|
@ -223,7 +223,8 @@
|
||||
noautofocus="true"
|
||||
noautohide="true"
|
||||
flip="none"
|
||||
consumeoutsideclicks="false">
|
||||
consumeoutsideclicks="false"
|
||||
mousethrough="always">
|
||||
<box id="UITourHighlight"></box>
|
||||
</panel>
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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/>
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
},
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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();
|
||||
});
|
||||
|
||||
|
@ -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 -->
|
||||
|
@ -233,5 +233,5 @@ let SessionWorker = (function () {
|
||||
AsyncShutdown.profileBeforeChange.addBlocker(
|
||||
"SessionFile: Finish writing the latest sessionstore.js",
|
||||
function() {
|
||||
return SessionFile._latestWrite;
|
||||
return SessionFileInternal._latestWrite;
|
||||
});
|
||||
|
@ -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 });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -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;"
|
||||
|
@ -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]
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
@ -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);
|
||||
});
|
||||
}
|
@ -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>
|
@ -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"
|
||||
|
@ -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;
|
||||
|
@ -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
1816
browser/extensions/pdfjs/content/build/pdf.worker.js
vendored
1816
browser/extensions/pdfjs/content/build/pdf.worker.js
vendored
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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; > &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.">
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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">
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
44
browser/metro/base/tests/mochitest/browser_flyouts.js
Normal file
44
browser/metro/base/tests/mochitest/browser_flyouts.js
Normal 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();
|
||||
}
|
@ -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">
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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.");
|
||||
|
@ -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
|
||||
});
|
||||
|
@ -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();
|
||||
}
|
||||
},
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -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>
|
@ -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"
|
@ -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 =
|
||||
|
@ -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;
|
||||
|
@ -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 = [];
|
||||
|
@ -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 */
|
||||
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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 */
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
51
mobile/android/base/home/HomeContextMenuInfo.java
Normal file
51
mobile/android/base/home/HomeContextMenuInfo.java
Normal 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));
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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) { }
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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',
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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" />
|
||||
|
||||
|
@ -90,5 +90,6 @@
|
||||
|
||||
<color name="home_last_tab_bar_bg">#FFF5F7F9</color>
|
||||
|
||||
<color name="panel_grid_item_image_background">#D1D9E1</color>
|
||||
</resources>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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"/>
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
|
21
mobile/android/base/widget/SquaredImageView.java
Normal file
21
mobile/android/base/widget/SquaredImageView.java
Normal 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());
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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
201
services/common/hawk.js
Normal 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;
|
||||
}
|
||||
}
|
@ -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
|
||||
);
|
||||
}
|
||||
};
|
||||
|
485
services/common/tests/unit/test_hawk.js
Normal file
485
services/common/tests/unit/test_hawk.js
Normal 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();
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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]
|
||||
|
@ -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) {
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user