Merge m-c to inbound.

This commit is contained in:
Ryan VanderMeulen 2013-12-11 22:42:05 -05:00
commit 076f3db22c
38 changed files with 1408 additions and 173 deletions

View File

@ -106,11 +106,6 @@ function appUpdater()
this.bundle = Services.strings.
createBundle("chrome://browser/locale/browser.properties");
this.updateBtn = document.getElementById("updateButton");
// The button label value must be set so its height is correct.
this.setupUpdateButton("update.checkInsideButton");
let manualURL = Services.urlFormatter.formatURLPref("app.update.url.manual");
let manualLink = document.getElementById("manualLink");
manualLink.value = manualURL;
@ -123,8 +118,7 @@ function appUpdater()
}
if (this.isPending || this.isApplied) {
this.setupUpdateButton("update.restart." +
(this.isMajor ? "upgradeButton" : "updateButton"));
this.selectPanel("apply");
return;
}
@ -135,15 +129,19 @@ function appUpdater()
if (this.isDownloading) {
this.startDownload();
// selectPanel("downloading") is called from setupDownloadingUI().
return;
}
if (this.updateEnabled && this.updateAuto) {
this.selectPanel("checkingForUpdates");
this.isChecking = true;
this.checker.checkForUpdates(this.updateCheckListener, true);
return;
}
// If app.update.enabled is false, we don't pop up an update dialog
// automatically, but opening the About dialog is considered manually
// checking for updates, so we always check.
// If app.update.auto is false, we ask before downloading though,
// in onCheckComplete.
this.selectPanel("checkingForUpdates");
this.isChecking = true;
this.checker.checkForUpdates(this.updateCheckListener, true);
// after checking, onCheckComplete() is called
}
appUpdater.prototype =
@ -180,13 +178,6 @@ appUpdater.prototype =
this.um.activeUpdate.state == "downloading";
},
// true when the update type is major.
get isMajor() {
if (this.update)
return this.update.type == "major";
return this.um.activeUpdate.type == "major";
},
// true when updating is disabled by an administrator.
get updateDisabledAndLocked() {
return !this.updateEnabled &&
@ -218,36 +209,54 @@ appUpdater.prototype =
},
/**
* Sets the deck's selected panel.
* Sets the panel of the updateDeck.
*
* @param aChildID
* The id of the deck's child to select.
* The id of the deck's child to select, e.g. "apply".
*/
selectPanel: function(aChildID) {
this.updateDeck.selectedPanel = document.getElementById(aChildID);
this.updateBtn.disabled = (aChildID != "updateButtonBox");
let panel = document.getElementById(aChildID);
let button = panel.querySelector("button");
if (button) {
if (aChildID == "downloadAndInstall") {
let updateVersion = gAppUpdater.update.displayVersion;
button.label = this.bundle.formatStringFromName("update.downloadAndInstallButton.label", [updateVersion], 1);
button.accessKey = this.bundle.GetStringFromName("update.downloadAndInstallButton.accesskey");
}
this.updateDeck.selectedPanel = panel;
if (!document.commandDispatcher.focusedElement || // don't steal the focus
document.commandDispatcher.focusedElement.localName == "button") // except from the other buttons
button.focus();
} else {
this.updateDeck.selectedPanel = panel;
}
},
/**
* Sets the update button's label and accesskey.
*
* @param aKeyPrefix
* The prefix for the properties file entry to use for setting the
* label and accesskey.
* Check for addon compat, or start the download right away
*/
setupUpdateButton: function(aKeyPrefix) {
this.updateBtn.label = this.bundle.GetStringFromName(aKeyPrefix + ".label");
this.updateBtn.accessKey = this.bundle.GetStringFromName(aKeyPrefix + ".accesskey");
if (!document.commandDispatcher.focusedElement ||
document.commandDispatcher.focusedElement == this.updateBtn)
this.updateBtn.focus();
doUpdate: function() {
// skip the compatibility check if the update doesn't provide appVersion,
// or the appVersion is unchanged, e.g. nightly update
if (!this.update.appVersion ||
Services.vc.compare(gAppUpdater.update.appVersion,
Services.appinfo.version) == 0) {
this.startDownload();
} else {
this.checkAddonCompatibility();
}
},
/**
* Handles oncommand for the update button.
* Handles oncommand for the "Restart to Update" button
* which is presented after the download has been downloaded.
*/
buttonOnCommand: function() {
if (this.isPending || this.isApplied) {
buttonRestartAfterDownload: function() {
if (!this.isPending && !this.isApplied)
return;
// Notify all windows that an application quit has been requested.
let cancelQuit = Components.classes["@mozilla.org/supports-PRBool;1"].
createInstance(Components.interfaces.nsISupportsPRBool);
@ -268,27 +277,21 @@ appUpdater.prototype =
appStartup.quit(Components.interfaces.nsIAppStartup.eAttemptQuit |
Components.interfaces.nsIAppStartup.eRestart);
return;
}
},
/**
* Handles oncommand for the "Apply Update…" button
* which is presented if we need to show the billboard or license.
*/
buttonApplyBillboard: function() {
const URI_UPDATE_PROMPT_DIALOG = "chrome://mozapps/content/update/updates.xul";
// Firefox no longer displays a license for updates and the licenseURL check
// is just in case a distibution does.
if (this.update && (this.update.billboardURL || this.update.licenseURL ||
this.addons.length != 0)) {
var ary = null;
ary = Components.classes["@mozilla.org/supports-array;1"].
createInstance(Components.interfaces.nsISupportsArray);
ary.AppendElement(this.update);
var openFeatures = "chrome,centerscreen,dialog=no,resizable=no,titlebar,toolbar=no";
Services.ww.openWindow(null, URI_UPDATE_PROMPT_DIALOG, "", openFeatures, ary);
window.close();
return;
}
this.selectPanel("checkingForUpdates");
this.isChecking = true;
this.checker.checkForUpdates(this.updateCheckListener, true);
var ary = null;
ary = Components.classes["@mozilla.org/supports-array;1"].
createInstance(Components.interfaces.nsISupportsArray);
ary.AppendElement(this.update);
var openFeatures = "chrome,centerscreen,dialog=no,resizable=no,titlebar,toolbar=no";
Services.ww.openWindow(null, URI_UPDATE_PROMPT_DIALOG, "", openFeatures, ary);
window.close(); // close the "About" window; updates.xul takes over.
},
/**
@ -326,21 +329,14 @@ appUpdater.prototype =
// Firefox no longer displays a license for updates and the licenseURL
// check is just in case a distibution does.
if (gAppUpdater.update.billboardURL || gAppUpdater.update.licenseURL) {
gAppUpdater.selectPanel("updateButtonBox");
gAppUpdater.setupUpdateButton("update.openUpdateUI." +
(this.isMajor ? "upgradeButton"
: "applyButton"));
gAppUpdater.selectPanel("applyBillboard");
return;
}
if (!gAppUpdater.update.appVersion ||
Services.vc.compare(gAppUpdater.update.appVersion,
Services.appinfo.version) == 0) {
gAppUpdater.startDownload();
return;
}
gAppUpdater.checkAddonCompatibility();
if (gAppUpdater.updateAuto) // automatically download and install
gAppUpdater.doUpdate();
else // ask
gAppUpdater.selectPanel("downloadAndInstall");
},
/**
@ -474,9 +470,7 @@ appUpdater.prototype =
return;
}
this.selectPanel("updateButtonBox");
this.setupUpdateButton("update.openUpdateUI." +
(this.isMajor ? "upgradeButton" : "applyButton"));
this.selectPanel("apply");
},
/**
@ -553,11 +547,9 @@ appUpdater.prototype =
if (status == "applied" || status == "applied-service" ||
status == "pending" || status == "pending-service") {
// If the update is successfully applied, or if the updater has
// fallen back to non-staged updates, show the Restart to Update
// fallen back to non-staged updates, show the "Restart to Update"
// button.
self.selectPanel("updateButtonBox");
self.setupUpdateButton("update.restart." +
(self.isMajor ? "upgradeButton" : "updateButton"));
self.selectPanel("apply");
} else if (status == "failed") {
// Background update has failed, let's show the UI responsible for
// prompting the user to update manually.
@ -572,9 +564,7 @@ appUpdater.prototype =
Services.obs.removeObserver(arguments.callee, "update-staged");
}, "update-staged", false);
} else {
this.selectPanel("updateButtonBox");
this.setupUpdateButton("update.restart." +
(this.isMajor ? "upgradeButton" : "updateButton"));
this.selectPanel("apply");
}
break;
default:
@ -582,7 +572,6 @@ appUpdater.prototype =
this.selectPanel("downloadFailed");
break;
}
},
/**

View File

@ -50,17 +50,29 @@
<vbox id="updateBox">
#ifdef MOZ_UPDATER
<deck id="updateDeck" orient="vertical">
<hbox id="updateButtonBox" align="center">
<hbox id="downloadAndInstall" align="center">
<button id="downloadAndInstallButton" align="start"
oncommand="gAppUpdater.doUpdate();"/>
<!-- label and accesskey will be filled by JS -->
<spacer flex="1"/>
</hbox>
<hbox id="apply" align="center">
<button id="updateButton" align="start"
oncommand="gAppUpdater.buttonOnCommand();"/>
label="&update.updateButton.label;"
accesskey="&update.updateButton.accesskey;"
oncommand="gAppUpdater.buttonRestartAfterDownload();"/>
<spacer flex="1"/>
</hbox>
<hbox id="applyBillboard" align="center">
<button id="applyButtonBillboard" align="start"
label="&update.applyButtonBillboard.label;"
accesskey="&update.applyButtonBillboard.accesskey;"
oncommand="gAppUpdater.buttonApplyBillboard();"/>
<spacer flex="1"/>
</hbox>
<hbox id="checkingForUpdates" align="center">
<image class="update-throbber"/><label>&update.checkingForUpdates;</label>
</hbox>
<hbox id="checkingAddonCompat" align="center">
<image class="update-throbber"/><label>&update.checkingAddonCompat;</label>
</hbox>
<hbox id="downloading" align="center">
<image class="update-throbber"/><label>&update.downloading.start;</label><label id="downloadStatus"/><label>&update.downloading.end;</label>
</hbox>

View File

@ -457,10 +457,17 @@ let CustomizableUIInternal = {
// If the placements have items in them which are (now) no longer removable,
// we shouldn't be moving them:
if (node.parentNode != container && !this.isWidgetRemovable(node)) {
if (provider == CustomizableUI.PROVIDER_API) {
let widgetInfo = gPalette.get(id);
if (!widgetInfo.removable && aArea != widgetInfo.defaultArea) {
placementsToRemove.add(id);
continue;
}
} else if (provider == CustomizableUI.PROVIDER_XUL &&
node.parentNode != container && !this.isWidgetRemovable(node)) {
placementsToRemove.add(id);
continue;
}
} // Special widgets are always removable, so no need to check them
if (inPrivateWindow && provider == CustomizableUI.PROVIDER_API) {
let widget = gPalette.get(id);
@ -1736,7 +1743,7 @@ let CustomizableUIInternal = {
source: aSource || "addon",
instances: new Map(),
currentArea: null,
removable: false,
removable: true,
overflows: true,
defaultArea: null,
shortcutId: null,
@ -1778,6 +1785,11 @@ let CustomizableUIInternal = {
if (aData.defaultArea && gAreas.has(aData.defaultArea)) {
widget.defaultArea = aData.defaultArea;
} else if (!widget.removable) {
ERROR("Widget '" + widget.id + "' is not removable but does not specify " +
"a valid defaultArea. That's not possible; it must specify a " +
"valid defaultArea as well.");
return null;
}
if ("type" in aData && gSupportedWidgetTypes.has(aData.type)) {
@ -2383,11 +2395,13 @@ this.CustomizableUI = {
* invoked when a user hides your view.
* - tooltiptext: string to use for the tooltip of the widget
* - label: string to use for the label of the widget
* - removable: whether the widget is removable (optional, default: false)
* - removable: whether the widget is removable (optional, default: true)
* NB: if you specify false here, you must provide a
* defaultArea, too.
* - overflows: whether widget can overflow when in an overflowable
* toolbar (optional, default: true)
* - defaultArea: default area to add the widget to
* (optional, default: none)
* (optional, default: none; required if non-removable)
* - shortcutId: id of an element that has a shortcut for this widget
* (optional, default: null). This is only used to display
* the shortcut as part of the tooltip for builtin widgets

View File

@ -61,7 +61,6 @@ const CustomizableWidgets = [{
type: "view",
viewId: "PanelUI-history",
shortcutId: "key_gotoHistory",
removable: true,
defaultArea: CustomizableUI.AREA_PANEL,
onViewShowing: function(aEvent) {
// Populate our list of history
@ -148,7 +147,6 @@ const CustomizableWidgets = [{
}
}, {
id: "privatebrowsing-button",
removable: true,
shortcutId: "key_privatebrowsing",
defaultArea: CustomizableUI.AREA_PANEL,
onCommand: function(e) {
@ -161,7 +159,6 @@ const CustomizableWidgets = [{
}
}, {
id: "save-page-button",
removable: true,
shortcutId: "key_savePage",
defaultArea: CustomizableUI.AREA_PANEL,
onCommand: function(aEvent) {
@ -174,7 +171,6 @@ const CustomizableWidgets = [{
}
}, {
id: "find-button",
removable: true,
shortcutId: "key_find",
defaultArea: CustomizableUI.AREA_PANEL,
onCommand: function(aEvent) {
@ -187,7 +183,6 @@ const CustomizableWidgets = [{
}
}, {
id: "open-file-button",
removable: true,
shortcutId: "openFileKb",
defaultArea: CustomizableUI.AREA_PANEL,
onCommand: function(aEvent) {
@ -202,7 +197,6 @@ const CustomizableWidgets = [{
id: "developer-button",
type: "view",
viewId: "PanelUI-developer",
removable: true,
shortcutId: "key_devToolboxMenuItem",
defaultArea: CustomizableUI.AREA_PANEL,
onViewShowing: function(aEvent) {
@ -265,7 +259,6 @@ const CustomizableWidgets = [{
}
}, {
id: "add-ons-button",
removable: true,
shortcutId: "key_openAddons",
defaultArea: CustomizableUI.AREA_PANEL,
onCommand: function(aEvent) {
@ -278,7 +271,6 @@ const CustomizableWidgets = [{
}
}, {
id: "preferences-button",
removable: true,
defaultArea: CustomizableUI.AREA_PANEL,
#ifdef XP_WIN
label: "preferences-button.labelWin",
@ -295,7 +287,6 @@ const CustomizableWidgets = [{
}, {
id: "zoom-controls",
type: "custom",
removable: true,
defaultArea: CustomizableUI.AREA_PANEL,
onBuild: function(aDocument) {
const kPanelId = "PanelUI-popup";
@ -441,7 +432,6 @@ const CustomizableWidgets = [{
}, {
id: "edit-controls",
type: "custom",
removable: true,
defaultArea: CustomizableUI.AREA_PANEL,
onBuild: function(aDocument) {
let inPanel = (this.currentArea == CustomizableUI.AREA_PANEL);
@ -536,7 +526,6 @@ const CustomizableWidgets = [{
id: "feed-button",
type: "view",
viewId: "PanelUI-feeds",
removable: true,
defaultArea: CustomizableUI.AREA_PANEL,
onClick: function(aEvent) {
let win = aEvent.target.ownerDocument.defaultView;
@ -576,7 +565,6 @@ const CustomizableWidgets = [{
id: "characterencoding-button",
type: "view",
viewId: "PanelUI-characterEncodingView",
removable: true,
defaultArea: CustomizableUI.AREA_PANEL,
maybeDisableMenu: function(aDocument) {
let window = aDocument.defaultView;
@ -783,7 +771,6 @@ const CustomizableWidgets = [{
}
}, {
id: "email-link-button",
removable: true,
onCommand: function(aEvent) {
let win = aEvent.view;
win.MailIntegration.sendLinkForWindow(win.content);
@ -801,7 +788,6 @@ if (Services.sysinfo.getProperty("hasWindowsTouchInterface")) {
id: "switch-to-metro-button",
label: "switch-to-metro-button2.label",
tooltiptext: metroTooltip,
removable: true,
defaultArea: CustomizableUI.AREA_PANEL,
showInPrivateBrowsing: false, /* See bug 928068 */
onCommand: function(aEvent) {

View File

@ -45,4 +45,5 @@ skip-if = os == "mac"
[browser_942581_unregisterArea_keeps_placements.js]
[browser_943683_migration_test.js]
[browser_944887_destroyWidget_should_destroy_in_palette.js]
[browser_947987_removable_default.js]
[browser_panel_toggle.js]

View File

@ -10,16 +10,17 @@ let gTests = [
let navbar = document.getElementById("nav-bar");
ok(CustomizableUI.inDefaultState, "Should start in default state");
CustomizableUI.createWidget({id: kWidgetId, removable: false, label: "Test"});
let button = createDummyXULButton(kWidgetId, "Test non-removable inDefaultState handling");
CustomizableUI.addWidgetToArea(kWidgetId, CustomizableUI.AREA_NAVBAR);
button.setAttribute("removable", "false");
ok(CustomizableUI.inDefaultState, "Should still be in default state after navbar addition");
CustomizableUI.destroyWidget(kWidgetId);
button.remove();
CustomizableUI.createWidget({id: kWidgetId, removable: false, label: "Test"});
button = createDummyXULButton(kWidgetId, "Test non-removable inDefaultState handling");
CustomizableUI.addWidgetToArea(kWidgetId, CustomizableUI.AREA_PANEL);
button.setAttribute("removable", "false");
ok(CustomizableUI.inDefaultState, "Should still be in default state after panel addition");
CustomizableUI.destroyWidget(kWidgetId);
button.remove();
ok(CustomizableUI.inDefaultState, "Should be in default state after destroying both widgets");
},
teardown: null

View File

@ -0,0 +1,79 @@
/* 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/. */
let kWidgetId = "test-removable-widget-default";
const kNavBar = CustomizableUI.AREA_NAVBAR;
let widgetCounter = 0;
let gTests = [
{
desc: "Sanity checks",
run: function() {
let brokenSpec = {id: kWidgetId + (widgetCounter++), removable: false};
SimpleTest.doesThrow(function() CustomizableUI.createWidget(brokenSpec),
"Creating non-removable widget without defaultArea should throw.");
// Widget without removable set should be removable:
let wrapper = CustomizableUI.createWidget({id: kWidgetId + (widgetCounter++)});
ok(CustomizableUI.isWidgetRemovable(wrapper.id), "Should be removable by default.");
CustomizableUI.destroyWidget(wrapper.id);
}
},
{
desc: "Test non-removable widget with defaultArea",
run: function() {
// Non-removable widget with defaultArea should work:
let spec = {id: kWidgetId + (widgetCounter++), removable: false,
defaultArea: kNavBar};
let widgetWrapper;
try {
widgetWrapper = CustomizableUI.createWidget(spec);
} catch (ex) {
ok(false, "Creating a non-removable widget with a default area should not throw.");
return;
}
let placement = CustomizableUI.getPlacementOfWidget(spec.id);
ok(placement, "Widget should be placed.");
is(placement.area, kNavBar, "Widget should be in navbar");
let singleWrapper = widgetWrapper.forWindow(window);
ok(singleWrapper, "Widget should exist in window.");
ok(singleWrapper.node, "Widget node should exist in window.");
let expectedParent = CustomizableUI.getCustomizeTargetForArea(kNavBar, window);
is(singleWrapper.node.parentNode, expectedParent, "Widget should be in navbar.");
let otherWin = yield openAndLoadWindow(true);
placement = CustomizableUI.getPlacementOfWidget(spec.id);
ok(placement, "Widget should be placed.");
is(placement && placement.area, kNavBar, "Widget should be in navbar");
singleWrapper = widgetWrapper.forWindow(otherWin);
ok(singleWrapper, "Widget should exist in other window.");
if (singleWrapper) {
ok(singleWrapper.node, "Widget node should exist in other window.");
if (singleWrapper.node) {
let expectedParent = CustomizableUI.getCustomizeTargetForArea(kNavBar, otherWin);
is(singleWrapper.node.parentNode, expectedParent,
"Widget should be in navbar in other window.");
}
}
otherWin.close();
}
},
];
function asyncCleanup() {
yield resetCustomization();
}
function cleanup() {
removeCustomToolbars();
}
function test() {
waitForExplicitFinish();
registerCleanupFunction(cleanup);
runTests(gTests, asyncCleanup);
}

View File

@ -31,3 +31,4 @@ support-files =
[browser_toolbar_tooltip.js]
[browser_toolbar_webconsole_errors_count.js]
[browser_spectrum.js]
[browser_csstransformpreview.js]

View File

@ -0,0 +1,139 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests that the spectrum color picker works correctly
const TEST_URI = "data:text/html;charset=utf-8,<div></div>";
const {CSSTransformPreviewer} = devtools.require("devtools/shared/widgets/CSSTransformPreviewer");
let doc, root;
function test() {
waitForExplicitFinish();
addTab(TEST_URI, () => {
doc = content.document;
root = doc.querySelector("div");
startTests();
});
}
function endTests() {
doc = root = null;
gBrowser.removeCurrentTab();
finish();
}
function startTests() {
testCreateAndDestroyShouldAppendAndRemoveElements();
}
function testCreateAndDestroyShouldAppendAndRemoveElements() {
ok(root, "We have the root node to append the preview to");
is(root.childElementCount, 0, "Root node is empty");
let p = new CSSTransformPreviewer(root);
p.preview("matrix(1, -0.2, 0, 1, 0, 0)");
ok(root.childElementCount > 0, "Preview has appended elements");
ok(root.querySelector("canvas"), "Canvas preview element is here");
p.destroy();
is(root.childElementCount, 0, "Destroying preview removed all nodes");
testCanvasDimensionIsConstrainedByMaxDim();
}
function testCanvasDimensionIsConstrainedByMaxDim() {
let p = new CSSTransformPreviewer(root);
p.MAX_DIM = 500;
p.preview("scale(1)", "center", 1000, 1000);
let canvas = root.querySelector("canvas");
is(canvas.width, 500, "Canvas width is correct");
is(canvas.height, 500, "Canvas height is correct");
p.destroy();
testCallingPreviewSeveralTimesReusesTheSameCanvas();
}
function testCallingPreviewSeveralTimesReusesTheSameCanvas() {
let p = new CSSTransformPreviewer(root);
p.preview("scale(1)", "center", 1000, 1000);
let canvas = root.querySelector("canvas");
p.preview("rotate(90deg)");
let canvases = root.querySelectorAll("canvas");
is(canvases.length, 1, "Still one canvas element");
is(canvases[0], canvas, "Still the same canvas element");
p.destroy();
testCanvasDimensionAreCorrect();
}
function testCanvasDimensionAreCorrect() {
// Only test a few simple transformations
let p = new CSSTransformPreviewer(root);
// Make sure we have a square
let w = 200, h = w;
p.MAX_DIM = w;
// We can't test the content of the canvas here, just that, given a max width
// the aspect ratio of the canvas seems correct.
// Translate a square by its width, should be a rectangle
p.preview("translateX(200px)", "center", w, h);
let canvas = root.querySelector("canvas");
is(canvas.width, w, "width is correct");
is(canvas.height, h/2, "height is half of the width");
// Rotate on the top right corner, should be a rectangle
p.preview("rotate(-90deg)", "top right", w, h);
is(canvas.width, w, "width is correct");
is(canvas.height, h/2, "height is half of the width");
// Rotate on the bottom left corner, should be a rectangle
p.preview("rotate(90deg)", "top right", w, h);
is(canvas.width, w/2, "width is half of the height");
is(canvas.height, h, "height is correct");
// Scale from center, should still be a square
p.preview("scale(2)", "center", w, h);
is(canvas.width, w, "width is correct");
is(canvas.height, h, "height is correct");
// Skew from center, 45deg, should be a rectangle
p.preview("skew(45deg)", "center", w, h);
is(canvas.width, w, "width is correct");
is(canvas.height, h/2, "height is half of the height");
p.destroy();
testPreviewingInvalidTransformReturnsFalse();
}
function testPreviewingInvalidTransformReturnsFalse() {
let p = new CSSTransformPreviewer(root);
ok(!p.preview("veryWow(muchPx) suchTransform(soDeg)"), "Returned false for invalid transform");
ok(!p.preview("rotae(3deg)"), "Returned false for invalid transform");
// Verify the canvas is empty by checking the image data
let canvas = root.querySelector("canvas"), ctx = canvas.getContext("2d");
let data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
for (let i = 0, n = data.length; i < n; i += 4) {
// Let's not log 250*250*4 asserts! Instead, just log when it fails
let red = data[i];
let green = data[i + 1];
let blue = data[i + 2];
let alpha = data[i + 3];
if (red !== 0 || green !== 0 || blue !== 0 || alpha !== 0) {
ok(false, "Image data is not empty after an invalid transformed was previewed");
break;
}
}
is(p.preview("translateX(30px)"), true, "Returned true for a valid transform");
endTests();
}

View File

@ -0,0 +1,389 @@
/* 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";
/**
* The CSSTransformPreview module displays, using a <canvas> a rectangle, with
* a given width and height and its transformed version, given a css transform
* property and origin. It also displays arrows from/to each corner.
*
* It is useful to visualize how a css transform affected an element. It can
* help debug tricky transformations. It is used today in a tooltip, and this
* tooltip is shown when hovering over a css transform declaration in the rule
* and computed view panels.
*
* TODO: For now, it multiplies matrices itself to calculate the coordinates of
* the transformed box, but that should be removed as soon as we can get access
* to getQuads().
*/
const HTML_NS = "http://www.w3.org/1999/xhtml";
/**
* The TransformPreview needs an element to output a canvas tag.
*
* Usage example:
*
* let t = new CSSTransformPreviewer(myRootElement);
* t.preview("rotate(45deg)", "top left", 200, 400);
* t.preview("skew(19deg)", "center", 100, 500);
* t.preview("matrix(1, -0.2, 0, 1, 0, 0)");
* t.destroy();
*
* @param {nsIDOMElement} parentEl
* Where the canvas will go
*/
function CSSTransformPreviewer(parentEl) {
this.parentEl = parentEl;
this.doc = this.parentEl.ownerDocument;
this.canvas = null;
this.ctx = null;
}
module.exports.CSSTransformPreviewer = CSSTransformPreviewer;
CSSTransformPreviewer.prototype = {
/**
* The preview look-and-feel can be changed using these properties
*/
MAX_DIM: 250,
PAD: 5,
ORIGINAL_FILL: "#1F303F",
ORIGINAL_STROKE: "#B2D8FF",
TRANSFORMED_FILL: "rgba(200, 200, 200, .5)",
TRANSFORMED_STROKE: "#B2D8FF",
ARROW_STROKE: "#329AFF",
ORIGIN_STROKE: "#329AFF",
ARROW_TIP_HEIGHT: 10,
ARROW_TIP_WIDTH: 8,
CORNER_SIZE_RATIO: 6,
/**
* Destroy removes the canvas from the parentelement passed in the constructor
*/
destroy: function() {
if (this.canvas) {
this.parentEl.removeChild(this.canvas);
}
if (this._hiddenDiv) {
this.parentEl.removeChild(this._hiddenDiv);
}
this.parentEl = this.canvas = this.ctx = this.doc = null;
},
_createMarkup: function() {
this.canvas = this.doc.createElementNS(HTML_NS, "canvas");
this.canvas.setAttribute("id", "canvas");
this.canvas.setAttribute("width", this.MAX_DIM);
this.canvas.setAttribute("height", this.MAX_DIM);
this.canvas.style.position = "relative";
this.parentEl.appendChild(this.canvas);
this.ctx = this.canvas.getContext("2d");
},
_getComputed: function(name, value, width, height) {
if (!this._hiddenDiv) {
// Create a hidden element to apply the style to
this._hiddenDiv = this.doc.createElementNS(HTML_NS, "div");
this._hiddenDiv.style.visibility = "hidden";
this._hiddenDiv.style.position = "absolute";
this.parentEl.appendChild(this._hiddenDiv);
}
// Camelcase the name
name = name.replace(/-([a-z]{1})/g, (m, letter) => letter.toUpperCase());
// Apply width and height to make sure computation is made correctly
this._hiddenDiv.style.width = width + "px";
this._hiddenDiv.style.height = height + "px";
// Show the hidden div, apply the style, read the computed style, hide the
// hidden div again
this._hiddenDiv.style.display = "block";
this._hiddenDiv.style[name] = value;
let computed = this.doc.defaultView.getComputedStyle(this._hiddenDiv);
let computedValue = computed[name];
this._hiddenDiv.style.display = "none";
return computedValue;
},
_getMatrixFromTransformString: function(transformStr) {
let matrix = transformStr.substring(0, transformStr.length - 1).
substring(transformStr.indexOf("(") + 1).split(",");
matrix.forEach(function(value, index) {
matrix[index] = parseFloat(value, 10);
});
let transformMatrix = null;
if (matrix.length === 6) {
// 2d transform
transformMatrix = [
[matrix[0], matrix[2], matrix[4], 0],
[matrix[1], matrix[3], matrix[5], 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
];
} else {
// 3d transform
transformMatrix = [
[matrix[0], matrix[4], matrix[8], matrix[12]],
[matrix[1], matrix[5], matrix[9], matrix[13]],
[matrix[2], matrix[6], matrix[10], matrix[14]],
[matrix[3], matrix[7], matrix[11], matrix[15]]
];
}
return transformMatrix;
},
_getOriginFromOriginString: function(originStr) {
let offsets = originStr.split(" ");
offsets.forEach(function(item, index) {
offsets[index] = parseInt(item, 10);
});
return offsets;
},
_multiply: function(m1, m2) {
let m = [];
for (let m1Line = 0; m1Line < m1.length; m1Line++) {
m[m1Line] = 0;
for (let m2Col = 0; m2Col < m2.length; m2Col++) {
m[m1Line] += m1[m1Line][m2Col] * m2[m2Col];
}
}
return [m[0], m[1]];
},
_getTransformedPoint: function(matrix, point, origin) {
let pointMatrix = [point[0] - origin[0], point[1] - origin[1], 1, 1];
return this._multiply(matrix, pointMatrix);
},
_getTransformedPoints: function(matrix, rect, origin) {
return rect.map(point => {
let tPoint = this._getTransformedPoint(matrix, [point[0], point[1]], origin);
return [tPoint[0] + origin[0], tPoint[1] + origin[1]];
});
},
/**
* For canvas to avoid anti-aliasing
*/
_round: x => Math.round(x) + .5,
_drawShape: function(points, fillStyle, strokeStyle) {
this.ctx.save();
this.ctx.lineWidth = 1;
this.ctx.strokeStyle = strokeStyle;
this.ctx.fillStyle = fillStyle;
this.ctx.beginPath();
this.ctx.moveTo(this._round(points[0][0]), this._round(points[0][1]));
for (var i = 1; i < points.length; i++) {
this.ctx.lineTo(this._round(points[i][0]), this._round(points[i][1]));
}
this.ctx.lineTo(this._round(points[0][0]), this._round(points[0][1]));
this.ctx.fill();
this.ctx.stroke();
this.ctx.restore();
},
_drawArrow: function(x1, y1, x2, y2) {
// do not draw if the line is too small
if (Math.abs(x2-x1) < 20 && Math.abs(y2-y1) < 20) {
return;
}
this.ctx.save();
this.ctx.strokeStyle = this.ARROW_STROKE;
this.ctx.fillStyle = this.ARROW_STROKE;
this.ctx.lineWidth = 1;
this.ctx.beginPath();
this.ctx.moveTo(this._round(x1), this._round(y1));
this.ctx.lineTo(this._round(x2), this._round(y2));
this.ctx.stroke();
this.ctx.beginPath();
this.ctx.translate(x2, y2);
let radians = Math.atan((y1 - y2) / (x1 - x2));
radians += ((x1 >= x2) ? -90 : 90) * Math.PI / 180;
this.ctx.rotate(radians);
this.ctx.moveTo(0, 0);
this.ctx.lineTo(this.ARROW_TIP_WIDTH / 2, this.ARROW_TIP_HEIGHT);
this.ctx.lineTo(-this.ARROW_TIP_WIDTH / 2, this.ARROW_TIP_HEIGHT);
this.ctx.closePath();
this.ctx.fill();
this.ctx.restore();
},
_drawOrigin: function(x, y) {
this.ctx.save();
this.ctx.strokeStyle = this.ORIGIN_STROKE;
this.ctx.fillStyle = this.ORIGIN_STROKE;
this.ctx.beginPath();
this.ctx.arc(x, y, 4, 0, 2 * Math.PI, false);
this.ctx.stroke();
this.ctx.fill();
this.ctx.restore();
},
/**
* Computes the largest width and height of all the given shapes and changes
* all of the shapes' points (by reference) so they fit into the configured
* MAX_DIM - 2*PAD area.
* @return {Object} A {w, h} giving the size the canvas should be
*/
_fitAllShapes: function(allShapes) {
let allXs = [], allYs = [];
for (let shape of allShapes) {
for (let point of shape) {
allXs.push(point[0]);
allYs.push(point[1]);
}
}
let minX = Math.min.apply(Math, allXs);
let maxX = Math.max.apply(Math, allXs);
let minY = Math.min.apply(Math, allYs);
let maxY = Math.max.apply(Math, allYs);
let spanX = maxX - minX;
let spanY = maxY - minY;
let isWide = spanX > spanY;
let cw = isWide ? this.MAX_DIM :
this.MAX_DIM * Math.min(spanX, spanY) / Math.max(spanX, spanY);
let ch = !isWide ? this.MAX_DIM :
this.MAX_DIM * Math.min(spanX, spanY) / Math.max(spanX, spanY);
let mapX = x => this.PAD + ((cw - 2 * this.PAD) / (maxX - minX)) * (x - minX);
let mapY = y => this.PAD + ((ch - 2 * this.PAD) / (maxY - minY)) * (y - minY);
for (let shape of allShapes) {
for (let point of shape) {
point[0] = mapX(point[0]);
point[1] = mapY(point[1]);
}
}
return {w: cw, h: ch};
},
_drawShapes: function(shape, corner, transformed, transformedCorner) {
this._drawOriginal(shape);
this._drawOriginalCorner(corner);
this._drawTransformed(transformed);
this._drawTransformedCorner(transformedCorner);
},
_drawOriginal: function(points) {
this._drawShape(points, this.ORIGINAL_FILL, this.ORIGINAL_STROKE);
},
_drawTransformed: function(points) {
this._drawShape(points, this.TRANSFORMED_FILL, this.TRANSFORMED_STROKE);
},
_drawOriginalCorner: function(points) {
this._drawShape(points, this.ORIGINAL_STROKE, this.ORIGINAL_STROKE);
},
_drawTransformedCorner: function(points) {
this._drawShape(points, this.TRANSFORMED_STROKE, this.TRANSFORMED_STROKE);
},
_drawArrows: function(shape, transformed) {
this._drawArrow(shape[0][0], shape[0][1], transformed[0][0], transformed[0][1]);
this._drawArrow(shape[1][0], shape[1][1], transformed[1][0], transformed[1][1]);
this._drawArrow(shape[2][0], shape[2][1], transformed[2][0], transformed[2][1]);
this._drawArrow(shape[3][0], shape[3][1], transformed[3][0], transformed[3][1]);
},
/**
* Draw a transform preview
*
* @param {String} transform
* The css transform value as a string, as typed by the user, as long
* as it can be computed by the browser
* @param {String} origin
* Same as above for the transform-origin value. Defaults to "center"
* @param {Number} width
* The width of the container. Defaults to 200
* @param {Number} height
* The height of the container. Defaults to 200
* @return {Boolean} Whether or not the preview could be created. Will return
* false for instance if the transform is invalid
*/
preview: function(transform, origin="center", width=200, height=200) {
// Create/clear the canvas
if (!this.canvas) {
this._createMarkup();
}
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// Get computed versions of transform and origin
transform = this._getComputed("transform", transform, width, height);
if (transform && transform !== "none") {
origin = this._getComputed("transform-origin", origin, width, height);
// Get the matrix, origin and width height data for the previewed element
let originData = this._getOriginFromOriginString(origin);
let matrixData = this._getMatrixFromTransformString(transform);
// Compute the original box rect and transformed box rect
let shapePoints = [
[0, 0],
[width, 0],
[width, height],
[0, height]
];
let transformedPoints = this._getTransformedPoints(matrixData, shapePoints, originData);
// Do the same for the corner triangle shape
let cornerSize = Math.min(shapePoints[2][1] - shapePoints[1][1],
shapePoints[1][0] - shapePoints[0][0]) / this.CORNER_SIZE_RATIO;
let cornerPoints = [
[shapePoints[1][0], shapePoints[1][1]],
[shapePoints[1][0], shapePoints[1][1] + cornerSize],
[shapePoints[1][0] - cornerSize, shapePoints[1][1]]
];
let transformedCornerPoints = this._getTransformedPoints(matrixData, cornerPoints, originData);
// Resize points to fit everything in the canvas
let {w, h} = this._fitAllShapes([
shapePoints,
transformedPoints,
cornerPoints,
transformedCornerPoints,
[originData]
]);
this.canvas.setAttribute("width", w);
this.canvas.setAttribute("height", h);
this._drawShapes(shapePoints, cornerPoints, transformedPoints, transformedCornerPoints)
this._drawArrows(shapePoints, transformedPoints);
this._drawOrigin(originData[0], originData[1]);
return true;
} else {
return false;
}
}
};

View File

@ -12,6 +12,7 @@ const {Spectrum} = require("devtools/shared/widgets/Spectrum");
const EventEmitter = require("devtools/shared/event-emitter");
const {colorUtils} = require("devtools/css-color");
const Heritage = require("sdk/core/heritage");
const {CSSTransformPreviewer} = require("devtools/shared/widgets/CSSTransformPreviewer");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
@ -100,7 +101,6 @@ let PanelFactory = {
panel.setAttribute("hidden", true);
panel.setAttribute("ignorekeys", true);
// Prevent the click used to close the panel from being consumed
panel.setAttribute("consumeoutsideclicks", options.get("consumeOutsideClick"));
panel.setAttribute("noautofocus", options.get("noAutoFocus"));
panel.setAttribute("type", "arrow");
@ -229,6 +229,10 @@ Tooltip.prototype = {
return this.panel.state !== "closed" && this.panel.state !== "hiding";
},
setSize: function(width, height) {
this.panel.sizeTo(width, height);
},
/**
* Empty the tooltip's content
*/
@ -304,7 +308,8 @@ Tooltip.prototype = {
* The container for all target nodes
* @param {Function} targetNodeCb
* A function that accepts a node argument and returns true or false
* to signify if the tooltip should be shown on that node or not.
* (or a promise that resolves or rejects) to signify if the tooltip
* should be shown on that node or not.
* Additionally, the function receives a second argument which is the
* tooltip instance itself, to be used to add/modify the content of the
* tooltip if needed. If omitted, the tooltip will be shown everytime.
@ -312,7 +317,7 @@ Tooltip.prototype = {
* An optional delay that will be observed before showing the tooltip.
* Defaults to this.defaultShowDelay.
*/
startTogglingOnHover: function(baseNode, targetNodeCb, showDelay = this.defaultShowDelay) {
startTogglingOnHover: function(baseNode, targetNodeCb, showDelay=this.defaultShowDelay) {
if (this._basedNode) {
this.stopTogglingOnHover();
}
@ -357,7 +362,12 @@ Tooltip.prototype = {
},
_showOnHover: function(target) {
if (this._targetNodeCb(target, this)) {
let res = this._targetNodeCb(target, this);
if (res && res.then) {
res.then(() => {
this.show(target);
});
} else if (res) {
this.show(target);
}
},
@ -527,6 +537,8 @@ Tooltip.prototype = {
let w = options.naturalWidth || imgObj.naturalWidth;
let h = options.naturalHeight || imgObj.naturalHeight;
label.textContent = w + " x " + h;
this.setSize(vbox.width, vbox.height);
}
},
@ -583,6 +595,54 @@ Tooltip.prototype = {
// Put the iframe in the tooltip
this.content = iframe;
return def.promise;
},
/**
* Set the content of the tooltip to be the result of CSSTransformPreviewer.
* Meaning a canvas previewing a css transformation.
*
* @param {String} transform
* The CSS transform value (e.g. "rotate(45deg) translateX(50px)")
* @param {PageStyleActor} pageStyle
* An instance of the PageStyleActor that will be used to retrieve
* computed styles
* @param {NodeActor} node
* The NodeActor for the currently selected node
* @return A promise that resolves when the tooltip content is ready, or
* rejects if no transform is provided or is invalid
*/
setCssTransformContent: function(transform, pageStyle, node) {
let def = promise.defer();
if (transform) {
// Look into the computed styles to find the width and height and possibly
// the origin if it hadn't been provided
pageStyle.getComputed(node, {
filter: "user",
markMatched: false,
onlyMatched: false
}).then(styles => {
let origin = styles["transform-origin"].value;
let width = parseInt(styles["width"].value);
let height = parseInt(styles["height"].value);
let root = this.doc.createElementNS(XHTML_NS, "div");
let previewer = new CSSTransformPreviewer(root);
this.content = root;
if (!previewer.preview(transform, origin, width, height)) {
// If the preview didn't work, reject the promise
def.reject();
} else {
// Else, make sure the tooltip has the right size and resolve
this.setSize(previewer.canvas.width, previewer.canvas.height);
def.resolve();
}
});
} else {
def.reject();
}
return def.promise;
}
};

View File

@ -507,21 +507,27 @@ CssHtmlTree.prototype = {
*/
_buildTooltipContent: function(target)
{
// If the hovered element is not a property view and is not a background
// image, then don't show a tooltip
let isPropertyValue = target.classList.contains("property-value");
if (!isPropertyValue) {
return false;
}
let propName = target.parentNode.querySelector(".property-name");
let isBackgroundImage = propName.textContent === "background-image";
if (!isBackgroundImage) {
return false;
// Test for image url
if (target.classList.contains("theme-link")) {
let propValue = target.parentNode;
let propName = propValue.parentNode.querySelector(".property-name");
if (propName.textContent === "background-image") {
this.tooltip.setCssBackgroundImageContent(propValue.textContent);
return true;
}
}
// Fill some content
this.tooltip.setCssBackgroundImageContent(target.textContent);
return true;
// Test for css transform
if (target.classList.contains("property-value")) {
let def = promise.defer();
let propValue = target;
let propName = target.parentNode.querySelector(".property-name");
if (propName.textContent === "transform") {
this.tooltip.setCssTransformContent(propValue.textContent,
this.pageStyle, this.viewedElement).then(def.resolve);
return def.promise;
}
}
},
/**

View File

@ -1154,27 +1154,32 @@ CssRuleView.prototype = {
* prepare some content for the tooltip
*/
_buildTooltipContent: function(target) {
let isImageHref = target.classList.contains("theme-link") &&
target.parentNode.classList.contains("ruleview-propertyvalue");
let property = target.textProperty, def = promise.defer(), hasTooltip = false;
// If the inplace-editor is visible or if this is not a background image
// don't show the tooltip
if (!isImageHref) {
return false;
// Test for css transform
if (property && property.name === "transform") {
this.previewTooltip.setCssTransformContent(property.value, this.pageStyle,
this._viewedElement).then(def.resolve);
hasTooltip = true;
}
// Retrieve the TextProperty for the hovered element
let property = target.parentNode.textProperty;
let href = property.rule.domRule.href;
// Test for image
let isImageHref = target.classList.contains("theme-link") &&
target.parentNode.classList.contains("ruleview-propertyvalue");
if (isImageHref) {
property = target.parentNode.textProperty;
this.previewTooltip.setCssBackgroundImageContent(property.value,
property.rule.domRule.href);
def.resolve();
hasTooltip = true;
}
// Fill some content
this.previewTooltip.setCssBackgroundImageContent(property.value, href);
if (hasTooltip) {
this.colorPicker.revert();
this.colorPicker.hide();
}
// Hide the color picker tooltip if shown and revert changes
this.colorPicker.revert();
this.colorPicker.hide();
return true;
return def.promise;
},
/**
@ -1330,7 +1335,7 @@ CssRuleView.prototype = {
/**
* Update the highlighted element.
*
* @param {nsIDOMElement} aElement
* @param {NodeActor} aElement
* The node whose style rules we'll inspect.
*/
highlight: function CssRuleView_highlight(aElement)

View File

@ -53,6 +53,8 @@ support-files = browser_ruleview_pseudoelement.html
[browser_bug913014_matched_expand.js]
[browser_bug765105_background_image_tooltip.js]
[browser_bug889638_rule_view_color_picker.js]
[browser_bug726427_csstransform_tooltip.js]
[browser_bug940500_rule_view_pick_gradient_color.js]
[browser_ruleview_original_source_link.js]
support-files =

View File

@ -0,0 +1,206 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
let contentDoc;
let inspector;
let ruleView;
let computedView;
const PAGE_CONTENT = [
'<style type="text/css">',
' #testElement {',
' width: 500px;',
' height: 300px;',
' background: red;',
' transform: skew(16deg);',
' }',
' .test-element {',
' transform-origin: top left;',
' transform: rotate(45deg);',
' }',
' div {',
' transform: scaleX(1.5);',
' transform-origin: bottom right;',
' }',
' [attr] {',
' }',
'</style>',
'<div id="testElement" class="test-element" attr="value">transformed element</div>'
].join("\n");
function test() {
waitForExplicitFinish();
gBrowser.selectedTab = gBrowser.addTab();
gBrowser.selectedBrowser.addEventListener("load", function(evt) {
gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee, true);
contentDoc = content.document;
waitForFocus(createDocument, content);
}, true);
content.location = "data:text/html,rule view css transform tooltip test";
}
function createDocument() {
contentDoc.body.innerHTML = PAGE_CONTENT;
openRuleView((aInspector, aRuleView) => {
inspector = aInspector;
ruleView = aRuleView;
startTests();
});
}
function startTests() {
inspector.selection.setNode(contentDoc.querySelector("#testElement"));
inspector.once("inspector-updated", testTransformTooltipOnIDSelector);
}
function endTests() {
contentDoc = inspector = ruleView = computedView = null;
gBrowser.removeCurrentTab();
finish();
}
function testTransformTooltipOnIDSelector() {
info("Testing that a transform tooltip appears on the #ID rule");
let panel = ruleView.previewTooltip.panel;
ok(panel, "The XUL panel exists for the rule-view preview tooltips");
let {valueSpan} = getRuleViewProperty("#testElement", "transform");
assertTooltipShownOn(ruleView.previewTooltip, valueSpan, () => {
// The transform preview is canvas, so there's not much we can test, so for
// now, let's just be happy with the fact that the tooltips is shown!
ok(true, "Tooltip shown on the transform property of the #ID rule");
ruleView.previewTooltip.hide();
executeSoon(testTransformTooltipOnClassSelector);
});
}
function testTransformTooltipOnClassSelector() {
info("Testing that a transform tooltip appears on the .class rule");
let {valueSpan} = getRuleViewProperty(".test-element", "transform");
assertTooltipShownOn(ruleView.previewTooltip, valueSpan, () => {
// The transform preview is canvas, so there's not much we can test, so for
// now, let's just be happy with the fact that the tooltips is shown!
ok(true, "Tooltip shown on the transform property of the .class rule");
ruleView.previewTooltip.hide();
executeSoon(testTransformTooltipOnTagSelector);
});
}
function testTransformTooltipOnTagSelector() {
info("Testing that a transform tooltip appears on the tag rule");
let {valueSpan} = getRuleViewProperty("div", "transform");
assertTooltipShownOn(ruleView.previewTooltip, valueSpan, () => {
// The transform preview is canvas, so there's not much we can test, so for
// now, let's just be happy with the fact that the tooltips is shown!
ok(true, "Tooltip shown on the transform property of the tag rule");
ruleView.previewTooltip.hide();
executeSoon(testTransformTooltipNotShownOnInvalidTransform);
});
}
function testTransformTooltipNotShownOnInvalidTransform() {
info("Testing that a transform tooltip does not appear for invalid values");
let ruleEditor;
for (let rule of ruleView._elementStyle.rules) {
if (rule.matchedSelectors[0] === "[attr]") {
ruleEditor = rule.editor;
}
}
ruleEditor.addProperty("transform", "muchTransform(suchAngle)", "");
let {valueSpan} = getRuleViewProperty("[attr]", "transform");
assertTooltipNotShownOn(ruleView.previewTooltip, valueSpan, () => {
executeSoon(testTransformTooltipOnComputedView);
});
}
function testTransformTooltipOnComputedView() {
info("Testing that a transform tooltip appears in the computed view too");
inspector.sidebar.select("computedview");
computedView = inspector.sidebar.getWindowForTab("computedview").computedview.view;
let doc = computedView.styleDocument;
let panel = computedView.tooltip.panel;
let {valueSpan} = getComputedViewProperty("transform");
assertTooltipShownOn(computedView.tooltip, valueSpan, () => {
// The transform preview is canvas, so there's not much we can test, so for
// now, let's just be happy with the fact that the tooltips is shown!
ok(true, "Tooltip shown on the computed transform property");
computedView.tooltip.hide();
executeSoon(endTests);
});
}
function assertTooltipShownOn(tooltip, element, cb) {
// If there is indeed a show-on-hover on element, the xul panel will be shown
tooltip.panel.addEventListener("popupshown", function shown() {
tooltip.panel.removeEventListener("popupshown", shown, true);
cb();
}, true);
tooltip._showOnHover(element);
}
function assertTooltipNotShownOn(tooltip, element, cb) {
// The only way to make sure the tooltip is not shown is try and show it, wait
// for a given amount of time, and then check if it's shown or not
tooltip._showOnHover(element);
setTimeout(() => {
ok(!tooltip.isShown(), "The tooltip did not appear on hover of the element");
cb();
}, tooltip.defaultShowDelay + 100);
}
function getRule(selectorText) {
let rule;
[].forEach.call(ruleView.doc.querySelectorAll(".ruleview-rule"), aRule => {
let selector = aRule.querySelector(".ruleview-selector-matched");
if (selector && selector.textContent === selectorText) {
rule = aRule;
}
});
return rule;
}
function getRuleViewProperty(selectorText, propertyName) {
let prop;
let rule = getRule(selectorText);
if (rule) {
// Look for the propertyName in that rule element
[].forEach.call(rule.querySelectorAll(".ruleview-property"), property => {
let nameSpan = property.querySelector(".ruleview-propertyname");
let valueSpan = property.querySelector(".ruleview-propertyvalue");
if (nameSpan.textContent === propertyName) {
prop = {nameSpan: nameSpan, valueSpan: valueSpan};
}
});
}
return prop;
}
function getComputedViewProperty(name) {
let prop;
[].forEach.call(computedView.styleDocument.querySelectorAll(".property-view"), property => {
let nameSpan = property.querySelector(".property-name");
let valueSpan = property.querySelector(".property-value");
if (nameSpan.textContent === name) {
prop = {nameSpan: nameSpan, valueSpan: valueSpan};
}
});
return prop;
}

View File

@ -142,8 +142,9 @@ function testComputedView() {
let panel = computedView.tooltip.panel;
let {valueSpan} = getComputedViewProperty("background-image");
let uriSpan = valueSpan.querySelector(".theme-link");
assertTooltipShownOn(computedView.tooltip, valueSpan, () => {
assertTooltipShownOn(computedView.tooltip, uriSpan, () => {
let images = panel.getElementsByTagName("image");
is(images.length, 1, "Tooltip contains an image");
ok(images[0].src === "chrome://global/skin/icons/warning-64.png");

View File

@ -3,6 +3,17 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<!ENTITY aboutDialog.title "About &brandFullName;">
<!-- LOCALIZATION NOTE update.applyButton.*, update.upgradeButton.*):
# Only one button is present at a time.
# The button when displayed is located directly under the Firefox version in
# the about dialog (see bug 596813 for screenshots).
-->
<!ENTITY update.updateButton.label "Restart to Update">
<!ENTITY update.updateButton.accesskey "R">
<!ENTITY update.applyButtonBillboard.label "Apply Update…">
<!ENTITY update.applyButtonBillboard.accesskey "A">
<!-- LOCALIZATION NOTE (warningDesc.version): This is a warning about the experimental nature of Nightly and Aurora builds. It is only shown in those versions. -->
<!ENTITY warningDesc.version "&brandShortName; is experimental and may be unstable.">
<!-- LOCALIZATION NOTE (warningDesc.telemetryDesc): This is a notification that Nightly/Aurora builds automatically send Telemetry data back to Mozilla. It is only shown in those versions. "It" refers to brandShortName. -->
@ -41,8 +52,6 @@
<!-- LOCALIZATION NOTE (update.checkingForUpdates): try to make the localized text short (see bug 596813 for screenshots). -->
<!ENTITY update.checkingForUpdates "Checking for updates…">
<!-- LOCALIZATION NOTE (update.checkingAddonCompat): try to make the localized text short (see bug 596813 for screenshots). -->
<!ENTITY update.checkingAddonCompat "Checking Add-on compatibility…">
<!-- LOCALIZATION NOTE (update.noUpdatesFound): try to make the localized text short (see bug 596813 for screenshots). -->
<!ENTITY update.noUpdatesFound "&brandShortName; is up to date">
<!-- LOCALIZATION NOTE (update.adminDisabled): try to make the localized text short (see bug 596813 for screenshots). -->

View File

@ -191,24 +191,10 @@ sanitizeEverythingWarning2=All history will be cleared.
# provided that the user has modified the default set of history items to clear.
sanitizeSelectedWarning=All selected items will be cleared.
# Check for Updates in the About Dialog - button labels and accesskeys
# LOCALIZATION NOTE - all of the following update buttons labels will only be
# displayed one at a time. So, if a button is displayed nothing else will
# be displayed alongside of the button. The button when displayed is located
# directly under the Firefox version in the about dialog (see bug 596813 for
# screenshots).
update.checkInsideButton.label=Check for Updates
update.checkInsideButton.accesskey=C
update.resumeButton.label=Resume Downloading %S…
update.resumeButton.accesskey=D
update.openUpdateUI.applyButton.label=Apply Update…
update.openUpdateUI.applyButton.accesskey=A
update.restart.updateButton.label=Restart to Update
update.restart.updateButton.accesskey=R
update.openUpdateUI.upgradeButton.label=Upgrade Now…
update.openUpdateUI.upgradeButton.accesskey=U
update.restart.upgradeButton.label=Upgrade Now
update.restart.upgradeButton.accesskey=U
# LOCALIZATION NOTE (downloadAndInstallButton.label): %S is replaced by the
# version of the update: "Update to 28.0".
update.downloadAndInstallButton.label=Update to %S
update.downloadAndInstallButton.accesskey=U
# RSS Pretty Print
feedShowFeedNew=Subscribe to '%S'…

View File

@ -166,7 +166,7 @@ pref("dom.experimental_forms", true);
pref("dom.forms.number", true);
// Don't enable <input type=color> yet as we don't have a color picker
// implemented for Android (bug 875750)
pref("dom.forms.color", false);
pref("dom.forms.color", true);
/* extension manager and xpinstall */
pref("xpinstall.whitelist.add", "addons.mozilla.org");

View File

@ -1515,9 +1515,13 @@ public class GeckoAppShell
public static boolean isNetworkLinkUp() {
ConnectivityManager cm = (ConnectivityManager)
getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo info = cm.getActiveNetworkInfo();
if (info == null || !info.isConnected())
try {
NetworkInfo info = cm.getActiveNetworkInfo();
if (info == null || !info.isConnected())
return false;
} catch (SecurityException se) {
return false;
}
return true;
}
@ -1525,8 +1529,12 @@ public class GeckoAppShell
public static boolean isNetworkLinkKnown() {
ConnectivityManager cm = (ConnectivityManager)
getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
if (cm.getActiveNetworkInfo() == null)
try {
if (cm.getActiveNetworkInfo() == null)
return false;
} catch (SecurityException se) {
return false;
}
return true;
}

View File

@ -220,7 +220,11 @@ public class GeckoNetworkManager extends BroadcastReceiver {
return NetworkType.NETWORK_NONE;
}
NetworkInfo ni = cm.getActiveNetworkInfo();
NetworkInfo ni = null;
try {
ni = cm.getActiveNetworkInfo();
} catch (SecurityException se) {} // if we don't have the permission, fall through to null check
if (ni == null) {
return NetworkType.NETWORK_NONE;
}

View File

@ -266,6 +266,7 @@ gbjar.sources += [
'preferences/SyncPreference.java',
'PrefsHelper.java',
'PrivateTab.java',
'prompts/ColorPickerInput.java',
'prompts/IconGridInput.java',
'prompts/Prompt.java',
'prompts/PromptInput.java',
@ -314,6 +315,7 @@ gbjar.sources += [
'widget/AllCapsTextView.java',
'widget/AnimatedHeightLayout.java',
'widget/ArrowPopup.java',
'widget/BasicColorPicker.java',
'widget/ButtonToast.java',
'widget/CheckableLinearLayout.java',
'widget/ClickableWhenDisabledEditText.java',
@ -430,6 +432,7 @@ ANDROID_RESFILES += [
'resources/drawable-hdpi/bookmark_folder_closed.png',
'resources/drawable-hdpi/bookmark_folder_opened.png',
'resources/drawable-hdpi/close.png',
'resources/drawable-hdpi/color_picker_row_bg.9.png',
'resources/drawable-hdpi/copy.png',
'resources/drawable-hdpi/cut.png',
'resources/drawable-hdpi/favicon.png',
@ -578,6 +581,7 @@ ANDROID_RESFILES += [
'resources/drawable-mdpi/bookmarkdefaults_favicon_addons.png',
'resources/drawable-mdpi/bookmarkdefaults_favicon_support.png',
'resources/drawable-mdpi/close.png',
'resources/drawable-mdpi/color_picker_row_bg.9.png',
'resources/drawable-mdpi/copy.png',
'resources/drawable-mdpi/cut.png',
'resources/drawable-mdpi/desktop_notification.png',
@ -718,6 +722,7 @@ ANDROID_RESFILES += [
'resources/drawable-xhdpi/bookmark_folder_closed.png',
'resources/drawable-xhdpi/bookmark_folder_opened.png',
'resources/drawable-xhdpi/close.png',
'resources/drawable-xhdpi/color_picker_row_bg.9.png',
'resources/drawable-xhdpi/copy.png',
'resources/drawable-xhdpi/cut.png',
'resources/drawable-xhdpi/favicon.png',
@ -818,6 +823,7 @@ ANDROID_RESFILES += [
'resources/drawable/action_bar_button.xml',
'resources/drawable/action_bar_button_inverse.xml',
'resources/drawable/bookmark_folder.xml',
'resources/drawable/color_picker_checkmark.xml',
'resources/drawable/divider_horizontal.xml',
'resources/drawable/divider_vertical.xml',
'resources/drawable/handle_end_level.xml',
@ -867,11 +873,13 @@ ANDROID_RESFILES += [
'resources/layout/arrow_popup.xml',
'resources/layout/autocomplete_list.xml',
'resources/layout/autocomplete_list_item.xml',
'resources/layout/basic_color_picker_dialog.xml',
'resources/layout/bookmark_edit.xml',
'resources/layout/bookmark_folder_row.xml',
'resources/layout/bookmark_item_row.xml',
'resources/layout/browser_search.xml',
'resources/layout/browser_toolbar.xml',
'resources/layout/color_picker_row.xml',
'resources/layout/datetime_picker.xml',
'resources/layout/doorhanger.xml',
'resources/layout/doorhanger_button.xml',

View File

@ -0,0 +1,63 @@
/* -*- 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.prompts;
import org.mozilla.gecko.R;
import org.mozilla.gecko.widget.BasicColorPicker;
import org.json.JSONObject;
import android.content.Context;
import android.graphics.Color;
import android.view.View;
import android.view.LayoutInflater;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.LinearLayout.LayoutParams;
public class ColorPickerInput extends PromptInput {
public static final String INPUT_TYPE = "color";
public static final String LOGTAG = "GeckoColorPickerInput";
private boolean mShowAdvancedButton = true;
private int mInitialColor;
public ColorPickerInput(JSONObject obj) {
super(obj);
String init = obj.optString("value");
mInitialColor = Color.rgb(Integer.parseInt(init.substring(1,3), 16),
Integer.parseInt(init.substring(3,5), 16),
Integer.parseInt(init.substring(5,7), 16));
}
@Override
public View getView(Context context) throws UnsupportedOperationException {
LayoutInflater inflater = LayoutInflater.from(context);
mView = inflater.inflate(R.layout.basic_color_picker_dialog, null);
BasicColorPicker cp = (BasicColorPicker) mView.findViewById(R.id.colorpicker);
cp.setColor(mInitialColor);
return mView;
}
@Override
public String getValue() {
BasicColorPicker cp = (BasicColorPicker) mView.findViewById(R.id.colorpicker);
int color = cp.getColor();
return "#" + Integer.toHexString(color).substring(2);
}
@Override
public boolean getScrollable() {
return true;
}
@Override
public boolean canApplyInputStyle() {
return false;
}
}

View File

@ -9,6 +9,7 @@ import org.mozilla.gecko.util.GeckoEventResponder;
import org.mozilla.gecko.GeckoEvent;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.widget.DateTimePicker;
import org.mozilla.gecko.prompts.ColorPickerInput;
import org.mozilla.gecko.R;
import org.mozilla.gecko.GeckoAppShell;
@ -108,8 +109,11 @@ public class Prompt implements OnClickListener, OnCancelListener, OnItemClickLis
}
}
private View applyInputStyle(View view) {
view.setPadding(mInputPaddingSize, 0, mInputPaddingSize, 0);
private View applyInputStyle(View view, PromptInput input) {
// Don't add padding to color picker views
if (input.canApplyInputStyle()) {
view.setPadding(mInputPaddingSize, 0, mInputPaddingSize, 0);
}
return view;
}
@ -333,12 +337,14 @@ public class Prompt implements OnClickListener, OnCancelListener, OnItemClickLis
if (length == 1) {
root = mInputs[0].getView(mContext);
applyInputStyle(root, mInputs[0]);
scrollable |= mInputs[0].getScrollable();
} else if (length > 1) {
LinearLayout linearLayout = new LinearLayout(mContext);
linearLayout.setOrientation(LinearLayout.VERTICAL);
for (int i = 0; i < length; i++) {
View content = mInputs[i].getView(mContext);
applyInputStyle(content, mInputs[i]);
linearLayout.addView(content);
scrollable |= mInputs[i].getScrollable();
}
@ -346,11 +352,11 @@ public class Prompt implements OnClickListener, OnCancelListener, OnItemClickLis
}
if (scrollable) {
builder.setView(applyInputStyle(root));
builder.setView(root);
} else {
ScrollView view = new ScrollView(mContext);
view.addView(root);
builder.setView(applyInputStyle(view));
builder.setView(view);
}
} catch(Exception ex) {
Log.e(LOGTAG, "Error showing prompt inputs", ex);

View File

@ -350,6 +350,8 @@ public class PromptInput {
return new LabelInput(obj);
} else if (IconGridInput.INPUT_TYPE.equals(type)) {
return new IconGridInput(obj);
} else if (ColorPickerInput.INPUT_TYPE.equals(type)) {
return new ColorPickerInput(obj);
} else {
for (String dtType : DateTimeInput.INPUT_TYPES) {
if (dtType.equals(type)) {
@ -375,4 +377,8 @@ public class PromptInput {
public boolean getScrollable() {
return false;
}
public boolean canApplyInputStyle() {
return true;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 B

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="ring"
android:innerRadius="15dip"
android:thickness="4dip"
android:useLevel="false">
<solid android:color="@android:color/white"/>
</shape>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:orientation="vertical">
<org.mozilla.gecko.widget.BasicColorPicker android:id="@+id/colorpicker"
android:layout_height="0dip"
android:layout_weight="1"
android:drawSelectorOnTop="true"
android:choiceMode="singleChoice"
android:divider="@android:color/transparent"
android:dividerHeight="0dip"
android:listSelector="#22FFFFFF"
android:layout_width="fill_parent"/>
</LinearLayout>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.Widget.TextView"
style="@style/Widget.ListItem"
android:background="@drawable/color_picker_row_bg"
android:checkMark="@android:color/transparent"/>

View File

@ -0,0 +1,139 @@
/* -*- 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.widget;
import org.mozilla.gecko.R;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.ArrayAdapter;
import android.widget.AdapterView;
import android.widget.CheckedTextView;
import android.widget.ListView;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.TypedValue;
public class BasicColorPicker extends ListView {
private final static String LOGTAG = "GeckoBasicColorPicker";
private final static List<Integer> DEFAULT_COLORS = Arrays.asList(Color.rgb(215,57,32),
Color.rgb(255,134,5),
Color.rgb(255,203,19),
Color.rgb(95,173,71),
Color.rgb(84,201,168),
Color.rgb(33,161,222),
Color.rgb(16,36,87),
Color.rgb(91,32,103),
Color.rgb(212,221,228),
Color.BLACK);
private static Drawable mCheckDrawable = null;
private int mSelected = 0;
final private ColorPickerListAdapter mAdapter;
public BasicColorPicker(Context context) {
this(context, null);
}
public BasicColorPicker(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BasicColorPicker(Context context, AttributeSet attrs, int style) {
this(context, attrs, style, DEFAULT_COLORS);
}
public BasicColorPicker(Context context, AttributeSet attrs, int style, List<Integer> colors) {
super(context, attrs, style);
mAdapter = new ColorPickerListAdapter(context, new ArrayList<Integer>(colors));
setAdapter(mAdapter);
setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
mSelected = position;
mAdapter.notifyDataSetChanged();
}
});
}
public int getColor() {
return mAdapter.getItem(mSelected);
}
public void setColor(int color) {
if (!DEFAULT_COLORS.contains(color)) {
mSelected = mAdapter.getCount();
mAdapter.add(color);
} else {
mSelected = DEFAULT_COLORS.indexOf(color);
}
setSelection(mSelected);
mAdapter.notifyDataSetChanged();
}
private Drawable getCheckDrawable() {
if (mCheckDrawable == null) {
Resources res = getContext().getResources();
TypedValue typedValue = new TypedValue();
getContext().getTheme().resolveAttribute(android.R.attr.listPreferredItemHeight, typedValue, true);
DisplayMetrics metrics = new android.util.DisplayMetrics();
((WindowManager)getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getMetrics(metrics);
int height = (int) typedValue.getDimension(metrics);
Drawable background = res.getDrawable(R.drawable.color_picker_row_bg);
Rect r = new Rect();
background.getPadding(r);
height -= r.top + r.bottom;
mCheckDrawable = res.getDrawable(R.drawable.color_picker_checkmark);
mCheckDrawable.setBounds(0, 0, height, height);
}
return mCheckDrawable;
}
private class ColorPickerListAdapter extends ArrayAdapter<Integer> {
private final List<Integer> mColors;
public ColorPickerListAdapter(Context context, List<Integer> colors) {
super(context, R.layout.color_picker_row, colors);
mColors = colors;
}
public View getView(int position, View convertView, ViewGroup parent) {
View v = super.getView(position, convertView, parent);
Drawable d = v.getBackground();
d.setColorFilter(getItem(position), PorterDuff.Mode.MULTIPLY);
v.setBackground(d);
Drawable check = null;
CheckedTextView checked = ((CheckedTextView) v);
if (mSelected == position) {
check = getCheckDrawable();
}
checked.setCompoundDrawables(check, null, null, null);
checked.setText("");
return v;
}
}
}

View File

@ -0,0 +1,51 @@
/* 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/. */
const Ci = Components.interfaces;
const Cu = Components.utils;
const Cc = Components.classes;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Prompt.jsm");
function ColorPicker() {
}
ColorPicker.prototype = {
_initial: 0,
_domWin: null,
_title: "",
get strings() {
delete this.strings;
return this.strings = Services.strings.createBundle("chrome://browser/locale/browser.properties");
},
init: function(aParent, aTitle, aInitial) {
this._domWin = aParent;
this._initial = aInitial;
this._title = aTitle;
},
open: function(aCallback) {
let p = new Prompt({ title: this._title,
buttons: [
this.strings.GetStringFromName("inputWidgetHelper.set"),
this.strings.GetStringFromName("inputWidgetHelper.cancel")
] })
.addColorPicker({ value: this._initial })
.show((data) => {
if (data.button == 0)
aCallback.done(data.color0);
else
aCallback.done(this._initial);
});
},
classID: Components.ID("{430b987f-bb9f-46a3-99a5-241749220b29}"),
QueryInterface: XPCOMUtils.generateQI([Ci.nsIColorPicker])
};
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ColorPicker]);

View File

@ -105,3 +105,7 @@ component {a78d7e59-b558-4321-a3d6-dffe2f1e76dd} Snippets.js
contract @mozilla.org/snippets;1 {a78d7e59-b558-4321-a3d6-dffe2f1e76dd}
category profile-after-change Snippets @mozilla.org/snippets;1
category update-timer Snippets @mozilla.org/snippets;1,getService,snippets-update-timer,browser.snippets.updateInterval,86400
# ColorPicker.js
component {430b987f-bb9f-46a3-99a5-241749220b29} ColorPicker.js
contract @mozilla.org/colorpicker;1 {430b987f-bb9f-46a3-99a5-241749220b29}

View File

@ -13,6 +13,7 @@ XPIDL_MODULE = 'MobileComponents'
EXTRA_COMPONENTS += [
'AddonUpdateService.js',
'BlocklistPrompt.js',
'ColorPicker.js',
'ContentDispatchChooser.js',
'ContentPermissionPrompt.js',
'DownloadManagerUI.js',

View File

@ -558,6 +558,7 @@ bin/components/@DLL_PREFIX@nkgnomevfs@DLL_SUFFIX@
@BINPATH@/components/AddonUpdateService.js
@BINPATH@/components/BlocklistPrompt.js
@BINPATH@/components/BrowserCLH.js
@BINPATH@/components/ColorPicker.js
@BINPATH@/components/ContentDispatchChooser.js
@BINPATH@/components/ContentPermissionPrompt.js
@BINPATH@/components/DirectoryProvider.js

View File

@ -117,6 +117,14 @@ Prompt.prototype = {
});
},
addColorPicker: function(aOptions) {
return this._addInput({
type: "color",
value: aOptions.value,
id: aOptions.id
});
},
addLabel: function(aOptions) {
return this._addInput({
type: "label",

View File

@ -397,6 +397,10 @@ nsLookAndFeel::GetIntImpl(IntID aID, int32_t &aResult)
aResult = 1;
break;
case eIntID_ColorPickerAvailable:
aResult = 1;
break;
case eIntID_WindowsDefaultTheme:
case eIntID_WindowsThemeIdentifier:
case eIntID_OperatingSystemVersionIdentifier: