merge fx-team to mozilla-central a=merge

--HG--
rename : devtools/client/shared/css-color.js => devtools/shared/css-color.js
This commit is contained in:
Carsten "Tomcat" Book 2016-07-25 15:49:05 +02:00
Родитель f3cbadf7c4 2c684e6c97
Коммит 42933ba381
126 изменённых файлов: 2708 добавлений и 2143 удалений

Просмотреть файл

@ -113,6 +113,8 @@ devtools/server/**
!devtools/server/child.js
!devtools/server/css-logic.js
!devtools/server/main.js
!devtools/server/actors/inspector.js
!devtools/server/actors/highlighters/eye-dropper.js
!devtools/server/actors/webbrowser.js
!devtools/server/actors/styles.js
!devtools/server/actors/string.js

Просмотреть файл

@ -1463,5 +1463,5 @@ pref("signon.schemeUpgrades", true);
pref("print.use_simplify_page", true);
// Space separated list of URLS that are allowed to send objects (instead of
// only strings) through webchannels.
// only strings) through webchannels. This list is duplicated in mobile/android/app/mobile.js
pref("webchannel.allowObject.urlWhitelist", "https://accounts.firefox.com https://content.cdn.mozilla.net https://hello.firefox.com https://input.mozilla.org https://support.mozilla.org https://install.mozilla.org");

Просмотреть файл

@ -407,10 +407,12 @@ var gFxAccounts = {
}
// "All devices" menu item
const separator = document.createElement("menuseparator");
fragment.appendChild(separator);
const allDevicesLabel = this.strings.GetStringFromName("sendTabToAllDevices.menuitem");
addTargetDevice("", allDevicesLabel);
if (clients.length > 1) {
const separator = document.createElement("menuseparator");
fragment.appendChild(separator);
const allDevicesLabel = this.strings.GetStringFromName("sendTabToAllDevices.menuitem");
addTargetDevice("", allDevicesLabel);
}
devicesPopup.appendChild(fragment);
},

Просмотреть файл

@ -7761,21 +7761,6 @@ XPCOMUtils.defineLazyGetter(ResponsiveUI, "ResponsiveUIManager", function() {
return tmp.ResponsiveUIManager;
});
function openEyedropper() {
var eyedropper = new this.Eyedropper(this, { context: "menu",
copyOnSelect: true });
eyedropper.open();
}
Object.defineProperty(this, "Eyedropper", {
get: function() {
let devtools = Cu.import("resource://devtools/shared/Loader.jsm", {}).devtools;
return devtools.require("devtools/client/eyedropper/eyedropper").Eyedropper;
},
configurable: true,
enumerable: true
});
XPCOMUtils.defineLazyGetter(window, "gShowPageResizers", function () {
// Only show resizers on Windows 2000 and XP
return AppConstants.isPlatformAndVersionAtMost("win", "5.9");

Просмотреть файл

@ -42,44 +42,46 @@ function getSender(context, target, sender) {
}
}
// WeakMap[ExtensionContext -> {tab, parentWindow}]
var pageDataMap = new WeakMap();
function getDocShellOwner(docShell) {
let browser = docShell.chromeEventHandler;
let xulWindow = browser.ownerGlobal;
let {gBrowser} = xulWindow;
if (gBrowser) {
let tab = gBrowser.getTabForBrowser(browser);
return {xulWindow, tab};
}
return {};
}
/* eslint-disable mozilla/balanced-listeners */
// This listener fires whenever an extension page opens in a tab
// (either initiated by the extension or the user). Its job is to fill
// in some tab-specific details and keep data around about the
// ExtensionContext.
extensions.on("page-load", (type, page, params, sender, delegate) => {
extensions.on("page-load", (type, context, params, sender, delegate) => {
if (params.type == "tab" || params.type == "popup") {
let browser = params.docShell.chromeEventHandler;
let {xulWindow, tab} = getDocShellOwner(params.docShell);
let parentWindow = browser.ownerGlobal;
page.windowId = WindowManager.getId(parentWindow);
let tab = parentWindow.gBrowser.getTabForBrowser(browser);
// FIXME: Handle tabs being moved between windows.
context.windowId = WindowManager.getId(xulWindow);
if (tab) {
sender.tabId = TabManager.getId(tab);
page.tabId = TabManager.getId(tab);
context.tabId = TabManager.getId(tab);
}
pageDataMap.set(page, {tab, parentWindow});
}
delegate.getSender = getSender;
});
extensions.on("page-unload", (type, page) => {
pageDataMap.delete(page);
});
extensions.on("page-shutdown", (type, page) => {
if (pageDataMap.has(page)) {
let {tab, parentWindow} = pageDataMap.get(page);
pageDataMap.delete(page);
extensions.on("page-shutdown", (type, context) => {
if (context.type == "tab") {
let {xulWindow, tab} = getDocShellOwner(context.docShell);
if (tab) {
parentWindow.gBrowser.removeTab(tab);
xulWindow.gBrowser.removeTab(tab);
}
}
});
@ -96,9 +98,9 @@ extensions.on("fill-browser-data", (type, browser, data, result) => {
/* eslint-enable mozilla/balanced-listeners */
global.currentWindow = function(context) {
let pageData = pageDataMap.get(context);
if (pageData) {
return pageData.parentWindow;
let {xulWindow} = getDocShellOwner(context.docShell);
if (xulWindow) {
return xulWindow;
}
return WindowManager.topWindow;
};

Просмотреть файл

@ -43,7 +43,10 @@ add_task(function* () {
});
});
browser.tabs.executeScript({file: "script.js"});
browser.tabs.executeScript({file: "script.js"}).catch(e => {
browser.test.fail(`Error: ${e} :: ${e.stack}`);
browser.test.notifyFail("contentscript_connect.pass");
});
},
files: {

Просмотреть файл

@ -257,3 +257,66 @@ add_task(function* test_options_no_manifest() {
yield extension.awaitFinish("options-no-manifest");
yield extension.unload();
});
add_task(function* test_inline_options_uninstall() {
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
let extension = yield loadExtension({
manifest: {
"options_ui": {
"page": "options.html",
},
},
background: function() {
let _optionsPromise;
let awaitOptions = () => {
browser.test.assertFalse(_optionsPromise, "Should not be awaiting options already");
return new Promise(resolve => {
_optionsPromise = {resolve};
});
};
browser.runtime.onMessage.addListener((msg, sender) => {
if (msg == "options.html") {
if (_optionsPromise) {
_optionsPromise.resolve(sender.tab);
_optionsPromise = null;
} else {
browser.test.fail("Saw unexpected options page load");
}
}
});
let firstTab;
browser.tabs.query({currentWindow: true, active: true}).then(tabs => {
firstTab = tabs[0].id;
browser.test.log("Open options page. Expect fresh load.");
return Promise.all([
browser.runtime.openOptionsPage(),
awaitOptions(),
]);
}).then(([, tab]) => {
browser.test.assertEq("about:addons", tab.url, "Tab contains AddonManager");
browser.test.assertTrue(tab.active, "Tab is active");
browser.test.assertTrue(tab.id != firstTab, "Tab is a new tab");
browser.test.sendMessage("options-ui-open");
}).catch(error => {
browser.test.fail(`Error: ${error} :: ${error.stack}`);
});
},
});
yield extension.awaitMessage("options-ui-open");
yield extension.unload();
is(gBrowser.selectedBrowser.currentURI.spec, "about:addons",
"Add-on manager tab should still be open");
yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
yield BrowserTestUtils.removeTab(tab);
});

Просмотреть файл

@ -1,3 +1,3 @@
This is the pdf.js project output, https://github.com/mozilla/pdf.js
Current extension version is: 1.5.337
Current extension version is: 1.5.345

Просмотреть файл

@ -277,7 +277,7 @@ var PdfJs = {
/**
* pdf.js is only enabled if it is both selected as the pdf viewer and if the
* global switch enabling it is true.
* @return {boolean} Wether or not it's enabled.
* @return {boolean} Whether or not it's enabled.
*/
get enabled() {
var disabled = getBoolPref(PREF_DISABLED, true);

Просмотреть файл

@ -334,7 +334,7 @@ ChromeActions.prototype = {
var result = this.localizedStrings[data];
return JSON.stringify(result || null);
} catch (e) {
log('Unable to retrive localized strings: ' + e);
log('Unable to retrieve localized strings: ' + e);
return 'null';
}
},

Просмотреть файл

@ -28,8 +28,8 @@ factory((root.pdfjsDistBuildPdf = {}));
// Use strict in our context only - users might not want it
'use strict';
var pdfjsVersion = '1.5.337';
var pdfjsBuild = '11381cd';
var pdfjsVersion = '1.5.345';
var pdfjsBuild = '10f9f11';
var pdfjsFilePath =
typeof document !== 'undefined' && document.currentScript ?
@ -1080,7 +1080,7 @@ function isSpace(ch) {
*
* @typedef {Object} PromiseCapability
* @property {Promise} promise - A promise object.
* @property {function} resolve - Fullfills the promise.
* @property {function} resolve - Fulfills the promise.
* @property {function} reject - Rejects the promise.
*/
@ -1103,8 +1103,8 @@ function createPromiseCapability() {
/**
* Polyfill for Promises:
* The following promise implementation tries to generally implement the
* Promise/A+ spec. Some notable differences from other promise libaries are:
* - There currently isn't a seperate deferred and promise object.
* Promise/A+ spec. Some notable differences from other promise libraries are:
* - There currently isn't a separate deferred and promise object.
* - Unhandled rejections eventually show an error if they aren't handled.
*
* Based off of the work in:
@ -3615,7 +3615,7 @@ var createMeshCanvas = (function createMeshCanvasClosure() {
// MAX_PATTERN_SIZE is used to avoid OOM situation.
var MAX_PATTERN_SIZE = 3000; // 10in @ 300dpi shall be enough
// We need to keep transparent border around our pattern for fill():
// createPattern with 'no-repeat' will bleed edges accross entire area.
// createPattern with 'no-repeat' will bleed edges across entire area.
var BORDER_SIZE = 2;
var offsetX = Math.floor(bounds[0]);
@ -5688,7 +5688,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
var currentCtx = this.ctx;
// TODO non-isolated groups - according to Rik at adobe non-isolated
// group results aren't usually that different and they even have tools
// that ignore this setting. Notes from Rik on implmenting:
// that ignore this setting. Notes from Rik on implementing:
// - When you encounter an transparency group, create a new canvas with
// the dimensions of the bbox
// - copy the content from the previous canvas to the new canvas

Просмотреть файл

@ -28,8 +28,8 @@ factory((root.pdfjsDistBuildPdfWorker = {}));
// Use strict in our context only - users might not want it
'use strict';
var pdfjsVersion = '1.5.337';
var pdfjsBuild = '11381cd';
var pdfjsVersion = '1.5.345';
var pdfjsBuild = '10f9f11';
var pdfjsFilePath =
typeof document !== 'undefined' && document.currentScript ?
@ -3101,7 +3101,7 @@ function isSpace(ch) {
*
* @typedef {Object} PromiseCapability
* @property {Promise} promise - A promise object.
* @property {function} resolve - Fullfills the promise.
* @property {function} resolve - Fulfills the promise.
* @property {function} reject - Rejects the promise.
*/
@ -3124,8 +3124,8 @@ function createPromiseCapability() {
/**
* Polyfill for Promises:
* The following promise implementation tries to generally implement the
* Promise/A+ spec. Some notable differences from other promise libaries are:
* - There currently isn't a seperate deferred and promise object.
* Promise/A+ spec. Some notable differences from other promise libraries are:
* - There currently isn't a separate deferred and promise object.
* - Unhandled rejections eventually show an error if they aren't handled.
*
* Based off of the work in:
@ -4298,7 +4298,7 @@ var CFFParser = (function CFFParserClosure() {
break;
default:
error('Unknow encoding format: ' + format + ' in CFF');
error('Unknown encoding format: ' + format + ' in CFF');
break;
}
var dataEnd = pos;
@ -4771,7 +4771,7 @@ var CFFCompiler = (function CFFCompilerClosure() {
var globalSubrIndex = this.compileIndex(cff.globalSubrIndex);
output.add(globalSubrIndex);
// Now start on the other entries that have no specfic order.
// Now start on the other entries that have no specific order.
if (cff.encoding && cff.topDict.hasName('Encoding')) {
if (cff.encoding.predefined) {
topDictTracker.setEntryLocation('Encoding', [cff.encoding.format],
@ -12533,13 +12533,13 @@ var JpxImage = (function JpxImageClosure() {
var subband = resolution.subbands[j];
var gainLog2 = SubbandsGainLog2[subband.type];
// calulate quantization coefficient (Section E.1.1.1)
// calculate quantization coefficient (Section E.1.1.1)
var delta = (reversible ? 1 :
Math.pow(2, precision + gainLog2 - epsilon) * (1 + mu / 2048));
var mb = (guardBits + epsilon - 1);
// In the first resolution level, copyCoefficients will fill the
// whole array with coefficients. In the succeding passes,
// whole array with coefficients. In the succeeding passes,
// copyCoefficients will consecutively fill in the values that belong
// to the interleaved positions of the HL, LH, and HH coefficients.
// The LL coefficients will then be interleaved in Transform.iterate().
@ -16378,7 +16378,7 @@ exports.getMetrics = getMetrics;
var Uint32ArrayView = sharedUtil.Uint32ArrayView;
var MurmurHash3_64 = (function MurmurHash3_64Closure (seed) {
// Workaround for missing math precison in JS.
// Workaround for missing math precision in JS.
var MASK_HIGH = 0xffff0000;
var MASK_LOW = 0xffff;
@ -24781,7 +24781,7 @@ var Lexer = (function LexerClosure() {
} else if (ch === 0x2D) { // '-'
// ignore minus signs in the middle of numbers to match
// Adobe's behavior
warn('Badly formated number');
warn('Badly formatted number');
} else if (ch === 0x45 || ch === 0x65) { // 'E', 'e'
// 'E' can be either a scientific notation or the beginning of a new
// operator
@ -25862,9 +25862,11 @@ exports.Type1Parser = Type1Parser;
var Util = sharedUtil.Util;
var assert = sharedUtil.assert;
var warn = sharedUtil.warn;
var error = sharedUtil.error;
var isInt = sharedUtil.isInt;
var isString = sharedUtil.isString;
var MissingDataException = sharedUtil.MissingDataException;
var isName = corePrimitives.isName;
var isCmd = corePrimitives.isCmd;
var isStream = corePrimitives.isStream;
@ -26712,41 +26714,49 @@ var CMapFactory = (function CMapFactoryClosure() {
var previous;
var embededUseCMap;
objLoop: while (true) {
var obj = lexer.getObj();
if (isEOF(obj)) {
break;
} else if (isName(obj)) {
if (obj.name === 'WMode') {
parseWMode(cMap, lexer);
} else if (obj.name === 'CMapName') {
parseCMapName(cMap, lexer);
try {
var obj = lexer.getObj();
if (isEOF(obj)) {
break;
} else if (isName(obj)) {
if (obj.name === 'WMode') {
parseWMode(cMap, lexer);
} else if (obj.name === 'CMapName') {
parseCMapName(cMap, lexer);
}
previous = obj;
} else if (isCmd(obj)) {
switch (obj.cmd) {
case 'endcmap':
break objLoop;
case 'usecmap':
if (isName(previous)) {
embededUseCMap = previous.name;
}
break;
case 'begincodespacerange':
parseCodespaceRange(cMap, lexer);
break;
case 'beginbfchar':
parseBfChar(cMap, lexer);
break;
case 'begincidchar':
parseCidChar(cMap, lexer);
break;
case 'beginbfrange':
parseBfRange(cMap, lexer);
break;
case 'begincidrange':
parseCidRange(cMap, lexer);
break;
}
}
previous = obj;
} else if (isCmd(obj)) {
switch (obj.cmd) {
case 'endcmap':
break objLoop;
case 'usecmap':
if (isName(previous)) {
embededUseCMap = previous.name;
}
break;
case 'begincodespacerange':
parseCodespaceRange(cMap, lexer);
break;
case 'beginbfchar':
parseBfChar(cMap, lexer);
break;
case 'begincidchar':
parseCidChar(cMap, lexer);
break;
case 'beginbfrange':
parseBfRange(cMap, lexer);
break;
case 'begincidrange':
parseCidRange(cMap, lexer);
break;
} catch (ex) {
if (ex instanceof MissingDataException) {
throw ex;
}
warn('Invalid cMap data: ' + ex);
continue;
}
}
@ -26757,9 +26767,8 @@ var CMapFactory = (function CMapFactoryClosure() {
}
if (useCMap) {
return extendCMap(cMap, builtInCMapParams, useCMap);
} else {
return Promise.resolve(cMap);
}
return Promise.resolve(cMap);
}
function extendCMap(cMap, builtInCMapParams, useCMap) {
@ -26821,8 +26830,6 @@ var CMapFactory = (function CMapFactoryClosure() {
parseCMap(cMap, lexer, builtInCMapParams, null).then(
function (parsedCMap) {
resolve(parsedCMap);
}).catch(function (e) {
reject(new Error({ message: 'Invalid CMap data', error: e }));
});
} else {
reject(new Error('Unable to get cMap at: ' + url));
@ -27290,6 +27297,7 @@ var ProblematicCharRanges = new Int32Array([
0x0600, 0x0780,
0x08A0, 0x10A0,
0x1780, 0x1800,
0x1C00, 0x1C50,
// General punctuation chars.
0x2000, 0x2010,
0x2011, 0x2012,
@ -29470,7 +29478,7 @@ var Font = (function FontClosure() {
// Naming tables
builder.addTable('name', createNameTable(fontName));
// PostScript informations
// PostScript information
builder.addTable('post', createPostTable(properties));
return builder.toArray();
@ -29847,7 +29855,7 @@ var Type1Font = (function Type1FontClosure() {
(pfbHeader[3] << 8) | pfbHeader[2];
}
// Get the data block containing glyphs and subrs informations
// Get the data block containing glyphs and subrs information
var headerBlock = getHeaderBlock(file, headerBlockLength);
headerBlockLength = headerBlock.length;
var headerBlockParser = new Type1Parser(headerBlock.stream, false,
@ -30714,7 +30722,7 @@ var PDFFunction = (function PDFFunctionClosure() {
// clip to domain
var v = clip(src[srcOffset], domain[0], domain[1]);
// calulate which bound the value is in
// calculate which bound the value is in
for (var i = 0, ii = bounds.length; i < ii; ++i) {
if (v < bounds[i]) {
break;
@ -33157,7 +33165,7 @@ var PDFImage = (function PDFImageClosure() {
i += 8;
}
// handle remaing bits
// handle remaining bits
if (i < loop2End) {
buf = buffer[bufferPos++];
mask = 128;
@ -33225,7 +33233,7 @@ var PDFImage = (function PDFImageClosure() {
width, height);
}
} else if (isArray(mask)) {
// Color key mask: if any of the compontents are outside the range
// Color key mask: if any of the components are outside the range
// then they should be painted.
alphaBuf = new Uint8Array(width * height);
var numComps = this.numComps;
@ -38058,7 +38066,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
// According to the spec if 'FontDescriptor' is declared, 'FirstChar',
// 'LastChar' and 'Widths' should exist too, but some PDF encoders seem
// to ignore this rule when a variant of a standart font is used.
// to ignore this rule when a variant of a standard font is used.
// TODO Fill the width array depending on which of the base font this is
// a variant.
var firstChar = (dict.get('FirstChar') || 0);

Просмотреть файл

@ -578,7 +578,7 @@ var PDFBug = (function PDFBugClosure() {
} else {
panel.textContent = tool.name + ' is disabled. To enable add ' +
' "' + tool.id + '" to the pdfBug parameter ' +
'and refresh (seperate multiple by commas).';
'and refresh (separate multiple by commas).';
}
buttons.push(panelButton);
}

Просмотреть файл

@ -5549,7 +5549,7 @@ var PDFPageView = (function PDFPageViewClosure() {
}, function(error) {
console.error(error);
// Tell the printEngine that rendering this canvas/page has failed.
// This will make the print proces stop.
// This will make the print process stop.
if ('abort' in obj) {
obj.abort();
} else {
@ -6346,22 +6346,34 @@ var PDFViewer = (function pdfViewer() {
return this._pages[index];
},
/**
* @returns {number}
*/
get currentPageNumber() {
return this._currentPageNumber;
},
/**
* @param {number} val - The page number.
*/
set currentPageNumber(val) {
if (!this.pdfDocument) {
this._currentPageNumber = val;
return;
}
this._setCurrentPageNumber(val);
// The intent can be to just reset a scroll position and/or scale.
this._resetCurrentPageView();
this._setCurrentPageNumber(val, /* resetCurrentPageView = */ true);
},
_setCurrentPageNumber: function pdfViewer_setCurrentPageNumber(val) {
/**
* @private
*/
_setCurrentPageNumber:
function pdfViewer_setCurrentPageNumber(val, resetCurrentPageView) {
if (this._currentPageNumber === val) {
if (resetCurrentPageView) {
this._resetCurrentPageView();
}
return;
}
var arg;
@ -6384,6 +6396,10 @@ var PDFViewer = (function pdfViewer() {
this._currentPageNumber = val;
this.eventBus.dispatch('pagechanging', arg);
this.eventBus.dispatch('pagechange', arg);
if (resetCurrentPageView) {
this._resetCurrentPageView();
}
},
/**
@ -6691,6 +6707,7 @@ var PDFViewer = (function pdfViewer() {
/**
* Refreshes page view: scrolls to the current page and updates the scale.
* @private
*/
_resetCurrentPageView: function () {
if (this.isInPresentationMode) {
@ -6715,8 +6732,7 @@ var PDFViewer = (function pdfViewer() {
}
if (this.isInPresentationMode || !dest) {
this._setCurrentPageNumber(pageNumber);
this._resetCurrentPageView();
this._setCurrentPageNumber(pageNumber, /* resetCurrentPageView */ true);
return;
}

Просмотреть файл

@ -98,8 +98,7 @@ Tools.inspector = {
inMenu: true,
commands: [
"devtools/client/responsivedesign/resize-commands",
"devtools/client/inspector/inspector-commands",
"devtools/client/eyedropper/commands.js"
"devtools/client/inspector/inspector-commands"
],
preventClosingOnKey: true,

Просмотреть файл

@ -1,57 +0,0 @@
/* 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 l10n = require("gcli/l10n");
const EventEmitter = require("devtools/shared/event-emitter");
const eventEmitter = new EventEmitter();
var { Eyedropper, EyedropperManager } = require("devtools/client/eyedropper/eyedropper");
/**
* 'eyedropper' command
*/
exports.items = [{
item: "command",
runAt: "client",
name: "eyedropper",
description: l10n.lookup("eyedropperDesc"),
manual: l10n.lookup("eyedropperManual"),
buttonId: "command-button-eyedropper",
buttonClass: "command-button command-button-invertable",
tooltipText: l10n.lookup("eyedropperTooltip"),
state: {
isChecked: function (target) {
if (!target.tab) {
return false;
}
let chromeWindow = target.tab.ownerDocument.defaultView;
let dropper = EyedropperManager.getInstance(chromeWindow);
if (dropper) {
return true;
}
return false;
},
onChange: function (target, changeHandler) {
eventEmitter.on("changed", changeHandler);
},
offChange: function (target, changeHandler) {
eventEmitter.off("changed", changeHandler);
},
},
exec: function (args, context) {
let chromeWindow = context.environment.chromeWindow;
let target = context.environment.target;
let dropper = EyedropperManager.createInstance(chromeWindow,
{ context: "command",
copyOnSelect: true });
dropper.open();
eventEmitter.emit("changed", { target: target });
dropper.once("destroy", () => {
eventEmitter.emit("changed", { target: target });
});
}
}];

Просмотреть файл

@ -1,3 +0,0 @@
* {
cursor: crosshair !important;
}

Просмотреть файл

@ -1,24 +0,0 @@
/* 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/. */
var { interfaces: Ci } = Components;
addMessageListener("Eyedropper:RequestContentScreenshot", sendContentScreenshot);
function sendContentScreenshot() {
let canvas = content.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
let scale = content.getInterface(Ci.nsIDOMWindowUtils).fullZoom;
let width = content.innerWidth;
let height = content.innerHeight;
canvas.width = width * scale;
canvas.height = height * scale;
canvas.mozOpaque = true;
let ctx = canvas.getContext("2d");
ctx.scale(scale, scale);
ctx.drawWindow(content, content.scrollX, content.scrollY, width, height, "#fff");
sendAsyncMessage("Eyedropper:Screenshot", canvas.toDataURL());
}

Просмотреть файл

@ -1,839 +0,0 @@
/* 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 {Cc, Ci} = require("chrome");
const {rgbToHsl, rgbToColorName} =
require("devtools/client/shared/css-color").colorUtils;
const Telemetry = require("devtools/client/shared/telemetry");
const EventEmitter = require("devtools/shared/event-emitter");
const promise = require("promise");
const defer = require("devtools/shared/defer");
const Services = require("Services");
loader.lazyGetter(this, "clipboardHelper", function () {
return Cc["@mozilla.org/widget/clipboardhelper;1"]
.getService(Ci.nsIClipboardHelper);
});
loader.lazyGetter(this, "ssService", function () {
return Cc["@mozilla.org/content/style-sheet-service;1"]
.getService(Ci.nsIStyleSheetService);
});
loader.lazyGetter(this, "ioService", function () {
return Cc["@mozilla.org/network/io-service;1"]
.getService(Ci.nsIIOService);
});
loader.lazyGetter(this, "DOMUtils", function () {
return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
});
loader.lazyGetter(this, "l10n", () => Services.strings
.createBundle("chrome://devtools/locale/eyedropper.properties"));
const EYEDROPPER_URL = "chrome://devtools/content/eyedropper/eyedropper.xul";
const CROSSHAIRS_URL = "chrome://devtools/content/eyedropper/crosshairs.css";
const NOCURSOR_URL = "chrome://devtools/content/eyedropper/nocursor.css";
const ZOOM_PREF = "devtools.eyedropper.zoom";
const FORMAT_PREF = "devtools.defaultColorUnit";
const CANVAS_WIDTH = 96;
const CANVAS_OFFSET = 3; // equals the border width of the canvas.
const CLOSE_DELAY = 750;
const HEX_BOX_WIDTH = CANVAS_WIDTH + CANVAS_OFFSET * 2;
const HSL_BOX_WIDTH = 158;
/**
* Manage instances of eyedroppers for windows. Registering here isn't
* necessary for creating an eyedropper, but can be used for testing.
*/
var EyedropperManager = {
_instances: new WeakMap(),
getInstance: function (chromeWindow) {
return this._instances.get(chromeWindow);
},
createInstance: function (chromeWindow, options) {
let dropper = this.getInstance(chromeWindow);
if (dropper) {
return dropper;
}
dropper = new Eyedropper(chromeWindow, options);
this._instances.set(chromeWindow, dropper);
dropper.on("destroy", () => {
this.deleteInstance(chromeWindow);
});
return dropper;
},
deleteInstance: function (chromeWindow) {
this._instances.delete(chromeWindow);
}
};
exports.EyedropperManager = EyedropperManager;
/**
* Eyedropper widget. Once opened, shows zoomed area above current pixel and
* displays the color value of the center pixel. Clicking on the window will
* close the widget and fire a 'select' event. If 'copyOnSelect' is true, the color
* will also be copied to the clipboard.
*
* let eyedropper = new Eyedropper(window);
* eyedropper.open();
*
* eyedropper.once("select", (ev, color) => {
* console.log(color); // "rgb(20, 50, 230)"
* })
*
* @param {DOMWindow} chromeWindow
* window to inspect
* @param {object} opts
* optional options object, with 'copyOnSelect', 'context'
*/
function Eyedropper(chromeWindow, opts = { copyOnSelect: true, context: "other" }) {
this.copyOnSelect = opts.copyOnSelect;
this._onFirstMouseMove = this._onFirstMouseMove.bind(this);
this._onMouseMove = this._onMouseMove.bind(this);
this._onMouseDown = this._onMouseDown.bind(this);
this._onKeyDown = this._onKeyDown.bind(this);
this._onFrameLoaded = this._onFrameLoaded.bind(this);
this._chromeWindow = chromeWindow;
this._chromeDocument = chromeWindow.document;
this._OS = Services.appinfo.OS;
this._dragging = true;
this.loaded = false;
this._mouseMoveCounter = 0;
this.format = Services.prefs.getCharPref(FORMAT_PREF); // color value format
this.zoom = Services.prefs.getIntPref(ZOOM_PREF); // zoom level - integer
this._zoomArea = {
x: 0, // the left coordinate of the center of the inspected region
y: 0, // the top coordinate of the center of the inspected region
width: CANVAS_WIDTH, // width of canvas to draw zoomed area onto
height: CANVAS_WIDTH // height of canvas
};
if (this._contentTab) {
let mm = this._contentTab.linkedBrowser.messageManager;
mm.loadFrameScript("resource://devtools/client/eyedropper/eyedropper-child.js", true);
}
// record if this was opened via the picker or standalone
var telemetry = new Telemetry();
if (opts.context == "command") {
telemetry.toolOpened("eyedropper");
}
else if (opts.context == "menu") {
telemetry.toolOpened("menueyedropper");
}
else if (opts.context == "picker") {
telemetry.toolOpened("pickereyedropper");
}
EventEmitter.decorate(this);
}
exports.Eyedropper = Eyedropper;
Eyedropper.prototype = {
/**
* Get the number of cells (blown-up pixels) per direction in the grid.
*/
get cellsWide() {
// Canvas will render whole "pixels" (cells) only, and an even
// number at that. Round up to the nearest even number of pixels.
let cellsWide = Math.ceil(this._zoomArea.width / this.zoom);
cellsWide += cellsWide % 2;
return cellsWide;
},
/**
* Get the size of each cell (blown-up pixel) in the grid.
*/
get cellSize() {
return this._zoomArea.width / this.cellsWide;
},
/**
* Get index of cell in the center of the grid.
*/
get centerCell() {
return Math.floor(this.cellsWide / 2);
},
/**
* Get color of center cell in the grid.
*/
get centerColor() {
let x, y;
x = y = (this.centerCell * this.cellSize) + (this.cellSize / 2);
let rgb = this._ctx.getImageData(x, y, 1, 1).data;
return rgb;
},
get _contentTab() {
return this._chromeWindow.gBrowser && this._chromeWindow.gBrowser.selectedTab;
},
/**
* Fetch a screenshot of the content.
*
* @return {promise}
* Promise that resolves with the screenshot as a dataURL
*/
getContentScreenshot: function () {
if (!this._contentTab) {
return promise.resolve(null);
}
let deferred = defer();
let mm = this._contentTab.linkedBrowser.messageManager;
function onScreenshot(message) {
mm.removeMessageListener("Eyedropper:Screenshot", onScreenshot);
deferred.resolve(message.data);
}
mm.addMessageListener("Eyedropper:Screenshot", onScreenshot);
mm.sendAsyncMessage("Eyedropper:RequestContentScreenshot");
return deferred.promise;
},
/**
* Start the eyedropper. Add listeners for a mouse move in the window to
* show the eyedropper.
*/
open: function () {
if (this.isOpen) {
// the eyedropper is aready open, don't create another panel.
return promise.resolve();
}
this.isOpen = true;
this._showCrosshairs();
// Get screenshot of content so we can inspect colors
return this.getContentScreenshot().then((dataURL) => {
// The data url may be null, e.g. if there is no content tab
if (dataURL) {
this._contentImage = new this._chromeWindow.Image();
this._contentImage.src = dataURL;
// Wait for screenshot to load
let imageLoaded = promise.defer();
this._contentImage.onload = imageLoaded.resolve
return imageLoaded.promise;
}
}).then(() => {
// Then start showing the eyedropper UI
this._chromeDocument.addEventListener("mousemove", this._onFirstMouseMove);
this.isStarted = true;
this.emit("started");
});
},
/**
* Called on the first mouse move over the window. Opens the eyedropper
* panel where the mouse is.
*/
_onFirstMouseMove: function (event) {
this._chromeDocument.removeEventListener("mousemove", this._onFirstMouseMove);
this._panel = this._buildPanel();
let popupSet = this._chromeDocument.querySelector("#mainPopupSet");
popupSet.appendChild(this._panel);
let { panelX, panelY } = this._getPanelCoordinates(event);
this._panel.openPopupAtScreen(panelX, panelY);
this._setCoordinates(event);
this._addListeners();
// hide cursor as we'll be showing the panel over the mouse instead.
this._hideCrosshairs();
this._hideCursor();
},
/**
* Whether the coordinates are over the content or chrome.
*
* @param {number} clientX
* x-coordinate of mouse relative to browser window.
* @param {number} clientY
* y-coordinate of mouse relative to browser window.
*/
_isInContent: function (clientX, clientY) {
let box = this._contentTab && this._contentTab.linkedBrowser.getBoundingClientRect();
if (box &&
clientX > box.left &&
clientX < box.right &&
clientY > box.top &&
clientY < box.bottom) {
return true;
}
return false;
},
/**
* Set the current coordinates to inspect from where a mousemove originated.
*
* @param {MouseEvent} event
* Event for the mouse move.
*/
_setCoordinates: function (event) {
let inContent = this._isInContent(event.clientX, event.clientY);
let win = this._chromeWindow;
// offset of mouse from browser window
let x = event.clientX;
let y = event.clientY;
if (inContent) {
// calculate the offset of the mouse from the content window
let box = this._contentTab.linkedBrowser.getBoundingClientRect();
x = x - box.left;
y = y - box.top;
this._zoomArea.contentWidth = box.width;
this._zoomArea.contentHeight = box.height;
}
this._zoomArea.inContent = inContent;
// don't let it inspect outside the browser window
x = Math.max(0, Math.min(x, win.outerWidth - 1));
y = Math.max(0, Math.min(y, win.outerHeight - 1));
this._zoomArea.x = x;
this._zoomArea.y = y;
},
/**
* Build and add a new eyedropper panel to the window.
*
* @return {Panel}
* The XUL panel holding the eyedropper UI.
*/
_buildPanel: function () {
let panel = this._chromeDocument.createElement("panel");
panel.setAttribute("noautofocus", true);
panel.setAttribute("noautohide", true);
panel.setAttribute("level", "floating");
panel.setAttribute("class", "devtools-eyedropper-panel");
let iframe = this._iframe = this._chromeDocument.createElement("iframe");
iframe.addEventListener("load", this._onFrameLoaded, true);
iframe.setAttribute("flex", "1");
iframe.setAttribute("transparent", "transparent");
iframe.setAttribute("allowTransparency", true);
iframe.setAttribute("class", "devtools-eyedropper-iframe");
iframe.setAttribute("src", EYEDROPPER_URL);
iframe.setAttribute("width", CANVAS_WIDTH);
iframe.setAttribute("height", CANVAS_WIDTH);
panel.appendChild(iframe);
return panel;
},
/**
* Event handler for the panel's iframe's load event. Emits
* a "load" event from this eyedropper object.
*/
_onFrameLoaded: function () {
this._iframe.removeEventListener("load", this._onFrameLoaded, true);
this._iframeDocument = this._iframe.contentDocument;
this._colorPreview = this._iframeDocument.querySelector("#color-preview");
this._colorValue = this._iframeDocument.querySelector("#color-value");
// value box will be too long for hex values and too short for hsl
let valueBox = this._iframeDocument.querySelector("#color-value-box");
if (this.format == "hex") {
valueBox.style.width = HEX_BOX_WIDTH + "px";
}
else if (this.format == "hsl") {
valueBox.style.width = HSL_BOX_WIDTH + "px";
}
this._canvas = this._iframeDocument.querySelector("#canvas");
this._ctx = this._canvas.getContext("2d");
// so we preserve the clear pixel boundaries
this._ctx.mozImageSmoothingEnabled = false;
this._drawWindow();
this._addPanelListeners();
this._iframe.focus();
this.loaded = true;
this.emit("load");
},
/**
* Add key listeners to the panel.
*/
_addPanelListeners: function () {
this._iframeDocument.addEventListener("keydown", this._onKeyDown);
let closeCmd = this._iframeDocument.getElementById("eyedropper-cmd-close");
closeCmd.addEventListener("command", this.destroy.bind(this), true);
let copyCmd = this._iframeDocument.getElementById("eyedropper-cmd-copy");
copyCmd.addEventListener("command", this.selectColor.bind(this), true);
},
/**
* Remove listeners from the panel.
*/
_removePanelListeners: function () {
this._iframeDocument.removeEventListener("keydown", this._onKeyDown);
},
/**
* Add mouse event listeners to the document we're inspecting.
*/
_addListeners: function () {
this._chromeDocument.addEventListener("mousemove", this._onMouseMove);
this._chromeDocument.addEventListener("mousedown", this._onMouseDown);
},
/**
* Remove mouse event listeners from the document we're inspecting.
*/
_removeListeners: function () {
this._chromeDocument.removeEventListener("mousemove", this._onFirstMouseMove);
this._chromeDocument.removeEventListener("mousemove", this._onMouseMove);
this._chromeDocument.removeEventListener("mousedown", this._onMouseDown);
},
/**
* Hide the cursor.
*/
_hideCursor: function () {
registerStyleSheet(NOCURSOR_URL);
},
/**
* Reset the cursor back to default.
*/
_resetCursor: function () {
unregisterStyleSheet(NOCURSOR_URL);
},
/**
* Show a crosshairs as the mouse cursor
*/
_showCrosshairs: function () {
registerStyleSheet(CROSSHAIRS_URL);
},
/**
* Reset cursor.
*/
_hideCrosshairs: function () {
unregisterStyleSheet(CROSSHAIRS_URL);
},
/**
* Event handler for a mouse move over the page we're inspecting.
* Preview the area under the cursor, and move panel to be under the cursor.
*
* @param {DOMEvent} event
* MouseEvent for the mouse moving
*/
_onMouseMove: function (event) {
if (!this._dragging || !this._panel || !this._canvas) {
return;
}
if (this._OS == "Linux" && ++this._mouseMoveCounter % 2 == 0) {
// skip every other mousemove to preserve performance.
return;
}
this._setCoordinates(event);
this._drawWindow();
let { panelX, panelY } = this._getPanelCoordinates(event);
this._movePanel(panelX, panelY);
},
/**
* Get coordinates of where the eyedropper panel should go based on
* the current coordinates of the mouse cursor.
*
* @param {MouseEvent} event
* object with properties 'screenX' and 'screenY'
*
* @return {object}
* object with properties 'panelX', 'panelY'
*/
_getPanelCoordinates: function ({screenX, screenY}) {
let win = this._chromeWindow;
let offset = CANVAS_WIDTH / 2 + CANVAS_OFFSET;
let panelX = screenX - offset;
let windowX = win.screenX + (win.outerWidth - win.innerWidth);
let maxX = win.screenX + win.outerWidth - offset - 1;
let panelY = screenY - offset;
let windowY = win.screenY + (win.outerHeight - win.innerHeight);
let maxY = win.screenY + win.outerHeight - offset - 1;
// don't let the panel move outside the browser window
panelX = Math.max(windowX - offset, Math.min(panelX, maxX));
panelY = Math.max(windowY - offset, Math.min(panelY, maxY));
return { panelX: panelX, panelY: panelY };
},
/**
* Move the eyedropper panel to the given coordinates.
*
* @param {number} screenX
* left coordinate on the screen
* @param {number} screenY
* top coordinate
*/
_movePanel: function (screenX, screenY) {
this._panelX = screenX;
this._panelY = screenY;
this._panel.moveTo(screenX, screenY);
},
/**
* Handler for the mouse down event on the inspected page. This means a
* click, so we'll select the color that's currently hovered.
*
* @param {Event} event
* DOM MouseEvent object
*/
_onMouseDown: function (event) {
event.preventDefault();
event.stopPropagation();
this.selectColor();
},
/**
* Select the current color that's being previewed. Fire a
* "select" event with the color as an rgb string.
*/
selectColor: function () {
if (this._isSelecting) {
return;
}
this._isSelecting = true;
this._dragging = false;
this.emit("select", this._colorValue.value);
if (this.copyOnSelect) {
this.copyColor(this.destroy.bind(this));
}
else {
this.destroy();
}
},
/**
* Copy the currently inspected color to the clipboard.
*
* @param {Function} callback
* Callback to be called when the color is in the clipboard.
*/
copyColor: function (callback) {
clearTimeout(this._copyTimeout);
let color = this._colorValue.value;
clipboardHelper.copyString(color);
this._colorValue.classList.add("highlight");
this._colorValue.value = "✓ " + l10n.GetStringFromName("colorValue.copied");
this._copyTimeout = setTimeout(() => {
this._colorValue.classList.remove("highlight");
this._colorValue.value = color;
if (callback) {
callback();
}
}, CLOSE_DELAY);
},
/**
* Handler for the keydown event on the panel. Either copy the color
* or move the panel in a direction depending on the key pressed.
*
* @param {Event} event
* DOM KeyboardEvent object
*/
_onKeyDown: function (event) {
if (event.metaKey && event.keyCode === event.DOM_VK_C) {
this.copyColor();
return;
}
let offsetX = 0;
let offsetY = 0;
let modifier = 1;
if (event.keyCode === event.DOM_VK_LEFT) {
offsetX = -1;
}
if (event.keyCode === event.DOM_VK_RIGHT) {
offsetX = 1;
}
if (event.keyCode === event.DOM_VK_UP) {
offsetY = -1;
}
if (event.keyCode === event.DOM_VK_DOWN) {
offsetY = 1;
}
if (event.shiftKey) {
modifier = 10;
}
offsetY *= modifier;
offsetX *= modifier;
if (offsetX !== 0 || offsetY !== 0) {
this._zoomArea.x += offsetX;
this._zoomArea.y += offsetY;
this._drawWindow();
this._movePanel(this._panelX + offsetX, this._panelY + offsetY);
event.preventDefault();
}
},
/**
* Draw the inspected area onto the canvas using the zoom level.
*/
_drawWindow: function () {
let { width, height, x, y, inContent,
contentWidth, contentHeight } = this._zoomArea;
let zoomedWidth = width / this.zoom;
let zoomedHeight = height / this.zoom;
let leftX = x - (zoomedWidth / 2);
let topY = y - (zoomedHeight / 2);
// draw the portion of the window we're inspecting
if (inContent) {
// draw from content source image "s" to destination rect "d"
let sx = leftX;
let sy = topY;
let sw = zoomedWidth;
let sh = zoomedHeight;
let dx = 0;
let dy = 0;
// we're at the content edge, so we have to crop the drawing
if (leftX < 0) {
sx = 0;
sw = zoomedWidth + leftX;
dx = -leftX;
}
else if (leftX + zoomedWidth > contentWidth) {
sw = contentWidth - leftX;
}
if (topY < 0) {
sy = 0;
sh = zoomedHeight + topY;
dy = -topY;
}
else if (topY + zoomedHeight > contentHeight) {
sh = contentHeight - topY;
}
let dw = sw;
let dh = sh;
// we don't want artifacts when we're inspecting the edges of content
if (leftX < 0 || topY < 0 ||
leftX + zoomedWidth > contentWidth ||
topY + zoomedHeight > contentHeight) {
this._ctx.fillStyle = "white";
this._ctx.fillRect(0, 0, width, height);
}
// draw from the screenshot to the eyedropper canvas
this._ctx.drawImage(this._contentImage, sx, sy, sw,
sh, dx, dy, dw, dh);
}
else {
// the mouse is over the chrome, so draw that instead of the content
this._ctx.drawWindow(this._chromeWindow, leftX, topY, zoomedWidth,
zoomedHeight, "white");
}
// now scale it
this._ctx.drawImage(this._canvas, 0, 0, zoomedWidth, zoomedHeight,
0, 0, width, height);
let rgb = this.centerColor;
this._colorPreview.style.backgroundColor = toColorString(rgb, "rgb");
this._colorValue.value = toColorString(rgb, this.format);
if (this.zoom > 2) {
// grid at 2x is too busy
this._drawGrid();
}
this._drawCrosshair();
},
/**
* Draw a grid on the canvas representing pixel boundaries.
*/
_drawGrid: function () {
let { width, height } = this._zoomArea;
this._ctx.lineWidth = 1;
this._ctx.strokeStyle = "rgba(143, 143, 143, 0.2)";
for (let i = 0; i < width; i += this.cellSize) {
this._ctx.beginPath();
this._ctx.moveTo(i - .5, 0);
this._ctx.lineTo(i - .5, height);
this._ctx.stroke();
this._ctx.beginPath();
this._ctx.moveTo(0, i - .5);
this._ctx.lineTo(width, i - .5);
this._ctx.stroke();
}
},
/**
* Draw a box on the canvas to highlight the center cell.
*/
_drawCrosshair: function () {
let x, y;
x = y = this.centerCell * this.cellSize;
this._ctx.lineWidth = 1;
this._ctx.lineJoin = "miter";
this._ctx.strokeStyle = "rgba(0, 0, 0, 1)";
this._ctx.strokeRect(x - 1.5, y - 1.5, this.cellSize + 2, this.cellSize + 2);
this._ctx.strokeStyle = "rgba(255, 255, 255, 1)";
this._ctx.strokeRect(x - 0.5, y - 0.5, this.cellSize, this.cellSize);
},
/**
* Destroy the eyedropper and clean up. Emits a "destroy" event.
*/
destroy: function () {
this._resetCursor();
this._hideCrosshairs();
if (this._panel) {
this._panel.hidePopup();
this._panel.remove();
this._panel = null;
}
this._removePanelListeners();
this._removeListeners();
this.isStarted = false;
this.isOpen = false;
this._isSelecting = false;
this.emit("destroy");
}
};
/**
* Add a user style sheet that applies to all documents.
*/
function registerStyleSheet(url) {
var uri = ioService.newURI(url, null, null);
if (!ssService.sheetRegistered(uri, ssService.AGENT_SHEET)) {
ssService.loadAndRegisterSheet(uri, ssService.AGENT_SHEET);
}
}
/**
* Remove a user style sheet.
*/
function unregisterStyleSheet(url) {
var uri = ioService.newURI(url, null, null);
if (ssService.sheetRegistered(uri, ssService.AGENT_SHEET)) {
ssService.unregisterSheet(uri, ssService.AGENT_SHEET);
}
}
/**
* Get a formatted CSS color string from a color value.
*
* @param {array} rgb
* Rgb values of a color to format
* @param {string} format
* Format of string. One of "hex", "rgb", "hsl", "name"
*
* @return {string}
* Formatted color value, e.g. "#FFF" or "hsl(20, 10%, 10%)"
*/
function toColorString(rgb, format) {
let [r, g, b] = rgb;
switch (format) {
case "hex":
return hexString(rgb);
case "rgb":
return "rgb(" + r + ", " + g + ", " + b + ")";
case "hsl":
let [h, s, l] = rgbToHsl(rgb);
return "hsl(" + h + ", " + s + "%, " + l + "%)";
case "name":
let str;
try {
str = rgbToColorName(r, g, b);
} catch (e) {
str = hexString(rgb);
}
return str;
default:
return hexString(rgb);
}
}
/**
* Produce a hex-formatted color string from rgb values.
*
* @param {array} rgb
* Rgb values of color to stringify
*
* @return {string}
* Hex formatted string for color, e.g. "#FFEE00"
*/
function hexString([r, g, b]) {
let val = (1 << 24) + (r << 16) + (g << 8) + (b << 0);
return "#" + val.toString(16).substr(-6).toUpperCase();
}

Просмотреть файл

@ -1,44 +0,0 @@
<?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/. -->
<!DOCTYPE window []>
<?xml-stylesheet href="chrome://devtools/skin/common.css" type="text/css"?>
<?xml-stylesheet href="chrome://devtools/skin/eyedropper.css" type="text/css"?>
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
no-theme="true">
<script type="application/javascript;version=1.8"
src="chrome://devtools/content/shared/theme-switching.js"/>
<commandset id="eyedropper-commandset">
<command id="eyedropper-cmd-close"
oncommand="void(0);"/>
<command id="eyedropper-cmd-copy"
oncommand="void(0);"/>
</commandset>
<keyset id="eyedropper-keyset">
<key id="eyedropper-key-escape"
keycode="VK_ESCAPE"
command="eyedropper-cmd-close"/>
<key id="eyedropper-key-enter"
keycode="VK_RETURN"
command="eyedropper-cmd-copy"/>
</keyset>
<box id="canvas-overflow">
<canvas id="canvas" xmlns="http://www.w3.org/1999/xhtml" width="96" height="96">
</canvas>
</box>
<hbox id="color-value-container">
<hbox id="color-value-box">
<box id="color-preview">
</box>
<label id="color-value" class="devtools-monospace">
</label>
</hbox>
</hbox>
</window>

Просмотреть файл

@ -1,13 +0,0 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
DevToolsModules(
'commands.js',
'eyedropper-child.js',
'eyedropper.js'
)
BROWSER_CHROME_MANIFESTS += ['test/browser.ini']

Просмотреть файл

@ -1,3 +0,0 @@
* {
cursor: none !important;
}

Просмотреть файл

@ -1,4 +0,0 @@
{
// Extend from the shared list of defined globals for mochitests.
"extends": "../../../.eslintrc.mochitests"
}

Просмотреть файл

@ -1,13 +0,0 @@
[DEFAULT]
tags = devtools
subsuite = clipboard
support-files =
color-block.html
head.js
!/devtools/client/commandline/test/helpers.js
!/devtools/client/framework/test/shared-head.js
[browser_eyedropper_basic.js]
skip-if = os == "win" && debug # bug 963492
[browser_eyedropper_cmd.js]
skip-if = true # bug 1278400

Просмотреть файл

@ -1,80 +0,0 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const TESTCASE_URI = CHROME_URL_ROOT + "color-block.html";
const DIV_COLOR = "#0000FF";
/**
* Test basic eyedropper widget functionality:
* - Opening eyedropper and pressing ESC closes the eyedropper
* - Opening eyedropper and clicking copies the center color
*/
add_task(function* () {
yield addTab(TESTCASE_URI);
info("added tab");
yield testEscape();
info("testing selecting a color");
yield testSelect();
});
function* testEscape() {
let dropper = new Eyedropper(window);
yield inspectPage(dropper, false);
let destroyed = dropper.once("destroy");
pressESC();
yield destroyed;
ok(true, "escape closed the eyedropper");
}
function* testSelect() {
let dropper = new Eyedropper(window);
let selected = dropper.once("select");
let copied = waitForClipboard(() => {}, DIV_COLOR);
yield inspectPage(dropper);
let color = yield selected;
is(color, DIV_COLOR, "correct color selected");
// wait for DIV_COLOR to be copied to the clipboard
yield copied;
}
/* Helpers */
function* inspectPage(dropper, click = true) {
yield dropper.open();
info("dropper opened");
let target = document.documentElement;
let win = window;
// get location of the <div> in the content, offset from browser window
let box = gBrowser.selectedBrowser.getBoundingClientRect();
let x = box.left + 100;
let y = box.top + 100;
EventUtils.synthesizeMouse(target, x, y, { type: "mousemove" }, win);
yield dropperLoaded(dropper);
EventUtils.synthesizeMouse(target, x + 10, y + 10, { type: "mousemove" }, win);
if (click) {
EventUtils.synthesizeMouse(target, x + 10, y + 10, {}, win);
}
}
function pressESC() {
EventUtils.synthesizeKey("VK_ESCAPE", { });
}

Просмотреть файл

@ -1,61 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests that the eyedropper command works
const TESTCASE_URI = CHROME_URL_ROOT + "color-block.html";
const DIV_COLOR = "#0000FF";
function test() {
return Task.spawn(spawnTest).then(finish, helpers.handleError);
}
function* spawnTest() {
let options = yield helpers.openTab(TESTCASE_URI);
yield helpers.openToolbar(options);
yield helpers.audit(options, [
{
setup: "eyedropper",
check: {
input: "eyedropper"
},
exec: { output: "" }
},
]);
yield inspectAndWaitForCopy();
yield helpers.closeToolbar(options);
yield helpers.closeTab(options);
}
function inspectAndWaitForCopy() {
let copied = waitForClipboard(() => {}, DIV_COLOR);
let ready = inspectPage(); // resolves once eyedropper is destroyed
return Promise.all([copied, ready]);
}
function inspectPage() {
let target = document.documentElement;
let win = window;
// get location of the <div> in the content, offset from browser window
let box = gBrowser.selectedBrowser.getBoundingClientRect();
let x = box.left + 100;
let y = box.top + 100;
let dropper = EyedropperManager.getInstance(window);
return dropperStarted(dropper).then(() => {
EventUtils.synthesizeMouse(target, x, y, { type: "mousemove" }, win);
return dropperLoaded(dropper).then(() => {
EventUtils.synthesizeMouse(target, x + 10, y + 10, { type: "mousemove" }, win);
EventUtils.synthesizeMouse(target, x + 10, y + 10, {}, win);
return dropper.once("destroy");
});
});
}

Просмотреть файл

@ -1,22 +0,0 @@
<!doctype html>
<html>
<head>
<title>basic eyedropper test case</title>
<style type="text/css">
body {
background: #f99;
}
#test {
margin: 100px;
background-color: blue;
width: 20px;
height: 20px;
}
</style>
</head>
<body>
<div id="test">
</div>
</body>
</html>

Просмотреть файл

@ -1,28 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// shared-head.js handles imports, constants, and utility functions
Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js", this);
Services.scriptloader.loadSubScript(TEST_DIR + "../../../commandline/test/helpers.js", this);
const { Eyedropper, EyedropperManager } = require("devtools/client/eyedropper/eyedropper");
function waitForClipboard(setup, expected) {
let deferred = defer();
SimpleTest.waitForClipboard(expected, setup, deferred.resolve, deferred.reject);
return deferred.promise;
}
function dropperStarted(dropper) {
if (dropper.isStarted) {
return promise.resolve();
}
return dropper.once("started");
}
function dropperLoaded(dropper) {
if (dropper.loaded) {
return promise.resolve();
}
return dropper.once("load");
}

Просмотреть файл

@ -93,7 +93,6 @@ const ToolboxButtons = exports.ToolboxButtons = [
{ id: "command-button-responsive" },
{ id: "command-button-paintflashing" },
{ id: "command-button-scratchpad" },
{ id: "command-button-eyedropper" },
{ id: "command-button-screenshot" },
{ id: "command-button-rulers" },
{ id: "command-button-measure" },

Просмотреть файл

@ -5,8 +5,9 @@
"use strict";
const l10n = require("gcli/l10n");
loader.lazyRequireGetter(this, "gDevTools",
"devtools/client/framework/devtools", true);
loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
const {EyeDropper, HighlighterEnvironment} = require("devtools/server/actors/highlighters");
const Telemetry = require("devtools/client/shared/telemetry");
exports.items = [{
item: "command",
@ -28,4 +29,43 @@ exports.items = [{
toolbox.getCurrentPanel().selection.setNode(args.selector, "gcli");
});
}
}, {
item: "command",
runAt: "client",
name: "eyedropper",
description: l10n.lookup("eyedropperDesc"),
manual: l10n.lookup("eyedropperManual"),
params: [{
// This hidden parameter is only set to true when the eyedropper browser menu item is
// used. It is useful to log a different telemetry event whether the tool was used
// from the menu, or from the gcli command line.
group: "hiddengroup",
params: [{
name: "frommenu",
type: "boolean",
hidden: true
}]
}],
exec: function (args, context) {
let telemetry = new Telemetry();
telemetry.toolOpened(args.frommenu ? "menueyedropper" : "eyedropper");
context.updateExec("eyedropper_server").catch(e => console.error(e));
}
}, {
item: "command",
runAt: "server",
name: "eyedropper_server",
hidden: true,
exec: function (args, {environment}) {
let env = new HighlighterEnvironment();
env.initFromWindow(environment.window);
let eyeDropper = new EyeDropper(env);
eyeDropper.show(environment.document.documentElement, {copyOnSelect: true});
eyeDropper.once("hidden", () => {
eyeDropper.destroy();
env.destroy();
});
}
}];

Просмотреть файл

@ -20,6 +20,7 @@ var {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
var {Task} = require("devtools/shared/task");
const {initCssProperties} = require("devtools/shared/fronts/css-properties");
const nodeConstants = require("devtools/shared/dom-node-constants");
const Telemetry = require("devtools/client/shared/telemetry");
const Menu = require("devtools/client/framework/menu");
const MenuItem = require("devtools/client/framework/menu-item");
@ -89,6 +90,8 @@ function InspectorPanel(iframeWindow, toolbox) {
this.panelWin = iframeWindow;
this.panelWin.inspector = this;
this.telemetry = new Telemetry();
this.nodeMenuTriggerInfo = null;
this._handleRejectionIfNotDestroyed = this._handleRejectionIfNotDestroyed.bind(this);
@ -102,13 +105,6 @@ function InspectorPanel(iframeWindow, toolbox) {
this.onPaneToggleButtonClicked = this.onPaneToggleButtonClicked.bind(this);
this._onMarkupFrameLoad = this._onMarkupFrameLoad.bind(this);
let doc = this.panelDoc;
// Handle 'Add Node' toolbar button.
this.addNode = this.addNode.bind(this);
this.addNodeButton = doc.getElementById("inspector-element-add-button");
this.addNodeButton.addEventListener("click", this.addNode);
this._target.on("will-navigate", this._onBeforeNavigate);
this._detectingActorFeatures = this._detectActorFeatures();
@ -255,6 +251,7 @@ InspectorPanel.prototype = {
this.setupSearchBox();
this.setupSidebar();
this.setupToolbar();
return deferred.promise;
},
@ -462,7 +459,6 @@ InspectorPanel.prototype = {
this.sidebar.toggleTab(true, "fontinspector");
}
this.setupSidebarToggle();
this.setupSidebarSize();
this.sidebar.show(defaultTab);
@ -513,10 +509,8 @@ InspectorPanel.prototype = {
});
},
/**
* Add the expand/collapse behavior for the sidebar panel.
*/
setupSidebarToggle: function () {
setupToolbar: function () {
// Setup the sidebar toggle button.
let SidebarToggle = this.React.createFactory(this.browserRequire(
"devtools/client/shared/components/sidebar-toggle"));
@ -529,6 +523,36 @@ InspectorPanel.prototype = {
let parentBox = this.panelDoc.getElementById("inspector-sidebar-toggle-box");
this._sidebarToggle = this.ReactDOM.render(sidebarToggle, parentBox);
// Setup the add-node button.
this.addNode = this.addNode.bind(this);
this.addNodeButton = this.panelDoc.getElementById("inspector-element-add-button");
this.addNodeButton.addEventListener("click", this.addNode);
// Setup the eye-dropper icon.
this.toolbox.target.actorHasMethod("inspector", "pickColorFromPage").then(value => {
if (!value) {
return;
}
this.onEyeDropperDone = this.onEyeDropperDone.bind(this);
this.onEyeDropperButtonClicked = this.onEyeDropperButtonClicked.bind(this);
this.eyeDropperButton = this.panelDoc.getElementById("inspector-eyedropper-toggle");
this.eyeDropperButton.style.display = "initial";
this.eyeDropperButton.addEventListener("click", this.onEyeDropperButtonClicked);
}, e => console.error(e));
},
teardownToolbar: function () {
if (this.addNodeButton) {
this.addNodeButton.removeEventListener("click", this.addNode);
this.addNodeButton = null;
}
if (this.eyeDropperButton) {
this.eyeDropperButton.removeEventListener("click", this.onEyeDropperButtonClicked);
this.eyeDropperButton = null;
}
},
/**
@ -768,7 +792,7 @@ InspectorPanel.prototype = {
let sidebarDestroyer = this.sidebar.destroy();
this.sidebar = null;
this.addNodeButton.removeEventListener("click", this.addNode);
this.teardownToolbar();
this.breadcrumbs.destroy();
this.selection.off("new-node-front", this.onNewSelection);
this.selection.off("before-new-node", this.onBeforeNewSelection);
@ -1251,6 +1275,52 @@ InspectorPanel.prototype = {
}, sidePaneContainer);
},
onEyeDropperButtonClicked: function () {
this.eyeDropperButton.hasAttribute("checked")
? this.hideEyeDropper()
: this.showEyeDropper();
},
startEyeDropperListeners: function () {
this.inspector.once("color-pick-canceled", this.onEyeDropperDone);
this.inspector.once("color-picked", this.onEyeDropperDone);
this.walker.once("new-root", this.onEyeDropperDone);
},
stopEyeDropperListeners: function () {
this.inspector.off("color-pick-canceled", this.onEyeDropperDone);
this.inspector.off("color-picked", this.onEyeDropperDone);
this.walker.off("new-root", this.onEyeDropperDone);
},
onEyeDropperDone: function () {
this.eyeDropperButton.removeAttribute("checked");
this.stopEyeDropperListeners();
},
/**
* Show the eyedropper on the page.
* @return {Promise} resolves when the eyedropper is visible.
*/
showEyeDropper: function () {
this.telemetry.toolOpened("toolbareyedropper");
this.eyeDropperButton.setAttribute("checked", "true");
this.startEyeDropperListeners();
return this.inspector.pickColorFromPage({copyOnSelect: true})
.catch(e => console.error(e));
},
/**
* Hide the eyedropper.
* @return {Promise} resolves when the eyedropper is hidden.
*/
hideEyeDropper: function () {
this.eyeDropperButton.removeAttribute("checked");
this.stopEyeDropperListeners();
return this.inspector.cancelPickColorFromPage()
.catch(e => console.error(e));
},
/**
* Create a new node as the last child of the current selection, expand the
* parent and select the new node.

Просмотреть файл

@ -45,8 +45,11 @@
timeout="50"
class="devtools-searchinput"
placeholder="&inspectorSearchHTML.label3;"/>
<html:button id="inspector-eyedropper-toggle"
title="&inspectorEyeDropper.label;"
class="devtools-button command-button-invertable" />
<div xmlns="http://www.w3.org/1999/xhtml"
id="inspector-sidebar-toggle-box" />
id="inspector-sidebar-toggle-box" />
</html:div>
<vbox flex="1" id="markup-box">
</vbox>
@ -88,83 +91,87 @@
</html:div>
<html:div id="ruleview-container" class="ruleview">
<html:div id="ruleview-container-focusable" tabindex="-1">
</html:div>
</html:div>
</html:div>
<html:div id="sidebar-panel-computedview" class="devtools-monospace theme-sidebar inspector-tabpanel">
<html:div class="devtools-toolbar">
<html:div class="devtools-searchbox">
<html:input id="computedview-searchbox"
class="devtools-filterinput devtools-rule-searchbox"
type="search"
placeholder="&filterStylesPlaceholder;"/>
<html:button id="computedview-searchinput-clear" class="devtools-searchinput-clear"></html:button>
</html:div>
<html:label id="browser-style-checkbox-label" for="browser-style-checkbox">
<html:input id="browser-style-checkbox"
type="checkbox"
class="includebrowserstyles"
label="&browserStylesLabel;"/>&browserStylesLabel;</html:label>
</html:div>
<html:div id="computedview-container">
<html:div id="layout-wrapper" class="theme-separator" tabindex="0">
<html:div id="layout-header">
<html:div id="layout-expander" class="expander theme-twisty expandable" open=""></html:div>
<html:span>&layoutViewTitle;</html:span>
<html:button class="devtools-button" id="layout-geometry-editor" title="&geometry.button.tooltip;"></html:button>
</html:div>
<html:div id="computedview-container-focusable" tabindex="-1">
<html:div id="layout-wrapper" tabindex="0">
<html:div id="layout-header">
<html:div id="layout-expander" class="expander theme-twisty expandable" open=""></html:div>
<html:span>&layoutViewTitle;</html:span>
<html:button class="devtools-button" id="layout-geometry-editor" title="&geometry.button.tooltip;"></html:button>
</html:div>
<html:div id="layout-container">
<html:div id="layout-main">
<html:span class="layout-legend" data-box="margin" title="&margin.tooltip;">&margin.tooltip;</html:span>
<html:div id="layout-margins" data-box="margin" title="&margin.tooltip;">
<html:span class="layout-legend" data-box="border" title="&border.tooltip;">&border.tooltip;</html:span>
<html:div id="layout-borders" data-box="border" title="&border.tooltip;">
<html:span class="layout-legend" data-box="padding" title="&padding.tooltip;">&padding.tooltip;</html:span>
<html:div id="layout-padding" data-box="padding" title="&padding.tooltip;">
<html:div id="layout-content" data-box="content" title="&content.tooltip;">
<html:div id="layout-container">
<html:div id="layout-main">
<html:span class="layout-legend" data-box="margin" title="&margin.tooltip;">&margin.tooltip;</html:span>
<html:div id="layout-margins" data-box="margin" title="&margin.tooltip;">
<html:span class="layout-legend" data-box="border" title="&border.tooltip;">&border.tooltip;</html:span>
<html:div id="layout-borders" data-box="border" title="&border.tooltip;">
<html:span class="layout-legend" data-box="padding" title="&padding.tooltip;">&padding.tooltip;</html:span>
<html:div id="layout-padding" data-box="padding" title="&padding.tooltip;">
<html:div id="layout-content" data-box="content" title="&content.tooltip;">
</html:div>
</html:div>
</html:div>
</html:div>
<html:p class="layout-margin layout-top"><html:span data-box="margin" class="layout-editable" title="margin-top"></html:span></html:p>
<html:p class="layout-margin layout-right"><html:span data-box="margin" class="layout-editable" title="margin-right"></html:span></html:p>
<html:p class="layout-margin layout-bottom"><html:span data-box="margin" class="layout-editable" title="margin-bottom"></html:span></html:p>
<html:p class="layout-margin layout-left"><html:span data-box="margin" class="layout-editable" title="margin-left"></html:span></html:p>
<html:p class="layout-border layout-top"><html:span data-box="border" class="layout-editable" title="border-top"></html:span></html:p>
<html:p class="layout-border layout-right"><html:span data-box="border" class="layout-editable" title="border-right"></html:span></html:p>
<html:p class="layout-border layout-bottom"><html:span data-box="border" class="layout-editable" title="border-bottom"></html:span></html:p>
<html:p class="layout-border layout-left"><html:span data-box="border" class="layout-editable" title="border-left"></html:span></html:p>
<html:p class="layout-padding layout-top"><html:span data-box="padding" class="layout-editable" title="padding-top"></html:span></html:p>
<html:p class="layout-padding layout-right"><html:span data-box="padding" class="layout-editable" title="padding-right"></html:span></html:p>
<html:p class="layout-padding layout-bottom"><html:span data-box="padding" class="layout-editable" title="padding-bottom"></html:span></html:p>
<html:p class="layout-padding layout-left"><html:span data-box="padding" class="layout-editable" title="padding-left"></html:span></html:p>
<html:p class="layout-size"><html:span data-box="content" title="&content.tooltip;"></html:span></html:p>
</html:div>
<html:p class="layout-margin layout-top"><html:span data-box="margin" class="layout-editable" title="margin-top"></html:span></html:p>
<html:p class="layout-margin layout-right"><html:span data-box="margin" class="layout-editable" title="margin-right"></html:span></html:p>
<html:p class="layout-margin layout-bottom"><html:span data-box="margin" class="layout-editable" title="margin-bottom"></html:span></html:p>
<html:p class="layout-margin layout-left"><html:span data-box="margin" class="layout-editable" title="margin-left"></html:span></html:p>
<html:div id="layout-info">
<html:span id="layout-element-size"></html:span>
<html:section id="layout-position-group">
<html:span id="layout-element-position"></html:span>
</html:section>
</html:div>
<html:p class="layout-border layout-top"><html:span data-box="border" class="layout-editable" title="border-top"></html:span></html:p>
<html:p class="layout-border layout-right"><html:span data-box="border" class="layout-editable" title="border-right"></html:span></html:p>
<html:p class="layout-border layout-bottom"><html:span data-box="border" class="layout-editable" title="border-bottom"></html:span></html:p>
<html:p class="layout-border layout-left"><html:span data-box="border" class="layout-editable" title="border-left"></html:span></html:p>
<html:p class="layout-padding layout-top"><html:span data-box="padding" class="layout-editable" title="padding-top"></html:span></html:p>
<html:p class="layout-padding layout-right"><html:span data-box="padding" class="layout-editable" title="padding-right"></html:span></html:p>
<html:p class="layout-padding layout-bottom"><html:span data-box="padding" class="layout-editable" title="padding-bottom"></html:span></html:p>
<html:p class="layout-padding layout-left"><html:span data-box="padding" class="layout-editable" title="padding-left"></html:span></html:p>
<html:p class="layout-size"><html:span data-box="content" title="&content.tooltip;"></html:span></html:p>
</html:div>
<html:div id="layout-info">
<html:span id="layout-element-size"></html:span>
<html:section id="layout-position-group">
<html:span id="layout-element-position"></html:span>
</html:section>
</html:div>
<html:div style="display: none">
<html:p id="layout-dummy"></html:p>
<html:div style="display: none">
<html:p id="layout-dummy"></html:p>
</html:div>
</html:div>
</html:div>
</html:div>
<html:div id="propertyContainer" class="theme-separator" tabindex="0">
</html:div>
<html:div id="computedview-toolbar" class="devtools-toolbar">
<html:div class="devtools-searchbox">
<html:input id="computedview-searchbox"
class="devtools-filterinput devtools-rule-searchbox"
type="search"
placeholder="&filterStylesPlaceholder;"/>
<html:button id="computedview-searchinput-clear" class="devtools-searchinput-clear"></html:button>
</html:div>
<html:label id="browser-style-checkbox-label" for="browser-style-checkbox">
<html:input id="browser-style-checkbox"
type="checkbox"
class="includebrowserstyles"
label="&browserStylesLabel;"/>&browserStylesLabel;</html:label>
</html:div>
<html:div id="computedview-no-results" hidden="">
&noPropertiesFound;
<html:div id="propertyContainer" class="theme-separator" tabindex="0">
</html:div>
<html:div id="computedview-no-results" hidden="">
&noPropertiesFound;
</html:div>
</html:div>
</html:div>
</html:div>

Просмотреть файл

@ -174,7 +174,7 @@ function CssRuleView(inspector, document, store, pageStyle) {
this._onTogglePseudoClass = this._onTogglePseudoClass.bind(this);
let doc = this.styleDocument;
this.element = doc.getElementById("ruleview-container");
this.element = doc.getElementById("ruleview-container-focusable");
this.addRuleButton = doc.getElementById("ruleview-add-rule-button");
this.searchField = doc.getElementById("ruleview-searchbox");
this.searchClearButton = doc.getElementById("ruleview-searchinput-clear");

Просмотреть файл

@ -76,7 +76,7 @@ add_task(function* () {
});
function* clickOnRuleviewScrollbar(view) {
let container = view.element;
let container = view.element.parentNode;
let onScroll = once(container, "scroll");
let rect = container.getBoundingClientRect();
// click 5 pixels before the bottom-right corner should hit the scrollbar

Просмотреть файл

@ -1,19 +1,10 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// So we can test collecting telemetry on the eyedropper
var oldCanRecord = Services.telemetry.canRecordExtended;
Services.telemetry.canRecordExtended = true;
registerCleanupFunction(function () {
Services.telemetry.canRecordExtended = oldCanRecord;
});
const EXPECTED_TELEMETRY = {
"DEVTOOLS_PICKER_EYEDROPPER_OPENED_COUNT": 2,
"DEVTOOLS_PICKER_EYEDROPPER_OPENED_PER_USER_FLAG": 1
};
// Test opening the eyedropper from the color picker. Pressing escape to close it, and
// clicking the page to select a color.
const TEST_URI = `
<style type="text/css">
@ -43,61 +34,61 @@ const ORIGINAL_COLOR = "rgb(255, 0, 153)";
// #ff5
const EXPECTED_COLOR = "rgb(255, 255, 85)";
// Test opening the eyedropper from the color picker. Pressing escape
// to close it, and clicking the page to select a color.
add_task(function* () {
// clear telemetry so we can get accurate counts
clearTelemetry();
info("Add the test tab, open the rule-view and select the test node");
yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
let {inspector, view} = yield openRuleView();
let {testActor, inspector, view} = yield openRuleView();
yield selectNode("#div2", inspector);
info("Get the background-color property from the rule-view");
let property = getRuleViewProperty(view, "#div2", "background-color");
let swatch = property.valueSpan.querySelector(".ruleview-colorswatch");
ok(swatch, "Color swatch is displayed for the bg-color property");
let dropper = yield openEyedropper(view, swatch);
info("Open the eyedropper from the colorpicker tooltip");
yield openEyedropper(view, swatch);
let tooltip = view.tooltips.colorPicker.tooltip;
ok(!tooltip.isVisible(),
"color picker tooltip is closed after opening eyedropper");
ok(!tooltip.isVisible(), "color picker tooltip is closed after opening eyedropper");
yield testESC(swatch, dropper);
info("Test that pressing escape dismisses the eyedropper");
yield testESC(swatch, inspector, testActor);
dropper = yield openEyedropper(view, swatch);
info("Open the eyedropper again");
yield openEyedropper(view, swatch);
ok(dropper, "dropper opened");
yield testSelect(view, swatch, dropper);
checkTelemetry();
info("Test that a color can be selected with the eyedropper");
yield testSelect(view, swatch, inspector, testActor);
});
function testESC(swatch, dropper) {
let deferred = defer();
dropper.once("destroy", () => {
let color = swatch.style.backgroundColor;
is(color, ORIGINAL_COLOR, "swatch didn't change after pressing ESC");
deferred.resolve();
function* testESC(swatch, inspector, testActor) {
info("Press escape");
let onCanceled = new Promise(resolve => {
inspector.inspector.once("color-pick-canceled", resolve);
});
yield testActor.synthesizeKey({key: "VK_ESCAPE", options: {}});
yield onCanceled;
inspectPage(dropper, false).then(pressESC);
return deferred.promise;
let color = swatch.style.backgroundColor;
is(color, ORIGINAL_COLOR, "swatch didn't change after pressing ESC");
}
function* testSelect(view, swatch, dropper) {
let onDestroyed = dropper.once("destroy");
// the change to the content is done async after rule view change
function* testSelect(view, swatch, inspector, testActor) {
info("Click at x:10px y:10px");
let onPicked = new Promise(resolve => {
inspector.inspector.once("color-picked", resolve);
});
// The change to the content is done async after rule view change
let onRuleViewChanged = view.once("ruleview-changed");
inspectPage(dropper);
yield testActor.synthesizeMouse({selector: "html", x: 10, y: 10,
options: {type: "mousemove"}});
yield testActor.synthesizeMouse({selector: "html", x: 10, y: 10,
options: {type: "mousedown"}});
yield testActor.synthesizeMouse({selector: "html", x: 10, y: 10,
options: {type: "mouseup"}});
yield onDestroyed;
yield onPicked;
yield onRuleViewChanged;
let color = swatch.style.backgroundColor;
@ -108,81 +99,18 @@ function* testSelect(view, swatch, dropper) {
"div's color set to body color after dropper");
}
function clearTelemetry() {
for (let histogramId in EXPECTED_TELEMETRY) {
let histogram = Services.telemetry.getHistogramById(histogramId);
histogram.clear();
}
}
function checkTelemetry() {
for (let histogramId in EXPECTED_TELEMETRY) {
let expected = EXPECTED_TELEMETRY[histogramId];
let histogram = Services.telemetry.getHistogramById(histogramId);
let snapshot = histogram.snapshot();
is(snapshot.sum, expected,
"eyedropper telemetry value correct for " + histogramId);
}
}
/* Helpers */
function openEyedropper(view, swatch) {
let deferred = defer();
function* openEyedropper(view, swatch) {
let tooltip = view.tooltips.colorPicker.tooltip;
tooltip.once("shown", () => {
let dropperButton = tooltip.doc.querySelector("#eyedropper-button");
tooltip.once("eyedropper-opened", (event, dropper) => {
deferred.resolve(dropper);
});
dropperButton.click();
});
info("Click on the swatch");
let onShown = tooltip.once("shown");
swatch.click();
return deferred.promise;
}
function inspectPage(dropper, click = true) {
let target = document.documentElement;
let win = window;
// get location of the content, offset from browser window
let box = gBrowser.selectedBrowser.getBoundingClientRect();
let x = box.left + 1;
let y = box.top + 1;
return dropperStarted(dropper).then(() => {
EventUtils.synthesizeMouse(target, x, y, { type: "mousemove" }, win);
return dropperLoaded(dropper).then(() => {
EventUtils.synthesizeMouse(target, x + 10, y + 10,
{ type: "mousemove" }, win);
if (click) {
EventUtils.synthesizeMouse(target, x + 10, y + 10, {}, win);
}
});
});
}
function dropperStarted(dropper) {
if (dropper.isStarted) {
return promise.resolve();
}
return dropper.once("started");
}
function dropperLoaded(dropper) {
if (dropper.loaded) {
return promise.resolve();
}
return dropper.once("load");
}
function pressESC() {
EventUtils.synthesizeKey("VK_ESCAPE", { });
yield onShown;
let dropperButton = tooltip.doc.querySelector("#eyedropper-button");
info("Click on the eyedropper icon");
let onOpened = tooltip.once("eyedropper-opened");
dropperButton.click();
yield onOpened;
}

Просмотреть файл

@ -293,7 +293,7 @@ TooltipsOverlay.prototype = {
if (this.isRuleView) {
// Color picker tooltip
this.colorPicker = new SwatchColorPickerTooltip(toolbox);
this.colorPicker = new SwatchColorPickerTooltip(toolbox, this.view.inspector);
// Cubic bezier tooltip
this.cubicBezier = new SwatchCubicBezierTooltip(toolbox);
// Filter editor tooltip

Просмотреть файл

@ -63,6 +63,10 @@ skip-if = os == "mac" # Full keyboard navigation on OSX only works if Full Keybo
[browser_inspector_highlighter-csstransform_01.js]
[browser_inspector_highlighter-csstransform_02.js]
[browser_inspector_highlighter-embed.js]
[browser_inspector_highlighter-eyedropper-clipboard.js]
subsuite = clipboard
[browser_inspector_highlighter-eyedropper-events.js]
[browser_inspector_highlighter-eyedropper-show-hide.js]
[browser_inspector_highlighter-geometry_01.js]
[browser_inspector_highlighter-geometry_02.js]
[browser_inspector_highlighter-geometry_03.js]
@ -134,6 +138,7 @@ skip-if = os == "mac" # Full keyboard navigation on OSX only works if Full Keybo
[browser_inspector_search_keyboard_trap.js]
[browser_inspector_search-reserved.js]
[browser_inspector_search-selection.js]
[browser_inspector_search-sidebar.js]
[browser_inspector_select-docshell.js]
[browser_inspector_select-last-selected.js]
[browser_inspector_search-navigation.js]

Просмотреть файл

@ -0,0 +1,65 @@
/* 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";
// Test that the eyedropper can copy colors to the clipboard
const HIGHLIGHTER_TYPE = "EyeDropper";
const ID = "eye-dropper-";
const TEST_URI = "data:text/html;charset=utf-8,<style>html{background:red}</style>";
add_task(function* () {
let helper = yield openInspectorForURL(TEST_URI)
.then(getHighlighterHelperFor(HIGHLIGHTER_TYPE));
helper.prefix = ID;
let {show, synthesizeKey, finalize} = helper;
info("Show the eyedropper with the copyOnSelect option");
yield show("html", {copyOnSelect: true});
info("Make sure to wait until the eyedropper is done taking a screenshot of the page");
yield waitForElementAttributeSet("root", "drawn", helper);
yield waitForClipboard(() => {
info("Activate the eyedropper so the background color is copied");
let generateKey = synthesizeKey({key: "VK_RETURN", options: {}});
generateKey.next();
}, "#FF0000");
ok(true, "The clipboard contains the right value");
yield waitForElementAttributeRemoved("root", "drawn", helper);
yield waitForElementAttributeSet("root", "hidden", helper);
ok(true, "The eyedropper is now hidden");
finalize();
});
function* waitForElementAttributeSet(id, name, {getElementAttribute}) {
yield poll(function* () {
let value = yield getElementAttribute(id, name);
return !!value;
}, `Waiting for element ${id} to have attribute ${name} set`);
}
function* waitForElementAttributeRemoved(id, name, {getElementAttribute}) {
yield poll(function* () {
let value = yield getElementAttribute(id, name);
return !value;
}, `Waiting for element ${id} to have attribute ${name} removed`);
}
function* poll(check, desc) {
info(desc);
for (let i = 0; i < 10; i++) {
if (yield check()) {
return;
}
yield new Promise(resolve => setTimeout(resolve, 200));
}
throw new Error(`Timeout while: ${desc}`);
}

Просмотреть файл

@ -0,0 +1,71 @@
/* 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";
// Test the eyedropper mouse and keyboard handling.
const HIGHLIGHTER_TYPE = "EyeDropper";
const ID = "eye-dropper-";
const MOVE_EVENTS_DATA = [
{type: "mouse", x: 200, y: 100, expected: {x: 200, y: 100}},
{type: "mouse", x: 100, y: 200, expected: {x: 100, y: 200}},
{type: "keyboard", key: "VK_LEFT", expected: {x: 99, y: 200}},
{type: "keyboard", key: "VK_LEFT", shift: true, expected: {x: 89, y: 200}},
{type: "keyboard", key: "VK_RIGHT", expected: {x: 90, y: 200}},
{type: "keyboard", key: "VK_RIGHT", shift: true, expected: {x: 100, y: 200}},
{type: "keyboard", key: "VK_DOWN", expected: {x: 100, y: 201}},
{type: "keyboard", key: "VK_DOWN", shift: true, expected: {x: 100, y: 211}},
{type: "keyboard", key: "VK_UP", expected: {x: 100, y: 210}},
{type: "keyboard", key: "VK_UP", shift: true, expected: {x: 100, y: 200}},
];
add_task(function* () {
let helper = yield openInspectorForURL("data:text/html;charset=utf-8,eye-dropper test")
.then(getHighlighterHelperFor(HIGHLIGHTER_TYPE));
helper.prefix = ID;
yield helper.show("html");
yield respondsToMoveEvents(helper);
yield respondsToReturnAndEscape(helper);
helper.finalize();
});
function* respondsToMoveEvents(helper) {
info("Checking that the eyedropper responds to events from the mouse and keyboard");
let {mouse, synthesizeKey} = helper;
for (let {type, x, y, key, shift, expected} of MOVE_EVENTS_DATA) {
info(`Simulating a ${type} event to move to ${expected.x} ${expected.y}`);
if (type === "mouse") {
yield mouse.move(x, y);
} else if (type === "keyboard") {
let options = shift ? {shiftKey: true} : {};
yield synthesizeKey({key, options});
}
yield checkPosition(expected, helper);
}
}
function* checkPosition({x, y}, {getElementAttribute}) {
let style = yield getElementAttribute("root", "style");
is(style, `top:${y}px;left:${x}px;`,
`The eyedropper is at the expected ${x} ${y} position`);
}
function* respondsToReturnAndEscape({synthesizeKey, isElementHidden, show}) {
info("Simulating return to select the color and hide the eyedropper");
yield synthesizeKey({key: "VK_RETURN", options: {}});
let hidden = yield isElementHidden("root");
ok(hidden, "The eyedropper has been hidden");
info("Showing the eyedropper again and simulating escape to hide it");
yield show("html");
yield synthesizeKey({key: "VK_ESCAPE", options: {}});
hidden = yield isElementHidden("root");
ok(hidden, "The eyedropper has been hidden again");
}

Просмотреть файл

@ -0,0 +1,42 @@
/* 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";
// Test the basic structure of the eye-dropper highlighter.
const HIGHLIGHTER_TYPE = "EyeDropper";
const ID = "eye-dropper-";
add_task(function* () {
let helper = yield openInspectorForURL("data:text/html;charset=utf-8,eye-dropper test")
.then(getHighlighterHelperFor(HIGHLIGHTER_TYPE));
helper.prefix = ID;
yield isInitiallyHidden(helper);
yield canBeShownAndHidden(helper);
helper.finalize();
});
function* isInitiallyHidden({isElementHidden}) {
info("Checking that the eyedropper is hidden by default");
let hidden = yield isElementHidden("root");
ok(hidden, "The eyedropper is hidden by default");
}
function* canBeShownAndHidden({show, hide, isElementHidden, getElementAttribute}) {
info("Asking to show and hide the highlighter actually works");
yield show("html");
let hidden = yield isElementHidden("root");
ok(!hidden, "The eyedropper is now shown");
let style = yield getElementAttribute("root", "style");
is(style, "top:100px;left:100px;", "The eyedropper is correctly positioned");
yield hide();
hidden = yield isElementHidden("root");
ok(hidden, "The eyedropper is now hidden again");
}

Просмотреть файл

@ -0,0 +1,74 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that depending where the user last clicked in the inspector, the right search
// field is focused when ctrl+F is pressed.
add_task(function* () {
let {inspector} = yield openInspectorForURL("data:text/html;charset=utf-8,Search!");
info("Check that by default, the inspector search field gets focused");
pressCtrlF();
isInInspectorSearchBox(inspector);
info("Click somewhere in the rule-view");
clickInRuleView(inspector);
info("Check that the rule-view search field gets focused");
pressCtrlF();
isInRuleViewSearchBox(inspector);
info("Click in the inspector again");
yield clickContainer("head", inspector);
info("Check that now we're back in the inspector, its search field gets focused");
pressCtrlF();
isInInspectorSearchBox(inspector);
info("Switch to the computed view, and click somewhere inside it");
selectComputedView(inspector);
clickInComputedView(inspector);
info("Check that the computed-view search field gets focused");
pressCtrlF();
isInComputedViewSearchBox(inspector);
info("Click in the inspector yet again");
yield clickContainer("body", inspector);
info("We're back in the inspector again, check the inspector search field focuses");
pressCtrlF();
isInInspectorSearchBox(inspector);
});
function pressCtrlF() {
EventUtils.synthesizeKey("f", {accelKey: true});
}
function clickInRuleView(inspector) {
let el = inspector.panelDoc.querySelector("#sidebar-panel-ruleview");
EventUtils.synthesizeMouseAtCenter(el, {}, inspector.panelDoc.defaultView);
}
function clickInComputedView(inspector) {
let el = inspector.panelDoc.querySelector("#sidebar-panel-computedview");
EventUtils.synthesizeMouseAtCenter(el, {}, inspector.panelDoc.defaultView);
}
function isInInspectorSearchBox(inspector) {
// Focus ends up in an anonymous child of the XUL textbox.
ok(inspector.panelDoc.activeElement.closest("#inspector-searchbox"),
"The inspector search field is focused when ctrl+F is pressed");
}
function isInRuleViewSearchBox(inspector) {
is(inspector.panelDoc.activeElement, inspector.ruleview.view.searchField,
"The rule-view search field is focused when ctrl+F is pressed");
}
function isInComputedViewSearchBox(inspector) {
is(inspector.panelDoc.activeElement, inspector.computedview.computedView.searchField,
"The computed-view search field is focused when ctrl+F is pressed");
}

Просмотреть файл

@ -422,6 +422,7 @@ const getHighlighterHelperFor = (type) => Task.async(
set prefix(value) {
prefix = value;
},
get highlightedNode() {
if (!highlightedNode) {
return null;
@ -435,9 +436,9 @@ const getHighlighterHelperFor = (type) => Task.async(
};
},
show: function* (selector = ":root") {
show: function* (selector = ":root", options) {
highlightedNode = yield getNodeFront(selector, inspector);
return yield highlighter.show(highlightedNode);
return yield highlighter.show(highlightedNode, options);
},
hide: function* () {
@ -464,6 +465,10 @@ const getHighlighterHelperFor = (type) => Task.async(
yield testActor.synthesizeMouse(options);
},
synthesizeKey: function* (options) {
yield testActor.synthesizeKey(options);
},
// This object will synthesize any "mouse" prefixed event to the
// `testActor`, using the name of method called as suffix for the
// event's name.

Просмотреть файл

@ -131,9 +131,6 @@ devtools.jar:
content/shared/widgets/mdn-docs.css (shared/widgets/mdn-docs.css)
content/shared/widgets/filter-widget.css (shared/widgets/filter-widget.css)
content/shared/widgets/spectrum.css (shared/widgets/spectrum.css)
content/eyedropper/eyedropper.xul (eyedropper/eyedropper.xul)
content/eyedropper/crosshairs.css (eyedropper/crosshairs.css)
content/eyedropper/nocursor.css (eyedropper/nocursor.css)
content/aboutdebugging/aboutdebugging.xhtml (aboutdebugging/aboutdebugging.xhtml)
content/aboutdebugging/aboutdebugging.css (aboutdebugging/aboutdebugging.css)
content/aboutdebugging/initializer.js (aboutdebugging/initializer.js)

Просмотреть файл

@ -15,3 +15,8 @@
DOM (as children of the currently selected element). -->
<!ENTITY inspectorAddNode.label "Create New Node">
<!ENTITY inspectorAddNode.accesskey "C">
<!-- LOCALIZATION NOTE (inspectorEyeDropper.label): A string displayed as the tooltip of
a button in the inspector which toggles the Eyedropper tool -->
<!ENTITY inspectorEyeDropper.label "Grab a color from the page">

Просмотреть файл

@ -37,7 +37,8 @@ const Services = require("Services");
const isMac = Services.appinfo.OS === "Darwin";
loader.lazyRequireGetter(this, "gDevToolsBrowser", "devtools/client/framework/devtools-browser", true);
loader.lazyRequireGetter(this, "Eyedropper", "devtools/client/eyedropper/eyedropper", true);
loader.lazyRequireGetter(this, "CommandUtils", "devtools/client/shared/developer-toolbar", true);
loader.lazyRequireGetter(this, "TargetFactory", "devtools/client/framework/target", true);
loader.lazyImporter(this, "BrowserToolboxProcess", "resource://devtools/client/framework/ToolboxProcess.jsm");
loader.lazyImporter(this, "ResponsiveUIManager", "resource://devtools/client/responsivedesign/responsivedesign.jsm");
@ -144,9 +145,13 @@ exports.menuitems = [
l10nKey: "eyedropper",
oncommand(event) {
let window = event.target.ownerDocument.defaultView;
let eyedropper = new Eyedropper(window, { context: "menu",
copyOnSelect: true });
eyedropper.open();
let target = TargetFactory.forTab(window.gBrowser.selectedTab);
CommandUtils.createRequisition(target, {
environment: CommandUtils.createEnvironment({target})
}).then(requisition => {
requisition.updateExec("eyedropper --frommenu");
}, e => console.error(e));
},
checkbox: true
},

Просмотреть файл

@ -13,7 +13,6 @@ DIRS += [
'commandline',
'debugger',
'dom',
'eyedropper',
'framework',
'inspector',
'jsonview',

Просмотреть файл

@ -18,7 +18,7 @@ const { CanvasGraphUtils } = require("devtools/client/shared/widgets/Graphs");
const promise = require("promise");
const EventEmitter = require("devtools/shared/event-emitter");
const { colorUtils } = require("devtools/client/shared/css-color");
const { colorUtils } = require("devtools/shared/css-color");
const { getColor } = require("devtools/client/shared/theme");
const ProfilerGlobal = require("devtools/client/performance/modules/global");
const { MarkersOverview } = require("devtools/client/performance/modules/widgets/markers-overview");

Просмотреть файл

@ -13,7 +13,7 @@ const { Cc, Ci, Cu, Cr } = require("chrome");
const { Heritage } = require("devtools/client/shared/widgets/view-helpers");
const { AbstractCanvasGraph } = require("devtools/client/shared/widgets/Graphs");
const { colorUtils } = require("devtools/client/shared/css-color");
const { colorUtils } = require("devtools/shared/css-color");
const { getColor } = require("devtools/client/shared/theme");
const ProfilerGlobal = require("devtools/client/performance/modules/global");
const { MarkerBlueprintUtils } = require("devtools/client/performance/modules/marker-blueprint-utils");

Просмотреть файл

@ -30,7 +30,7 @@ pref("devtools.toolbox.sidebar.width", 500);
pref("devtools.toolbox.host", "bottom");
pref("devtools.toolbox.previousHost", "side");
pref("devtools.toolbox.selectedTool", "webconsole");
pref("devtools.toolbox.toolbarSpec", '["splitconsole", "paintflashing toggle","scratchpad","resize toggle","eyedropper","screenshot --fullpage", "rulers", "measure"]');
pref("devtools.toolbox.toolbarSpec", '["splitconsole", "paintflashing toggle","scratchpad","resize toggle","screenshot --fullpage", "rulers", "measure"]');
pref("devtools.toolbox.sideEnabled", true);
pref("devtools.toolbox.zoomValue", "1");
pref("devtools.toolbox.splitconsoleEnabled", false);
@ -43,7 +43,6 @@ pref("devtools.command-button-splitconsole.enabled", true);
pref("devtools.command-button-paintflashing.enabled", false);
pref("devtools.command-button-scratchpad.enabled", false);
pref("devtools.command-button-responsive.enabled", true);
pref("devtools.command-button-eyedropper.enabled", false);
pref("devtools.command-button-screenshot.enabled", false);
pref("devtools.command-button-rulers.enabled", false);
pref("devtools.command-button-measure.enabled", false);

Просмотреть файл

@ -35,8 +35,9 @@ define(function (require, exports, module) {
},
getTitle: function (object, context) {
if (this.props.objectLink) {
return this.props.objectLink({
let objectLink = this.props.objectLink || span;
if (this.props.mode != "tiny") {
return objectLink({
object: object
}, object.class);
}
@ -117,11 +118,12 @@ define(function (require, exports, module) {
}
let objectLink = this.props.objectLink || span;
let title = this.getTitle(object);
return (
ObjectBox({
className: "array"},
this.getTitle(object),
title,
objectLink({
className: "arrayLeftBracket",
role: "presentation",

Просмотреть файл

@ -14,7 +14,6 @@ DevToolsModules(
'function.js',
'grip-array.js',
'grip.js',
'named-node-map.js',
'null.js',
'number.js',
'object-box.js',

Просмотреть файл

@ -1,180 +0,0 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* 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";
// Make this available to both AMD and CJS environments
define(function (require, exports, module) {
// ReactJS
const React = require("devtools/client/shared/vendor/react");
// Reps
const { createFactories, isGrip } = require("./rep-utils");
const { ObjectBox } = createFactories(require("./object-box"));
const { Caption } = createFactories(require("./caption"));
// Shortcuts
const { span } = React.DOM;
/**
* Used to render a map of values provided as a grip.
*/
let NamedNodeMap = React.createClass({
displayName: "NamedNodeMap",
propTypes: {
object: React.PropTypes.object.isRequired,
mode: React.PropTypes.string,
provider: React.PropTypes.object,
},
getLength: function (object) {
return object.preview.length;
},
getTitle: function (object) {
if (this.props.objectLink && object.class) {
return this.props.objectLink({
object: object
}, object.class);
}
return object.class ? object.class : "";
},
getItems: function (array, max) {
let items = this.propIterator(array, max);
items = items.map(item => PropRep(item));
if (items.length > max + 1) {
items.pop();
let objectLink = this.props.objectLink || span;
items.push(Caption({
key: "more",
object: objectLink({
object: this.props.object
}, "more…")
}));
}
return items;
},
propIterator: function (grip, max) {
max = max || 3;
let props = [];
let provider = this.props.provider;
if (!provider) {
return props;
}
let ownProperties = grip.preview ? grip.preview.ownProperties : [];
for (let name in ownProperties) {
if (props.length > max) {
break;
}
let item = ownProperties[name];
let label = provider.getLabel(item);
let value = provider.getValue(item);
props.push(Object.assign({}, this.props, {
name: label,
object: value,
equal: ": ",
delim: ", ",
}));
}
return props;
},
render: function () {
let grip = this.props.object;
let mode = this.props.mode;
let items;
if (mode == "tiny") {
items = this.getLength(grip);
} else {
let max = (mode == "short") ? 3 : 100;
items = this.getItems(grip, max);
}
let objectLink = this.props.objectLink || span;
return (
ObjectBox({className: "NamedNodeMap"},
this.getTitle(grip),
objectLink({
className: "arrayLeftBracket",
role: "presentation",
object: grip
}, "["),
items,
objectLink({
className: "arrayRightBracket",
role: "presentation",
object: grip
}, "]")
)
);
},
});
/**
* Property for a grip object.
*/
let PropRep = React.createFactory(React.createClass({
displayName: "PropRep",
propTypes: {
equal: React.PropTypes.string,
delim: React.PropTypes.string,
},
render: function () {
const { Rep } = createFactories(require("./rep"));
return (
span({},
span({
className: "nodeName"},
"$prop.name"
),
span({
className: "objectEqual",
role: "presentation"},
this.props.equal
),
Rep(this.props),
span({
className: "objectComma",
role: "presentation"},
this.props.delim
)
)
);
}
}));
// Registration
function supportsObject(grip, type) {
if (!isGrip(grip)) {
return false;
}
return (type == "NamedNodeMap" && grip.preview);
}
// Exports from this module
exports.NamedNodeMap = {
rep: NamedNodeMap,
supportsObject: supportsObject
};
});

Просмотреть файл

@ -27,7 +27,6 @@ define(function (require, exports, module) {
const { Document } = require("./document");
const { Event } = require("./event");
const { Func } = require("./function");
const { NamedNodeMap } = require("./named-node-map");
const { RegExp } = require("./regexp");
const { StyleSheet } = require("./stylesheet");
const { TextNode } = require("./text-node");
@ -46,7 +45,6 @@ define(function (require, exports, module) {
Event,
DateTime,
TextNode,
NamedNodeMap,
Attribute,
Func,
ArrayRep,

Просмотреть файл

@ -32,6 +32,8 @@ window.onload = Task.async(function* () {
yield testMoreThanShortMaxProps();
yield testMoreThanLongMaxProps();
yield testRecursiveArray();
yield testNamedNodeMap();
} catch(e) {
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
} finally {
@ -48,7 +50,7 @@ window.onload = Task.async(function* () {
is(renderedRep.type, GripArray.rep, `Rep correctly selects ${GripArray.rep.displayName}`);
// Test rendering
const defaultOutput = `[]`;
const defaultOutput = `Array[]`;
const modeTests = [
{
@ -76,7 +78,7 @@ window.onload = Task.async(function* () {
// Test array: `[1, "foo", {}]`;
const testName = "testMaxProps";
const defaultOutput = `[1, "foo", Object]`;
const defaultOutput = `Array[1, "foo", Object]`;
const modeTests = [
{
@ -104,7 +106,7 @@ window.onload = Task.async(function* () {
// Test array = `["test string"…] //4 items`
const testName = "testMoreThanShortMaxProps";
const defaultOutput = `[${Array(maxLength.short).fill("\"test string\"").join(", ")}, more…]`;
const defaultOutput = `Array[${Array(maxLength.short).fill("\"test string\"").join(", ")}, more…]`;
const modeTests = [
{
@ -121,7 +123,7 @@ window.onload = Task.async(function* () {
},
{
mode: "long",
expectedOutput: `[${Array(maxLength.short + 1).fill("\"test string\"").join(", ")}]`,
expectedOutput: `Array[${Array(maxLength.short + 1).fill("\"test string\"").join(", ")}]`,
}
];
@ -132,8 +134,8 @@ window.onload = Task.async(function* () {
// Test array = `["test string"…] //301 items`
const testName = "testMoreThanLongMaxProps";
const defaultShortOutput = `[${Array(maxLength.short).fill("\"test string\"").join(", ")}, more…]`;
const defaultLongOutput = `[${Array(maxLength.long).fill("\"test string\"").join(", ")}, more…]`;
const defaultShortOutput = `Array[${Array(maxLength.short).fill("\"test string\"").join(", ")}, more…]`;
const defaultLongOutput = `Array[${Array(maxLength.long).fill("\"test string\"").join(", ")}, more…]`;
const modeTests = [
{
@ -164,7 +166,7 @@ window.onload = Task.async(function* () {
// Test array = `let a = []; a = [a]`
const testName = "testRecursiveArray";
const defaultOutput = `[[1]]`;
const defaultOutput = `Array[[1]]`;
const modeTests = [
{
@ -188,6 +190,33 @@ window.onload = Task.async(function* () {
testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
}
function testNamedNodeMap() {
const testName = "testNamedNodeMap";
const defaultOutput = `NamedNodeMap[class="myclass", cellpadding="7", border="3"]`;
const modeTests = [
{
mode: undefined,
expectedOutput: defaultOutput,
},
{
mode: "tiny",
expectedOutput: `[3]`,
},
{
mode: "short",
expectedOutput: defaultOutput,
},
{
mode: "long",
expectedOutput: defaultOutput,
}
];
testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
}
function getGripStub(functionName) {
switch (functionName) {
case "testBasic":
@ -311,6 +340,68 @@ window.onload = Task.async(function* () {
]
}
};
case "testNamedNodeMap":
return {
"type": "object",
"class": "NamedNodeMap",
"actor": "server1.conn3.obj42",
"extensible": true,
"frozen": false,
"sealed": false,
"ownPropertyLength": 6,
"preview": {
"kind": "ArrayLike",
"length": 3,
"items": [
{
"type": "object",
"class": "Attr",
"actor": "server1.conn3.obj43",
"extensible": true,
"frozen": false,
"sealed": false,
"ownPropertyLength": 0,
"preview": {
"kind": "DOMNode",
"nodeType": 2,
"nodeName": "class",
"value": "myclass"
}
},
{
"type": "object",
"class": "Attr",
"actor": "server1.conn3.obj44",
"extensible": true,
"frozen": false,
"sealed": false,
"ownPropertyLength": 0,
"preview": {
"kind": "DOMNode",
"nodeType": 2,
"nodeName": "cellpadding",
"value": "7"
}
},
{
"type": "object",
"class": "Attr",
"actor": "server1.conn3.obj44",
"extensible": true,
"frozen": false,
"sealed": false,
"ownPropertyLength": 0,
"preview": {
"kind": "DOMNode",
"nodeType": 2,
"nodeName": "border",
"value": "3"
}
}
]
}
};
}
}
});

Просмотреть файл

@ -20,8 +20,6 @@ DevToolsModules(
'autocomplete-popup.js',
'browser-loader.js',
'css-angle.js',
'css-color-db.js',
'css-color.js',
'css-reload.js',
'Curl.jsm',
'demangle.js',

Просмотреть файл

@ -6,7 +6,7 @@
const {Cc, Ci} = require("chrome");
const {angleUtils} = require("devtools/client/shared/css-angle");
const {colorUtils} = require("devtools/client/shared/css-color");
const {colorUtils} = require("devtools/shared/css-color");
const {getCSSLexer} = require("devtools/shared/css-lexer");
const EventEmitter = require("devtools/shared/event-emitter");
const {

Просмотреть файл

@ -187,6 +187,10 @@ Telemetry.prototype = {
histogram: "DEVTOOLS_PICKER_EYEDROPPER_OPENED_COUNT",
userHistogram: "DEVTOOLS_PICKER_EYEDROPPER_OPENED_PER_USER_FLAG",
},
toolbareyedropper: {
histogram: "DEVTOOLS_TOOLBAR_EYEDROPPER_OPENED_COUNT",
userHistogram: "DEVTOOLS_TOOLBAR_EYEDROPPER_OPENED_PER_USER_FLAG",
},
developertoolbar: {
histogram: "DEVTOOLS_DEVELOPERTOOLBAR_OPENED_COUNT",
userHistogram: "DEVTOOLS_DEVELOPERTOOLBAR_OPENED_PER_USER_FLAG",

Просмотреть файл

@ -2,7 +2,7 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
const TEST_URI = "data:text/html;charset=utf-8,browser_css_color.js";
var {colorUtils} = require("devtools/client/shared/css-color");
var {colorUtils} = require("devtools/shared/css-color");
var origColorUnit;
add_task(function* () {

Просмотреть файл

@ -202,6 +202,7 @@ var testMouseInteraction = Task.async(function* () {
"Which is the unique column");
// popup should be open now
// clicking on second column label
let onPopupHidden = once(table.menupopup, "popuphidden");
event = table.once(TableWidget.EVENTS.HEADER_CONTEXT_MENU);
node = table.menupopup.querySelector("[data-id='col2']");
info("selecting to hide the second column");
@ -209,6 +210,7 @@ var testMouseInteraction = Task.async(function* () {
"Column is not hidden before hiding it");
click(node);
id = yield event;
yield onPopupHidden;
is(id, "col2", "Correct column was triggered to be hidden");
is(table.tbody.children[2].getAttribute("hidden"), "true",
"Column is hidden after hiding it");
@ -225,6 +227,7 @@ var testMouseInteraction = Task.async(function* () {
"Only 1 menuitem is disabled");
// popup should be open now
// clicking on second column label
onPopupHidden = once(table.menupopup, "popuphidden");
event = table.once(TableWidget.EVENTS.HEADER_CONTEXT_MENU);
node = table.menupopup.querySelector("[data-id='col3']");
info("selecting to hide the second column");
@ -232,6 +235,7 @@ var testMouseInteraction = Task.async(function* () {
"Column is not hidden before hiding it");
click(node);
id = yield event;
yield onPopupHidden;
is(id, "col3", "Correct column was triggered to be hidden");
is(table.tbody.children[4].getAttribute("hidden"), "true",
"Column is hidden after hiding it");
@ -256,6 +260,7 @@ var testMouseInteraction = Task.async(function* () {
// showing back 2nd column
// popup should be open now
// clicking on second column label
onPopupHidden = once(table.menupopup, "popuphidden");
event = table.once(TableWidget.EVENTS.HEADER_CONTEXT_MENU);
node = table.menupopup.querySelector("[data-id='col2']");
info("selecting to hide the second column");
@ -263,6 +268,7 @@ var testMouseInteraction = Task.async(function* () {
"Column is hidden before unhiding it");
click(node);
id = yield event;
yield onPopupHidden;
is(id, "col2", "Correct column was triggered to be hidden");
ok(!table.tbody.children[2].hasAttribute("hidden"),
"Column is not hidden after unhiding it");
@ -277,6 +283,7 @@ var testMouseInteraction = Task.async(function* () {
// popup should be open now
// clicking on second column label
onPopupHidden = once(table.menupopup, "popuphidden");
event = table.once(TableWidget.EVENTS.HEADER_CONTEXT_MENU);
node = table.menupopup.querySelector("[data-id='col3']");
info("selecting to hide the second column");
@ -284,6 +291,7 @@ var testMouseInteraction = Task.async(function* () {
"Column is hidden before unhiding it");
click(node);
id = yield event;
yield onPopupHidden;
is(id, "col3", "Correct column was triggered to be hidden");
ok(!table.tbody.children[4].hasAttribute("hidden"),
"Column is not hidden after unhiding it");

Просмотреть файл

@ -1,61 +1,55 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const TEST_URI = "data:text/html;charset=utf-8," +
"<p>browser_telemetry_button_eyedropper.js</p><div>test</div>";
var {EyedropperManager} = require("devtools/client/eyedropper/eyedropper");
add_task(function* () {
yield addTab(TEST_URI);
let Telemetry = loadTelemetryAndRecordLogs();
let target = TargetFactory.forTab(gBrowser.selectedTab);
let toolbox = yield gDevTools.showToolbox(target, "inspector");
info("inspector opened");
info("testing the eyedropper button");
yield testButton(toolbox, Telemetry);
stopRecordingTelemetryLogs(Telemetry);
yield gDevTools.closeToolbox(target);
gBrowser.removeCurrentTab();
});
function* testButton(toolbox, Telemetry) {
let button = toolbox.doc.querySelector("#command-button-eyedropper");
ok(button, "Captain, we have the eyedropper button");
let clicked = toolbox._requisition.commandOutputManager.onOutput.once();
info("clicking the button to open the eyedropper");
button.click();
yield clicked;
checkResults("_EYEDROPPER_", Telemetry);
}
function checkResults(histIdFocus, Telemetry) {
let result = Telemetry.prototype.telemetryInfo;
for (let [histId, value] of Iterator(result)) {
if (histId.startsWith("DEVTOOLS_INSPECTOR_") ||
!histId.includes(histIdFocus)) {
// Inspector stats are tested in
// browser_telemetry_toolboxtabs_{toolname}.js so we skip them here
// because we only open the inspector once for this test.
continue;
}
if (histId.endsWith("OPENED_PER_USER_FLAG")) {
ok(value.length === 1 && value[0] === true,
"Per user value " + histId + " has a single value of true");
} else if (histId.endsWith("OPENED_COUNT")) {
is(value.length, 1, histId + " has one entry");
let okay = value.every(element => element === true);
ok(okay, "All " + histId + " entries are === true");
}
}
}
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const TEST_URI = "data:text/html;charset=utf-8," +
"<p>browser_telemetry_button_eyedropper.js</p><div>test</div>";
add_task(function* () {
yield addTab(TEST_URI);
let Telemetry = loadTelemetryAndRecordLogs();
let target = TargetFactory.forTab(gBrowser.selectedTab);
let toolbox = yield gDevTools.showToolbox(target, "inspector");
info("inspector opened");
info("testing the eyedropper button");
yield testButton(toolbox, Telemetry);
stopRecordingTelemetryLogs(Telemetry);
yield gDevTools.closeToolbox(target);
gBrowser.removeCurrentTab();
});
function* testButton(toolbox, Telemetry) {
info("Calling the eyedropper button's callback");
// We call the button callback directly because we don't need to test the UI here, we're
// only concerned about testing the telemetry probe.
yield toolbox.getPanel("inspector").showEyeDropper();
checkResults("_EYEDROPPER_", Telemetry);
}
function checkResults(histIdFocus, Telemetry) {
let result = Telemetry.prototype.telemetryInfo;
for (let [histId, value] of Iterator(result)) {
if (histId.startsWith("DEVTOOLS_INSPECTOR_") ||
!histId.includes(histIdFocus)) {
// Inspector stats are tested in
// browser_telemetry_toolboxtabs_{toolname}.js so we skip them here
// because we only open the inspector once for this test.
continue;
}
if (histId.endsWith("OPENED_PER_USER_FLAG")) {
ok(value.length === 1 && value[0] === true,
"Per user value " + histId + " has a single value of true");
} else if (histId.endsWith("OPENED_COUNT")) {
is(value.length, 1, histId + " has one entry");
let okay = value.every(element => element === true);
ok(okay, "All " + histId + " entries are === true");
}
}
}

Просмотреть файл

@ -10,7 +10,7 @@ var Ci = Components.interfaces;
var Cc = Components.classes;
var {require, loader} = Cu.import("resource://devtools/shared/Loader.jsm", {});
const {colorUtils} = require("devtools/client/shared/css-color");
const {colorUtils} = require("devtools/shared/css-color");
loader.lazyGetter(this, "DOMUtils", function () {
return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);

Просмотреть файл

@ -13,8 +13,8 @@ var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
const DOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
const {colorUtils} = require("devtools/client/shared/css-color");
const {cssColors} = require("devtools/client/shared/css-color-db");
const {colorUtils} = require("devtools/shared/css-color");
const {cssColors} = require("devtools/shared/css-color-db");
function isValid(colorName) {
ok(colorUtils.isValidCSSColor(colorName),

Просмотреть файл

@ -12,11 +12,8 @@ const {CubicBezierWidget} =
const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
const {TooltipToggle} = require("devtools/client/shared/widgets/tooltip/TooltipToggle");
const EventEmitter = require("devtools/shared/event-emitter");
const {colorUtils} = require("devtools/client/shared/css-color");
const {colorUtils} = require("devtools/shared/css-color");
const Heritage = require("sdk/core/heritage");
const {Eyedropper} = require("devtools/client/eyedropper/eyedropper");
const {gDevTools} = require("devtools/client/framework/devtools");
const Services = require("Services");
const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
const {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip");
const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
@ -746,11 +743,15 @@ SwatchBasedEditorTooltip.prototype = {
*
* @param {Toolbox} toolbox
* The devtools toolbox, needed to get the devtools main window.
* @param {InspectorPanel} inspector
* The inspector panel, needed for the eyedropper.
*/
function SwatchColorPickerTooltip(toolbox) {
function SwatchColorPickerTooltip(toolbox, inspector) {
let stylesheet = "chrome://devtools/content/shared/widgets/spectrum.css";
SwatchBasedEditorTooltip.call(this, toolbox, stylesheet);
this.inspector = inspector;
// Creating a spectrum instance. this.spectrum will always be a promise that
// resolves to the spectrum instance
this.spectrum = this.setColorPickerContent([0, 0, 0, 1]);
@ -779,7 +780,7 @@ Heritage.extend(SwatchBasedEditorTooltip.prototype, {
eyedropper.className = "devtools-button";
container.appendChild(eyedropper);
this.tooltip.setContent(container, { width: 210, height: 216 });
this.tooltip.setContent(container, { width: 218, height: 224 });
let spectrum = new Spectrum(spectrumNode, color);
@ -810,8 +811,16 @@ Heritage.extend(SwatchBasedEditorTooltip.prototype, {
this.spectrum.updateUI();
}
let eyeButton = this.tooltip.doc.querySelector("#eyedropper-button");
eyeButton.addEventListener("click", this._openEyeDropper);
let {target} = this.inspector.toolbox;
target.actorHasMethod("inspector", "pickColorFromPage").then(value => {
let tooltipDoc = this.tooltip.doc;
let eyeButton = tooltipDoc.querySelector("#eyedropper-button");
if (value) {
eyeButton.addEventListener("click", this._openEyeDropper);
} else {
eyeButton.style.display = "none";
}
}, e => console.error(e));
},
_onSpectrumColorChange: function (event, rgba, cssColor) {
@ -834,39 +843,26 @@ Heritage.extend(SwatchBasedEditorTooltip.prototype, {
},
_openEyeDropper: function () {
let chromeWindow = this.tooltip.doc.defaultView.top;
let windowType = chromeWindow.document.documentElement
.getAttribute("windowtype");
let toolboxWindow;
if (windowType != gDevTools.chromeWindowType) {
// this means the toolbox is in a seperate window. We need to make
// sure we'll be inspecting the browser window instead
toolboxWindow = chromeWindow;
chromeWindow = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
chromeWindow.focus();
}
let dropper = new Eyedropper(chromeWindow, { copyOnSelect: false,
context: "picker" });
let {inspector, toolbox, telemetry} = this.inspector;
telemetry.toolOpened("pickereyedropper");
inspector.pickColorFromPage({copyOnSelect: false}).catch(e => console.error(e));
dropper.once("select", (event, color) => {
if (toolboxWindow) {
toolboxWindow.focus();
}
inspector.once("color-picked", color => {
toolbox.win.focus();
this._selectColor(color);
});
dropper.once("destroy", () => {
inspector.once("color-pick-canceled", () => {
this.eyedropperOpen = false;
this.activeSwatch = null;
});
dropper.open();
this.eyedropperOpen = true;
// close the colorpicker tooltip so that only the eyedropper is open.
this.hide();
this.tooltip.emit("eyedropper-opened", dropper);
this.tooltip.emit("eyedropper-opened");
},
_colorToRgba: function (color) {
@ -883,6 +879,7 @@ Heritage.extend(SwatchBasedEditorTooltip.prototype, {
destroy: function () {
SwatchBasedEditorTooltip.prototype.destroy.call(this);
this.inspector = null;
this.currentSwatchColor = null;
this.spectrum.off("changed", this._onSpectrumColorChange);
this.spectrum.destroy();

Просмотреть файл

@ -7,11 +7,12 @@
.cubic-bezier-container {
display: flex;
width: 500px;
width: 510px;
height: 370px;
flex-direction: row-reverse;
overflow: hidden;
padding: 5px;
box-sizing: border-box;
}
.display-wrap {
@ -30,11 +31,6 @@
position: relative;
}
.theme-dark .coordinate-plane:before,
.theme-dark .coordinate-plane:after {
border-color: #eee;
}
.control-point {
position: absolute;
z-index: 1;
@ -51,30 +47,36 @@
}
.display-wrap {
background: repeating-linear-gradient(0deg, transparent, rgba(0, 0, 0, 0.05) 0, rgba(0, 0, 0, 0.05) 1px, transparent 1px, transparent 15px) no-repeat, repeating-linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.05) 0, rgba(0, 0, 0, 0.05) 1px, transparent 1px, transparent 15px) no-repeat;
background:
repeating-linear-gradient(0deg,
transparent,
var(--bezier-grid-color) 0,
var(--bezier-grid-color) 1px,
transparent 1px,
transparent 15px) no-repeat,
repeating-linear-gradient(90deg,
transparent,
var(--bezier-grid-color) 0,
var(--bezier-grid-color) 1px,
transparent 1px,
transparent 15px) no-repeat;
background-size: 100% 100%, 100% 100%;
background-position: -2px 5px, -2px 5px;
-moz-user-select: none;
}
.theme-dark .display-wrap {
background: repeating-linear-gradient(0deg, transparent, rgba(0, 0, 0, 0.2) 0, rgba(0, 0, 0, 0.2) 1px, transparent 1px, transparent 15px) no-repeat, repeating-linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.2) 0, rgba(0, 0, 0, 0.2) 1px, transparent 1px, transparent 15px) no-repeat;
background-size: 100% 100%, 100% 100%;
background-position: -2px 5px, -2px 5px;
-moz-user-select: none;
}
canvas.curve {
background: linear-gradient(-45deg, transparent 49.7%, rgba(0,0,0,.2) 49.7%, rgba(0,0,0,.2) 50.3%, transparent 50.3%) center no-repeat;
background:
linear-gradient(-45deg,
transparent 49.7%,
var(--bezier-diagonal-color) 49.7%,
var(--bezier-diagonal-color) 50.3%,
transparent 50.3%) center no-repeat;
background-size: 100% 100%;
background-position: 0 0;
}
.theme-dark canvas.curve {
background: linear-gradient(-45deg, transparent 49.7%, #eee 49.7%, #eee 50.3%, transparent 50.3%) center no-repeat;
}
/* Timing Function Preview Widget */
.timing-function-preview {
@ -184,16 +186,12 @@ canvas.curve {
.preset canvas {
display: block;
border: 1px solid #ccc;
border: 1px solid var(--theme-splitter-color);
border-radius: 3px;
background-color: var(--theme-body-background);
margin: 0 auto;
}
.theme-dark .preset canvas {
border-color: #444e58;
}
.preset p {
font-size: 80%;
margin: 2px auto 0px auto;
@ -211,8 +209,8 @@ canvas.curve {
border-color: var(--theme-selection-background);
}
.active-preset canvas, .active-preset:hover canvas,
.theme-dark .active-preset canvas, .theme-dark .preset:hover canvas {
.active-preset canvas,
.active-preset:hover canvas {
background-color: var(--theme-selection-background-semitransparent);
border-color: var(--theme-selection-background);
}

Просмотреть файл

@ -5,10 +5,12 @@
/* Main container: Displays the filters and presets in 2 columns */
#filter-container {
height: 100%;
width: 510px;
height: 200px;
display: flex;
position: relative;
padding: 5px;
box-sizing: border-box;
/* when opened in a xul:panel, a gray color is applied to text */
color: var(--theme-body-color);
}
@ -138,12 +140,6 @@ input {
width: 8em;
}
.theme-light .add,
.theme-light .remove-button,
.theme-light #toggle-presets {
filter: invert(1);
}
.preset {
display: flex;
margin-bottom: 10px;
@ -174,10 +170,6 @@ input {
color: var(--theme-selection-color);
}
.theme-light .preset:hover .remove-button {
filter: invert(0);
}
.preset .remove-button {
order: 2;
}
@ -238,6 +230,12 @@ input {
background: url(chrome://devtools/skin/images/pseudo-class.svg);
}
.add,
.remove-button,
#toggle-presets {
filter: var(--icon-filter);
}
.show-presets #toggle-presets {
filter: url(chrome://devtools/skin/images/filters.svg#checked-icon-state);
}

Просмотреть файл

@ -8,7 +8,7 @@
}
#eyedropper-button::before {
background-image: url("chrome://devtools/skin/images/command-eyedropper.svg");
background-image: url(chrome://devtools/skin/images/command-eyedropper.svg);
}
/* Mix-in classes */
@ -37,6 +37,10 @@
/* Elements */
#spectrum-tooltip {
padding: 4px;
}
.spectrum-container {
position: relative;
display: none;

Просмотреть файл

@ -190,7 +190,15 @@ function removeBreakpoints(ctx) {
meta.breakpoints = {};
}
cm.doc.iter((line) => { removeBreakpoint(ctx, line); });
cm.doc.iter((line) => {
// The hasBreakpoint is a slow operation: checks the line type, whether cm
// is initialized and creates several new objects. Inlining the line's
// wrapClass property check directly.
if (line.wrapClass == null || !line.wrapClass.includes("breakpoint")) {
return;
}
removeBreakpoint(ctx, line);
});
}
/**

Просмотреть файл

@ -11,7 +11,22 @@
height: 100%;
}
#sidebar-panel-computedview > .devtools-toolbar {
#computedview-container {
overflow: auto;
height: 100%;
}
/* This extra wrapper only serves as a way to get the content of the view focusable.
So that when the user reaches it either via keyboard or mouse, we know that the view
is focused and therefore can handle shortcuts.
However, for accessibility reasons, tabindex is set to -1 to avoid having to tab
through it, and the outline is hidden. */
#computedview-container-focusable {
height: 100%;
outline: none;
}
#computedview-toolbar {
display: flex;
}
@ -32,17 +47,11 @@
align-items: center;
}
#computedview-container {
overflow: auto;
}
#propertyContainer {
-moz-user-select: text;
overflow-y: auto;
overflow-x: hidden;
flex: auto;
border-top-width: 1px;
border-top-style: dotted;
}
.row-striped {

Просмотреть файл

@ -3,6 +3,14 @@
* 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/. */
:root {
--eyedropper-image: url(images/command-eyedropper.svg);
}
.theme-firebug {
--eyedropper-image: url(images/firebug/command-eyedropper.svg);
}
/* Use flex layout for the Inspector toolbar. For now, it's done
specifically for the Inspector toolbar since general rule applied
on .devtools-toolbar breaks breadcrubs and also toolbars in other
@ -78,6 +86,17 @@
font: message-box;
}
/* Eyedropper toolbar button */
#inspector-eyedropper-toggle {
/* hidden by default, until we can check that the required highlighter exists */
display: none;
}
#inspector-eyedropper-toggle::before {
background-image: var(--eyedropper-image);
}
/* Add element toolbar button */
#inspector-element-add-button::before {
background-image: url("chrome://devtools/skin/images/add.svg");

Просмотреть файл

@ -2,6 +2,12 @@
* 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/ */
#layout-wrapper {
border-bottom-style: solid;
border-bottom-width: 1px;
border-color: var(--theme-splitter-color);
}
#layout-container {
/* The view will grow bigger as the window gets resized, until 400px */
max-width: 400px;

Просмотреть файл

@ -223,7 +223,6 @@
width: 16px;
height: 16px;
margin-inline-end: 4px;
cursor: pointer;
}
.security-state-insecure {
@ -269,7 +268,6 @@
margin: 0;
margin-inline-end: 3px;
-moz-user-select: none;
cursor: pointer;
}
.requests-menu-transferred {

Просмотреть файл

@ -82,6 +82,16 @@
height: 100%;
}
/* This extra wrapper only serves as a way to get the content of the view focusable.
So that when the user reaches it either via keyboard or mouse, we know that the view
is focused and therefore can handle shortcuts.
However, for accessibility reasons, tabindex is set to -1 to avoid having to tab
through it, and the outline is hidden. */
#ruleview-container-focusable {
height: 100%;
outline: none;
}
#ruleview-container.non-interactive {
pointer-events: none;
visibility: collapse;

Просмотреть файл

@ -17,7 +17,6 @@
--command-frames-image: url(images/command-frames.svg);
--command-splitconsole-image: url(images/command-console.svg);
--command-noautohide-image: url(images/command-noautohide.svg);
--command-eyedropper-image: url(images/command-eyedropper.svg);
--command-rulers-image: url(images/command-rulers.svg);
--command-measure-image: url(images/command-measure.svg);
}
@ -36,7 +35,6 @@
--command-frames-image: url(images/firebug/command-frames.svg);
--command-splitconsole-image: url(images/firebug/command-console.svg);
--command-noautohide-image: url(images/firebug/command-noautohide.svg);
--command-eyedropper-image: url(images/firebug/command-eyedropper.svg);
--command-rulers-image: url(images/firebug/command-rulers.svg);
--command-measure-image: url(images/firebug/command-measure.svg);
}

Просмотреть файл

@ -3,6 +3,18 @@
* 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/. */
/* Tooltip specific theme variables */
.theme-dark {
--bezier-diagonal-color: #eee;
--bezier-grid-color: rgba(0, 0, 0, 0.2);
}
.theme-light {
--bezier-diagonal-color: rgba(0, 0, 0, 0.2);
--bezier-grid-color: rgba(0, 0, 0, 0.05);
}
/* Tooltip widget (see devtools/client/shared/widgets/Tooltip.js) */
.devtools-tooltip .panel-arrowcontent {

Просмотреть файл

@ -400,3 +400,67 @@
stroke-dasharray: 5 3;
shape-rendering: crispEdges;
}
/* Eye dropper */
:-moz-native-anonymous .eye-dropper-root {
--magnifier-width: 96px;
--magnifier-height: 96px;
/* Width accounts for all color formats (hsl being the longest) */
--label-width: 160px;
--color: #e0e0e0;
position: absolute;
/* Tool start position. This should match the X/Y defines in JS */
top: 100px;
left: 100px;
/* Prevent interacting with the page when hovering and clicking */
pointer-events: auto;
/* Offset the UI so it is centered around the pointer */
transform: translate(
calc(var(--magnifier-width) / -2), calc(var(--magnifier-height) / -2));
filter: drop-shadow(0 0 1px rgba(0,0,0,.4));
/* We don't need the UI to be reversed in RTL locales, otherwise the # would appear
to the right of the hex code. Force LTR */
direction: ltr;
}
:-moz-native-anonymous .eye-dropper-canvas {
image-rendering: -moz-crisp-edges;
cursor: none;
width: var(--magnifier-width);
height: var(--magnifier-height);
border-radius: 50%;
box-shadow: 0 0 0 3px var(--color);
display: block;
}
:-moz-native-anonymous .eye-dropper-color-container {
background-color: var(--color);
border-radius: 2px;
width: var(--label-width);
transform: translateX(calc((var(--magnifier-width) - var(--label-width)) / 2));
position: relative;
}
:-moz-native-anonymous .eye-dropper-color-preview {
width: 16px;
height: 16px;
position: absolute;
offset-inline-start: 3px;
offset-block-start: 3px;
box-shadow: 0px 0px 0px black;
border: solid 1px #fff;
}
:-moz-native-anonymous .eye-dropper-color-value {
text-shadow: 1px 1px 1px #fff;
font: message-box;
font-size: 11px;
text-align: center;
padding: 4px 0;
}

Просмотреть файл

@ -679,3 +679,7 @@ exports.RulersHighlighter = RulersHighlighter;
const { MeasuringToolHighlighter } = require("./highlighters/measuring-tool");
register(MeasuringToolHighlighter);
exports.MeasuringToolHighlighter = MeasuringToolHighlighter;
const { EyeDropper } = require("./highlighters/eye-dropper");
register(EyeDropper);
exports.EyeDropper = EyeDropper;

Просмотреть файл

@ -0,0 +1,499 @@
/* 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";
// Eye-dropper tool. This is implemented as a highlighter so it can be displayed in the
// content page.
// It basically displays a magnifier that tracks mouse moves and shows a magnified version
// of the page. On click, it samples the color at the pixel being hovered.
const {Ci, Cc} = require("chrome");
const {CanvasFrameAnonymousContentHelper, createNode} = require("./utils/markup");
const Services = require("Services");
const EventEmitter = require("devtools/shared/event-emitter");
const {rgbToHsl, rgbToColorName} = require("devtools/shared/css-color").colorUtils;
const {getCurrentZoom, getFrameOffsets} = require("devtools/shared/layout/utils");
loader.lazyGetter(this, "clipboardHelper",
() => Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper));
loader.lazyGetter(this, "l10n",
() => Services.strings.createBundle("chrome://devtools/locale/eyedropper.properties"));
const ZOOM_LEVEL_PREF = "devtools.eyedropper.zoom";
const FORMAT_PREF = "devtools.defaultColorUnit";
// Width of the canvas.
const MAGNIFIER_WIDTH = 96;
// Height of the canvas.
const MAGNIFIER_HEIGHT = 96;
// Start position, when the tool is first shown. This should match the top/left position
// defined in CSS.
const DEFAULT_START_POS_X = 100;
const DEFAULT_START_POS_Y = 100;
// How long to wait before closing after copy.
const CLOSE_DELAY = 750;
/**
* The EyeDropper is the class that draws the gradient line and
* color stops as an overlay on top of a linear-gradient background-image.
*/
function EyeDropper(highlighterEnv) {
EventEmitter.decorate(this);
this.highlighterEnv = highlighterEnv;
this.markup = new CanvasFrameAnonymousContentHelper(this.highlighterEnv,
this._buildMarkup.bind(this));
// Get a couple of settings from prefs.
this.format = Services.prefs.getCharPref(FORMAT_PREF);
this.eyeDropperZoomLevel = Services.prefs.getIntPref(ZOOM_LEVEL_PREF);
}
EyeDropper.prototype = {
typeName: "EyeDropper",
ID_CLASS_PREFIX: "eye-dropper-",
get win() {
return this.highlighterEnv.window;
},
_buildMarkup() {
// Highlighter main container.
let container = createNode(this.win, {
attributes: {"class": "highlighter-container"}
});
// Wrapper element.
let wrapper = createNode(this.win, {
parent: container,
attributes: {
"id": "root",
"class": "root",
"hidden": "true"
},
prefix: this.ID_CLASS_PREFIX
});
// The magnifier canvas element.
createNode(this.win, {
parent: wrapper,
nodeType: "canvas",
attributes: {
"id": "canvas",
"class": "canvas",
"width": MAGNIFIER_WIDTH,
"height": MAGNIFIER_HEIGHT
},
prefix: this.ID_CLASS_PREFIX
});
// The color label element.
let colorLabelContainer = createNode(this.win, {
parent: wrapper,
attributes: {"class": "color-container"},
prefix: this.ID_CLASS_PREFIX
});
createNode(this.win, {
nodeType: "div",
parent: colorLabelContainer,
attributes: {"id": "color-preview", "class": "color-preview"},
prefix: this.ID_CLASS_PREFIX
});
createNode(this.win, {
nodeType: "div",
parent: colorLabelContainer,
attributes: {"id": "color-value", "class": "color-value"},
prefix: this.ID_CLASS_PREFIX
});
return container;
},
destroy() {
this.hide();
this.markup.destroy();
},
getElement(id) {
return this.markup.getElement(this.ID_CLASS_PREFIX + id);
},
/**
* Show the eye-dropper highlighter.
* @param {DOMNode} node The node which document the highlighter should be inserted in.
* @param {Object} options The options object may contain the following properties:
* - {Boolean} copyOnSelect Whether selecting a color should copy it to the clipboard.
*/
show(node, options = {}) {
this.options = options;
// Get the page's current zoom level.
this.pageZoom = getCurrentZoom(this.win);
// Take a screenshot of the viewport. This needs to be done first otherwise the
// eyedropper UI will appear in the screenshot itself (since the UI is injected as
// native anonymous content in the page).
// Once the screenshot is ready, the magnified area will be drawn.
this.prepareImageCapture();
// Start listening for user events.
let {pageListenerTarget} = this.highlighterEnv;
pageListenerTarget.addEventListener("mousemove", this);
pageListenerTarget.addEventListener("click", this);
pageListenerTarget.addEventListener("keydown", this);
pageListenerTarget.addEventListener("DOMMouseScroll", this);
pageListenerTarget.addEventListener("FullZoomChange", this);
// Show the eye-dropper.
this.getElement("root").removeAttribute("hidden");
// Prepare the canvas context on which we're drawing the magnified page portion.
this.ctx = this.getElement("canvas").getCanvasContext();
this.ctx.mozImageSmoothingEnabled = false;
this.magnifiedArea = {width: MAGNIFIER_WIDTH, height: MAGNIFIER_HEIGHT,
x: DEFAULT_START_POS_X, y: DEFAULT_START_POS_Y};
this.moveTo(DEFAULT_START_POS_X, DEFAULT_START_POS_Y);
// Focus the content so the keyboard can be used.
this.win.document.documentElement.focus();
return true;
},
/**
* Hide the eye-dropper highlighter.
*/
hide() {
this.pageImage = null;
let {pageListenerTarget} = this.highlighterEnv;
pageListenerTarget.removeEventListener("mousemove", this);
pageListenerTarget.removeEventListener("click", this);
pageListenerTarget.removeEventListener("keydown", this);
pageListenerTarget.removeEventListener("DOMMouseScroll", this);
pageListenerTarget.removeEventListener("FullZoomChange", this);
this.getElement("root").setAttribute("hidden", "true");
this.getElement("root").removeAttribute("drawn");
},
prepareImageCapture() {
// Get the page as an image.
let imageData = getWindowAsImageData(this.win);
let image = new this.win.Image();
image.src = imageData;
// Wait for screenshot to load
image.onload = () => {
this.pageImage = image;
// We likely haven't drawn anything yet (no mousemove events yet), so start now.
this.draw();
// Set an attribute on the root element to be able to run tests after the first draw
// was done.
this.getElement("root").setAttribute("drawn", "true");
};
},
/**
* Get the number of cells (blown-up pixels) per direction in the grid.
*/
get cellsWide() {
// Canvas will render whole "pixels" (cells) only, and an even number at that. Round
// up to the nearest even number of pixels.
let cellsWide = Math.ceil(this.magnifiedArea.width / this.eyeDropperZoomLevel);
cellsWide += cellsWide % 2;
return cellsWide;
},
/**
* Get the size of each cell (blown-up pixel) in the grid.
*/
get cellSize() {
return this.magnifiedArea.width / this.cellsWide;
},
/**
* Get index of cell in the center of the grid.
*/
get centerCell() {
return Math.floor(this.cellsWide / 2);
},
/**
* Get color of center cell in the grid.
*/
get centerColor() {
let pos = (this.centerCell * this.cellSize) + (this.cellSize / 2);
let rgb = this.ctx.getImageData(pos, pos, 1, 1).data;
return rgb;
},
draw() {
// If the image of the page isn't ready yet, bail out, we'll draw later on mousemove.
if (!this.pageImage) {
return;
}
let {width, height, x, y} = this.magnifiedArea;
let zoomedWidth = width / this.eyeDropperZoomLevel;
let zoomedHeight = height / this.eyeDropperZoomLevel;
let sx = x - (zoomedWidth / 2);
let sy = y - (zoomedHeight / 2);
let sw = zoomedWidth;
let sh = zoomedHeight;
this.ctx.drawImage(this.pageImage, sx, sy, sw, sh, 0, 0, width, height);
// Draw the grid on top, but only at 3x or more, otherwise it's too busy.
if (this.eyeDropperZoomLevel > 2) {
this.drawGrid();
}
this.drawCrosshair();
// Update the color preview and value.
let rgb = this.centerColor;
this.getElement("color-preview").setAttribute("style",
`background-color:${toColorString(rgb, "rgb")};`);
this.getElement("color-value").setTextContent(toColorString(rgb, this.format));
},
/**
* Draw a grid on the canvas representing pixel boundaries.
*/
drawGrid() {
let {width, height} = this.magnifiedArea;
this.ctx.lineWidth = 1;
this.ctx.strokeStyle = "rgba(143, 143, 143, 0.2)";
for (let i = 0; i < width; i += this.cellSize) {
this.ctx.beginPath();
this.ctx.moveTo(i - .5, 0);
this.ctx.lineTo(i - .5, height);
this.ctx.stroke();
this.ctx.beginPath();
this.ctx.moveTo(0, i - .5);
this.ctx.lineTo(width, i - .5);
this.ctx.stroke();
}
},
/**
* Draw a box on the canvas to highlight the center cell.
*/
drawCrosshair() {
let pos = this.centerCell * this.cellSize;
this.ctx.lineWidth = 1;
this.ctx.lineJoin = "miter";
this.ctx.strokeStyle = "rgba(0, 0, 0, 1)";
this.ctx.strokeRect(pos - 1.5, pos - 1.5, this.cellSize + 2, this.cellSize + 2);
this.ctx.strokeStyle = "rgba(255, 255, 255, 1)";
this.ctx.strokeRect(pos - 0.5, pos - 0.5, this.cellSize, this.cellSize);
},
handleEvent(e) {
switch (e.type) {
case "mousemove":
// We might be getting an event from a child frame, so account for the offset.
let [xOffset, yOffset] = getFrameOffsets(this.win, e.target);
let x = xOffset + e.pageX - this.win.scrollX;
let y = yOffset + e.pageY - this.win.scrollY;
// Update the zoom area.
this.magnifiedArea.x = x * this.pageZoom;
this.magnifiedArea.y = y * this.pageZoom;
// Redraw the portion of the screenshot that is now under the mouse.
this.draw();
// And move the eye-dropper's UI so it follows the mouse.
this.moveTo(x, y);
break;
case "click":
this.selectColor();
break;
case "keydown":
this.handleKeyDown(e);
break;
case "DOMMouseScroll":
// Prevent scrolling. That's because we only took a screenshot of the viewport, so
// scrolling out of the viewport wouldn't draw the expected things. In the future
// we can take the screenshot again on scroll, but for now it doesn't seem
// important.
e.preventDefault();
break;
case "FullZoomChange":
this.hide();
this.show();
break;
}
},
moveTo(x, y) {
this.getElement("root").setAttribute("style", `top:${y}px;left:${x}px;`);
},
/**
* Select the current color that's being previewed. Depending on the current options,
* selecting might mean copying to the clipboard and closing the
*/
selectColor() {
let onColorSelected = Promise.resolve();
if (this.options.copyOnSelect) {
onColorSelected = this.copyColor();
}
this.emit("selected", toColorString(this.centerColor, this.format));
onColorSelected.then(() => this.hide(), e => console.error(e));
},
/**
* Handler for the keydown event. Either select the color or move the panel in a
* direction depending on the key pressed.
*/
handleKeyDown(e) {
if (e.keyCode === e.DOM_VK_RETURN) {
this.selectColor();
return;
}
if (e.keyCode === e.DOM_VK_ESCAPE) {
this.emit("canceled");
this.hide();
return;
}
let offsetX = 0;
let offsetY = 0;
let modifier = 1;
if (e.keyCode === e.DOM_VK_LEFT) {
offsetX = -1;
}
if (e.keyCode === e.DOM_VK_RIGHT) {
offsetX = 1;
}
if (e.keyCode === e.DOM_VK_UP) {
offsetY = -1;
}
if (e.keyCode === e.DOM_VK_DOWN) {
offsetY = 1;
}
if (e.shiftKey) {
modifier = 10;
}
offsetY *= modifier;
offsetX *= modifier;
if (offsetX !== 0 || offsetY !== 0) {
this.magnifiedArea.x += offsetX;
this.magnifiedArea.y += offsetY;
this.draw();
this.moveTo(this.magnifiedArea.x / this.pageZoom,
this.magnifiedArea.y / this.pageZoom);
}
// Prevent all keyboard interaction with the page, except if a modifier is used to let
// keyboard shortcuts through.
let hasModifier = e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
if (!hasModifier) {
e.preventDefault();
}
},
/**
* Copy the currently inspected color to the clipboard.
* @return {Promise} Resolves when the copy has been done (after a delay that is used to
* let users know that something was copied).
*/
copyColor() {
// Copy to the clipboard.
let color = toColorString(this.centerColor, this.format);
clipboardHelper.copyString(color);
// Provide some feedback.
this.getElement("color-value").setTextContent(
"✓ " + l10n.GetStringFromName("colorValue.copied"));
// Hide the tool after a delay.
clearTimeout(this._copyTimeout);
return new Promise(resolve => {
this._copyTimeout = setTimeout(resolve, CLOSE_DELAY);
});
}
};
exports.EyeDropper = EyeDropper;
/**
* Get a content window as image data-url.
* @param {Window} win
* @return {String} The data-url
*/
function getWindowAsImageData(win) {
let canvas = win.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
let scale = getCurrentZoom(win);
let width = win.innerWidth;
let height = win.innerHeight;
canvas.width = width * scale;
canvas.height = height * scale;
canvas.mozOpaque = true;
let ctx = canvas.getContext("2d");
ctx.scale(scale, scale);
ctx.drawWindow(win, win.scrollX, win.scrollY, width, height, "#fff");
return canvas.toDataURL();
}
/**
* Get a formatted CSS color string from a color value.
* @param {array} rgb Rgb values of a color to format.
* @param {string} format Format of string. One of "hex", "rgb", "hsl", "name".
* @return {string} Formatted color value, e.g. "#FFF" or "hsl(20, 10%, 10%)".
*/
function toColorString(rgb, format) {
let [r, g, b] = rgb;
switch (format) {
case "hex":
return hexString(rgb);
case "rgb":
return "rgb(" + r + ", " + g + ", " + b + ")";
case "hsl":
let [h, s, l] = rgbToHsl(rgb);
return "hsl(" + h + ", " + s + "%, " + l + "%)";
case "name":
let str;
try {
str = rgbToColorName(r, g, b);
} catch (e) {
str = hexString(rgb);
}
return str;
default:
return hexString(rgb);
}
}
/**
* Produce a hex-formatted color string from rgb values.
* @param {array} rgb Rgb values of color to stringify.
* @return {string} Hex formatted string for color, e.g. "#FFEE00".
*/
function hexString([r, g, b]) {
let val = (1 << 24) + (r << 16) + (g << 8) + (b << 0);
return "#" + val.toString(16).substr(-6).toUpperCase();
}

Просмотреть файл

@ -12,6 +12,7 @@ DevToolsModules(
'auto-refresh.js',
'box-model.js',
'css-transform.js',
'eye-dropper.js',
'geometry-editor.js',
'measuring-tool.js',
'rect.js',

Просмотреть файл

@ -325,6 +325,10 @@ CanvasFrameAnonymousContentHelper.prototype = {
return typeof this.getAttributeForElement(id, name) === "string";
},
getCanvasContext: function (id, type = "2d") {
return this.content ? this.content.getCanvasContext(id, type) : null;
},
/**
* Add an event listener to one of the elements inserted in the canvasFrame
* native anonymous container.
@ -460,6 +464,7 @@ CanvasFrameAnonymousContentHelper.prototype = {
getAttribute: name => this.getAttributeForElement(id, name),
removeAttribute: name => this.removeAttributeForElement(id, name),
hasAttribute: name => this.hasAttributeForElement(id, name),
getCanvasContext: type => this.getCanvasContext(id, type),
addEventListener: (type, handler) => {
return this.addEventListenerForElement(id, type, handler);
},

Просмотреть файл

@ -53,20 +53,19 @@
const {Cc, Ci, Cu} = require("chrome");
const Services = require("Services");
const protocol = require("devtools/shared/protocol");
const {Arg, Option, method, RetVal, types} = protocol;
const {LongStringActor} = require("devtools/server/actors/string");
const promise = require("promise");
const {Task} = require("devtools/shared/task");
const object = require("sdk/util/object");
const events = require("sdk/event/core");
const {Class} = require("sdk/core/heritage");
const {WalkerSearch} = require("devtools/server/actors/utils/walker-search");
const {PageStyleActor, getFontPreviewData} = require("devtools/server/actors/styles");
const {
HighlighterActor,
CustomHighlighterActor,
isTypeRegistered,
HighlighterEnvironment
} = require("devtools/server/actors/highlighters");
const {EyeDropper} = require("devtools/server/actors/highlighters/eye-dropper");
const {
isAnonymous,
isNativeAnonymous,
@ -74,8 +73,7 @@ const {
isShadowAnonymous,
getFrameElement
} = require("devtools/shared/layout/utils");
const {getLayoutChangesObserver, releaseLayoutChangesObserver} =
require("devtools/server/actors/layout");
const {getLayoutChangesObserver, releaseLayoutChangesObserver} = require("devtools/server/actors/layout");
const nodeFilterConstants = require("devtools/shared/dom-node-filter-constants");
loader.lazyRequireGetter(this, "CSS", "CSS");
@ -442,7 +440,7 @@ var NodeActor = exports.NodeActor = protocol.ActorClassWithSpec(nodeSpec, {
getEventListeners: function (node) {
let parsers = this._eventParsers;
let dbg = this.parent().tabActor.makeDebugger();
let events = [];
let listeners = [];
for (let [, {getListeners, normalizeHandler}] of parsers) {
try {
@ -457,7 +455,7 @@ var NodeActor = exports.NodeActor = protocol.ActorClassWithSpec(nodeSpec, {
eventInfo.normalizeHandler = normalizeHandler;
}
this.processHandlerForEvent(node, events, dbg, eventInfo);
this.processHandlerForEvent(node, listeners, dbg, eventInfo);
}
} catch (e) {
// An object attached to the node looked like a listener but wasn't...
@ -465,11 +463,11 @@ var NodeActor = exports.NodeActor = protocol.ActorClassWithSpec(nodeSpec, {
}
}
events.sort((a, b) => {
listeners.sort((a, b) => {
return a.type.localeCompare(b.type);
});
return events;
return listeners;
},
/**
@ -501,7 +499,7 @@ var NodeActor = exports.NodeActor = protocol.ActorClassWithSpec(nodeSpec, {
* }
* }
*/
processHandlerForEvent: function (node, events, dbg, eventInfo) {
processHandlerForEvent: function (node, listeners, dbg, eventInfo) {
let type = eventInfo.type || "";
let handler = eventInfo.handler;
let tags = eventInfo.tags || "";
@ -592,7 +590,7 @@ var NodeActor = exports.NodeActor = protocol.ActorClassWithSpec(nodeSpec, {
hide: hide
};
events.push(eventObj);
listeners.push(eventObj);
dbg.removeDebuggee(globalDO);
},
@ -1298,8 +1296,8 @@ var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, {
// We're going to create a few document walkers with the same filter,
// make it easier.
let getFilteredWalker = node => {
return this.getDocumentWalker(node, options.whatToShow);
let getFilteredWalker = documentWalkerNode => {
return this.getDocumentWalker(documentWalkerNode, options.whatToShow);
};
// Need to know the first and last child.
@ -2107,7 +2105,7 @@ var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, {
if (isNodeDead(node) ||
isNodeDead(parent) ||
(sibling && isNodeDead(sibling))) {
return null;
return;
}
let rawNode = node.rawNode;
@ -2122,7 +2120,7 @@ var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, {
null;
if (rawNode === rawSibling || currentNextSibling === rawSibling) {
return null;
return;
}
}
@ -2150,8 +2148,7 @@ var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, {
} catch (x) {
// Failed to create a new element with that tag name, ignore the change,
// and signal the error to the front.
return Promise.reject(new Error("Could not change node's tagName to " +
tagName));
return Promise.reject(new Error("Could not change node's tagName to " + tagName));
}
let attrs = oldNode.attributes;
@ -2166,6 +2163,7 @@ var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, {
}
oldNode.remove();
return null;
},
/**
@ -2580,15 +2578,21 @@ var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, {
* Server side of the inspector actor, which is used to create
* inspector-related actors, including the walker.
*/
var InspectorActor = exports.InspectorActor = protocol.ActorClassWithSpec(inspectorSpec, {
exports.InspectorActor = protocol.ActorClassWithSpec(inspectorSpec, {
initialize: function (conn, tabActor) {
protocol.Actor.prototype.initialize.call(this, conn);
this.tabActor = tabActor;
this._onColorPicked = this._onColorPicked.bind(this);
this._onColorPickCanceled = this._onColorPickCanceled.bind(this);
this.destroyEyeDropper = this.destroyEyeDropper.bind(this);
},
destroy: function () {
protocol.Actor.prototype.destroy.call(this);
this.destroyEyeDropper();
this._highlighterPromise = null;
this._pageStylePromise = null;
this._walkerPromise = null;
@ -2736,6 +2740,66 @@ var InspectorActor = exports.InspectorActor = protocol.ActorClassWithSpec(inspec
let baseURI = Services.io.newURI(document.location.href, null, null);
return Services.io.newURI(url, null, baseURI).spec;
},
/**
* Create an instance of the eye-dropper highlighter and store it on this._eyeDropper.
* Note that for now, a new instance is created every time to deal with page navigation.
*/
createEyeDropper: function () {
this.destroyEyeDropper();
this._highlighterEnv = new HighlighterEnvironment();
this._highlighterEnv.initFromTabActor(this.tabActor);
this._eyeDropper = new EyeDropper(this._highlighterEnv);
},
/**
* Destroy the current eye-dropper highlighter instance.
*/
destroyEyeDropper: function () {
if (this._eyeDropper) {
this.cancelPickColorFromPage();
this._eyeDropper.destroy();
this._eyeDropper = null;
this._highlighterEnv.destroy();
this._highlighterEnv = null;
}
},
/**
* Pick a color from the page using the eye-dropper. This method doesn't return anything
* but will cause events to be sent to the front when a color is picked or when the user
* cancels the picker.
* @param {Object} options
*/
pickColorFromPage: function (options) {
this.createEyeDropper();
this._eyeDropper.show(this.window.document.documentElement, options);
this._eyeDropper.once("selected", this._onColorPicked);
this._eyeDropper.once("canceled", this._onColorPickCanceled);
events.once(this.tabActor, "will-navigate", this.destroyEyeDropper);
},
/**
* After the pickColorFromPage method is called, the only way to dismiss the eye-dropper
* highlighter is for the user to click in the page and select a color. If you need to
* dismiss the eye-dropper programatically instead, use this method.
*/
cancelPickColorFromPage: function () {
if (this._eyeDropper) {
this._eyeDropper.hide();
this._eyeDropper.off("selected", this._onColorPicked);
this._eyeDropper.off("canceled", this._onColorPickCanceled);
events.off(this.tabActor, "will-navigate", this.destroyEyeDropper);
}
},
_onColorPicked: function (e, color) {
events.emit(this, "color-picked", color);
},
_onColorPickCanceled: function () {
events.emit(this, "color-pick-canceled");
}
});
@ -2757,6 +2821,7 @@ function nodeDocshell(node) {
return win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDocShell);
}
return null;
}
function isNodeDead(node) {
@ -2962,7 +3027,7 @@ function ensureImageLoaded(image, timeout) {
});
// Don't timeout when testing. This is never settled.
let onAbort = new promise(() => {});
let onAbort = new Promise(() => {});
if (!DevToolsUtils.testing) {
// Tests are not running. Reject the promise after given timeout.

Просмотреть файл

@ -11,6 +11,7 @@ support-files =
inspector_css-properties.html
inspector_getImageData.html
inspector-delay-image-response.sjs
inspector-eyedropper.html
inspector-helpers.js
inspector-search-data.html
inspector-styles-data.css
@ -75,6 +76,7 @@ skip-if = buildapp == 'mulet'
[test_inspector-mutations-childlist.html]
[test_inspector-mutations-frameload.html]
[test_inspector-mutations-value.html]
[test_inspector-pick-color.html]
[test_inspector-pseudoclass-lock.html]
[test_inspector-release.html]
[test_inspector-reload.html]

Просмотреть файл

@ -0,0 +1,18 @@
<html>
<head>
<meta charset="UTF-8">
<title>Inspector Eyedropper tests</title>
<style>
html {
background: black;
}
</style>
<script type="text/javascript">
window.onload = function() {
window.opener.postMessage('ready', '*');
};
</script>
</head>
</body>
</body>
</html>

Просмотреть файл

@ -0,0 +1,101 @@
<!DOCTYPE HTML>
<html>
<!--
Test that the inspector actor has the pickColorFromPage and cancelPickColorFromPage
methods and that when a color is picked the color-picked event is emitted and that when
the eyedropper is dimissed, the color-pick-canceled event is emitted.
https://bugzilla.mozilla.org/show_bug.cgi?id=1262439
-->
<head>
<meta charset="utf-8">
<title>Test for Bug 1262439</title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript;version=1.8" src="inspector-helpers.js"></script>
<script type="application/javascript;version=1.8">
window.onload = function() {
const Cu = Components.utils;
Cu.import("resource://devtools/shared/Loader.jsm");
const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
const {InspectorFront} = devtools.require("devtools/shared/fronts/inspector");
const {console} = Cu.import("resource://gre/modules/Console.jsm", {});
SimpleTest.waitForExplicitFinish();
let win = null;
let inspector = null;
addAsyncTest(function*() {
info("Setting up inspector actor");
let url = document.getElementById("inspectorContent").href;
yield new Promise(resolve => {
attachURL(url, function(err, client, tab, doc) {
win = doc.defaultView;
inspector = InspectorFront(client, tab);
resolve();
});
});
runNextTest();
});
addAsyncTest(function*() {
info("Start picking a color from the page");
yield inspector.pickColorFromPage();
info("Click in the page and make sure a color-picked event is received");
let onColorPicked = waitForEvent("color-picked");
win.document.body.click();
let color = yield onColorPicked;
is(color, "#000000", "The color-picked event was received with the right color");
runNextTest();
});
addAsyncTest(function*() {
info("Start picking a color from the page");
yield inspector.pickColorFromPage();
info("Use the escape key to dismiss the eyedropper");
let onPickCanceled = waitForEvent("color-pick-canceled");
let keyboardEvent = win.document.createEvent("KeyboardEvent");
keyboardEvent.initKeyEvent("keydown", true, true, win, false, false,
false, false, 27, 0);
win.document.dispatchEvent(keyboardEvent);
yield onPickCanceled;
ok(true, "The color-pick-canceled event was received");
runNextTest();
});
addAsyncTest(function*() {
info("Start picking a color from the page");
yield inspector.pickColorFromPage();
info("And cancel the color picking");
yield inspector.cancelPickColorFromPage();
runNextTest();
});
function waitForEvent(name) {
return new Promise(resolve => inspector.once(name, resolve));
}
runNextTest();
};
</script>
</head>
<body>
<a id="inspectorContent" target="_blank" href="inspector-eyedropper.html">Test Document</a>
<p id="display"></p>
<div id="content" style="display: none"></div>
<pre id="test">
</pre>
</body>
</html>

Просмотреть файл

Просмотреть файл

@ -7,7 +7,7 @@
const Services = require("Services");
const {getCSSLexer} = require("devtools/shared/css-lexer");
const {cssColors} = require("devtools/client/shared/css-color-db");
const {cssColors} = require("devtools/shared/css-color-db");
const COLOR_UNIT_PREF = "devtools.defaultColorUnit";

Просмотреть файл

@ -166,6 +166,7 @@ function getFrameOffsets(boundaryWindow, node) {
return [xOffset * scale, yOffset * scale];
}
exports.getFrameOffsets = getFrameOffsets;
/**
* Get box quads adjusted for iframes and zoom level.

Просмотреть файл

@ -310,10 +310,6 @@ eyedropperDesc=Grab a color from the page
# command, displayed when the user asks for help on what it does.
eyedropperManual=Open a panel that magnifies an area of page to inspect pixels and copy color values
# LOCALIZATION NOTE (eyedropperTooltip) A string displayed as the
# tooltip of button in devtools toolbox which toggles the Eyedropper tool.
eyedropperTooltip=Grab a color from the page
# LOCALIZATION NOTE (debuggerClosed) Used in the output of several commands
# to explain that the debugger must be opened first.
debuggerClosed=The debugger must be opened before using this command

Просмотреть файл

@ -42,6 +42,8 @@ DevToolsModules(
'async-utils.js',
'builtin-modules.js',
'content-observer.js',
'css-color-db.js',
'css-color.js',
'css-lexer.js',
'css-parsing-utils.js',
'css-properties-db.js',

Просмотреть файл

@ -371,6 +371,16 @@ exports.walkerSpec = walkerSpec;
const inspectorSpec = generateActorSpec({
typeName: "inspector",
events: {
"color-picked": {
type: "colorPicked",
color: Arg(0, "string")
},
"color-pick-canceled": {
type: "colorPickCanceled"
}
},
methods: {
getWalker: {
request: {
@ -409,6 +419,14 @@ const inspectorSpec = generateActorSpec({
resolveRelativeURL: {
request: {url: Arg(0, "string"), node: Arg(1, "nullable:domnode")},
response: {value: RetVal("string")}
},
pickColorFromPage: {
request: {options: Arg(0, "nullable:json")},
response: {}
},
cancelPickColorFromPage: {
request: {},
response: {}
}
}
});

Просмотреть файл

@ -45,24 +45,38 @@ android {
}
productFlavors {
// For fast local development. If you have an API 21 device, you should
// use this.
// For API 21+ - with multi dex, this will be faster for local development.
local {
// Setting `minSdkVersion 21` allows the Android gradle plugin to
// For multi dex, setting `minSdkVersion 21` allows the Android gradle plugin to
// pre-DEX each module and produce an APK that can be tested on
// Android Lollipop without time consuming DEX merging processes.
minSdkVersion 21
dexOptions {
preDexLibraries true
// We only call `MultiDex.install()` for the automation build flavor
// so this may not work. However, I don't think the multidex support
// library is necessary for 21+, so I expect that it will work.
multiDexEnabled true
}
}
// For local development on older devices. Use this only if you only
// have a pre-API 21 device, or if you want to test API-specific things.
// For API < 21 - does not support multi dex because local development
// is slow in that case. Most builds will not require multi dex so this
// should not be an issue.
localOld {
}
// Automation builds.
automation {
dexOptions {
// As of FF48 on beta, the "test", "lint", etc. treeherder jobs fail because they
// exceed the method limit. Beta includes Adjust and its GPS dependencies, which
// increase the method count & explain the failures. Furthermore, this error only
// occurs on debug builds because we don't proguard.
//
// We enable multidex as an easy, quick-fix with minimal side effects but before we
// move to gradle for our production builds, we should re-evaluate this decision
// (bug 1286677).
multiDexEnabled true
}
}
}
@ -171,6 +185,8 @@ android {
}
dependencies {
compile 'com.android.support:multidex:1.0.0'
compile "com.android.support:support-v4:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
compile "com.android.support:appcompat-v7:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
compile "com.android.support:cardview-v7:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"

Просмотреть файл

@ -920,3 +920,7 @@ pref("dom.presentation.discovery.legacy.enabled", true); // for TV 2.5 backward
pref("dom.audiochannel.audioCompeting", true);
pref("dom.audiochannel.mediaControl", true);
// Space separated list of URLS that are allowed to send objects (instead of
// only strings) through webchannels. This list is duplicated in browser/app/profile/firefox.js
pref("webchannel.allowObject.urlWhitelist", "https://accounts.firefox.com https://content.cdn.mozilla.net https://hello.firefox.com https://input.mozilla.org https://support.mozilla.org https://install.mozilla.org");

Просмотреть файл

@ -279,6 +279,10 @@
android:authorities="@ANDROID_PACKAGE_NAME@.db.browser"
android:exported="false"/>
<provider android:name="org.mozilla.gecko.distribution.PartnerBookmarksProviderProxy"
android:authorities="@ANDROID_PACKAGE_NAME@.partnerbookmarks"
android:exported="false"/>
<!-- Share overlay activity
Setting launchMode="singleTop" ensures onNewIntent is called when the Activity is

Просмотреть файл

@ -6,7 +6,11 @@
package org.mozilla.gecko;
import android.content.Context;
import android.os.Build;
//#ifdef MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE
import android.support.multidex.MultiDex;
//#endif
/**
* A collection of constants that pertain to the build and runtime state of the
@ -343,4 +347,21 @@ public class AppConstants {
// (bug 1266820) Temporarily disabled since no one is working on it.
public static final boolean SCREENSHOTS_IN_BOOKMARKS_ENABLED = false;
/**
* Enables multidex depending on build flags. For more information,
* see `multiDexEnabled true` in mobile/android/app/build.gradle.
*
* As a method, this shouldn't be in AppConstants, but it's
* the only semi-relevant Java file that we pre-process.
*/
public static void maybeInstallMultiDex(final Context context) {
//#ifdef MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE
if (BuildConfig.FLAVOR.equals("automation")) {
MultiDex.install(context);
}
//#else
// Do nothing.
//#endif
}
}

Просмотреть файл

@ -138,6 +138,12 @@ public class GeckoApplication extends Application
mInBackground = false;
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
AppConstants.maybeInstallMultiDex(base);
}
@Override
public void onCreate() {
Log.i(LOG_TAG, "zerdatime " + SystemClock.uptimeMillis() + " - Fennec application start");

Просмотреть файл

@ -1,71 +0,0 @@
/* -*- 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.distribution;
import android.content.ContentResolver;
import android.database.Cursor;
import android.net.Uri;
import org.mozilla.gecko.db.BrowserContract;
/**
* Client for reading Android's PartnerBookmarksProvider.
*
* Note: This client is only invoked for distributions. Without a distribution the content provider
* will not be read and no bookmarks will be added to the UI.
*/
public class PartnerBookmarksProviderClient {
/**
* The contract between the partner bookmarks provider and applications. Contains the definition
* for the supported URIs and columns.
*/
private static class PartnerContract {
public static final Uri CONTENT_URI = Uri.parse("content://com.android.partnerbookmarks/bookmarks");
public static final int TYPE_BOOKMARK = 1;
public static final int TYPE_FOLDER = 2;
public static final int PARENT_ROOT_ID = 0;
public static final String ID = "_id";
public static final String TYPE = "type";
public static final String URL = "url";
public static final String TITLE = "title";
public static final String FAVICON = "favicon";
public static final String TOUCHICON = "touchicon";
public static final String PARENT = "parent";
}
public static Cursor getBookmarksInFolder(ContentResolver contentResolver, int folderId) {
// Use root folder id or transform negative id into actual (positive) folder id.
final long actualFolderId = folderId == BrowserContract.Bookmarks.FIXED_ROOT_ID
? PartnerContract.PARENT_ROOT_ID
: BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START - folderId;
return contentResolver.query(
PartnerContract.CONTENT_URI,
new String[] {
// Transform ids into negative values starting with FAKE_PARTNER_BOOKMARKS_START.
"(" + BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START + " - " + PartnerContract.ID + ") as " + BrowserContract.Bookmarks._ID,
PartnerContract.TITLE + " as " + BrowserContract.Bookmarks.TITLE,
PartnerContract.URL + " as " + BrowserContract.Bookmarks.URL,
// Transform parent ids to negative ids as well
"(" + BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START + " - " + PartnerContract.PARENT + ") as " + BrowserContract.Bookmarks.PARENT,
// Convert types (we use 0-1 and the partner provider 1-2)
"(2 - " + PartnerContract.TYPE + ") as " + BrowserContract.Bookmarks.TYPE,
// Use the ID of the entry as GUID
PartnerContract.ID + " as " + BrowserContract.Bookmarks.GUID
},
PartnerContract.PARENT + " = ?"
// Only select entries with valid type
+ " AND (" + BrowserContract.Bookmarks.TYPE + " = 1 OR " + BrowserContract.Bookmarks.TYPE + " = 2)"
// Only select entries with non empty title
+ " AND " + BrowserContract.Bookmarks.TITLE + " <> ''",
new String[] { String.valueOf(actualFolderId) },
// Same order we use in our content provider (without position)
BrowserContract.Bookmarks.TYPE + " ASC, " + BrowserContract.Bookmarks._ID + " ASC");
}
}

Просмотреть файл

@ -0,0 +1,322 @@
/* -*- 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.distribution;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.CursorWrapper;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.db.BrowserContract;
import java.util.HashSet;
import java.util.Set;
/**
* A proxy for the partner bookmarks provider. Bookmark and folder ids of the partner bookmarks providers
* will be transformed so that they do not overlap with the ids from the local database.
*
* Bookmarks in folder:
* content://{PACKAGE_ID}.partnerbookmarks/bookmarks/{folderId}
* Icon of bookmark:
* content://{PACKAGE_ID}.partnerbookmarks/icons/{bookmarkId}
*/
public class PartnerBookmarksProviderProxy extends ContentProvider {
/**
* The contract between the partner bookmarks provider and applications. Contains the definition
* for the supported URIs and columns.
*/
public static class PartnerContract {
public static final Uri CONTENT_URI = Uri.parse("content://com.android.partnerbookmarks/bookmarks");
public static final int TYPE_BOOKMARK = 1;
public static final int TYPE_FOLDER = 2;
public static final int PARENT_ROOT_ID = 0;
public static final String ID = "_id";
public static final String TYPE = "type";
public static final String URL = "url";
public static final String TITLE = "title";
public static final String FAVICON = "favicon";
public static final String TOUCHICON = "touchicon";
public static final String PARENT = "parent";
}
private static final String AUTHORITY_PREFIX = ".partnerbookmarks";
private static final int URI_MATCH_BOOKMARKS = 1000;
private static final int URI_MATCH_ICON = 1001;
private static final int URI_MATCH_BOOKMARK = 1002;
private static final String PREF_DELETED_PARTNER_BOOKMARKS = "distribution.partner.bookmark.deleted";
/**
* Cursor wrapper for filtering empty folders.
*/
private static class FilteredCursor extends CursorWrapper {
private HashSet<Integer> emptyFolderPositions;
private int count;
public FilteredCursor(PartnerBookmarksProviderProxy proxy, Cursor cursor) {
super(cursor);
emptyFolderPositions = new HashSet<>();
count = cursor.getCount();
for (int i = 0; i < cursor.getCount(); i++) {
cursor.moveToPosition(i);
final long id = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks._ID));
final int type = cursor.getInt(cursor.getColumnIndexOrThrow(BrowserContract.Bookmarks.TYPE));
if (type == BrowserContract.Bookmarks.TYPE_FOLDER && proxy.isFolderEmpty(id)) {
// We do not support deleting folders. So at least hide partner folders that are
// empty because all bookmarks inside it are deleted/hidden.
// Note that this will still show folders with empty folders in them. But multi-level
// partner bookmarks are very unlikely.
count--;
emptyFolderPositions.add(i);
}
}
}
@Override
public int getCount() {
return count;
}
@Override
public boolean moveToPosition(int position) {
final Cursor cursor = getWrappedCursor();
final int actualCount = cursor.getCount();
// Find the next position pointing to a bookmark or a non-empty folder
while (position < actualCount && emptyFolderPositions.contains(position)) {
position++;
}
return position < actualCount && cursor.moveToPosition(position);
}
}
private static String getAuthority(Context context) {
return context.getPackageName() + AUTHORITY_PREFIX;
}
public static Uri getUriForBookmarks(Context context, long folderId) {
return new Uri.Builder()
.scheme("content")
.authority(getAuthority(context))
.appendPath("bookmarks")
.appendPath(String.valueOf(folderId))
.build();
}
public static Uri getUriForIcon(Context context, long bookmarkId) {
return new Uri.Builder()
.scheme("content")
.authority(getAuthority(context))
.appendPath("icons")
.appendPath(String.valueOf(bookmarkId))
.build();
}
public static Uri getUriForBookmark(Context context, long bookmarkId) {
return new Uri.Builder()
.scheme("content")
.authority(getAuthority(context))
.appendPath("bookmark")
.appendPath(String.valueOf(bookmarkId))
.build();
}
private final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
@Override
public boolean onCreate() {
String authority = getAuthority(assertAndGetContext());
uriMatcher.addURI(authority, "bookmarks/*", URI_MATCH_BOOKMARKS);
uriMatcher.addURI(authority, "icons/*", URI_MATCH_ICON);
uriMatcher.addURI(authority, "bookmark/*", URI_MATCH_BOOKMARK);
return true;
}
@Override
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
final Context context = assertAndGetContext();
final int match = uriMatcher.match(uri);
final ContentResolver contentResolver = context.getContentResolver();
switch (match) {
case URI_MATCH_BOOKMARKS:
final long bookmarkId = ContentUris.parseId(uri);
if (bookmarkId == -1) {
throw new IllegalArgumentException("Bookmark id is not a number");
}
final Cursor cursor = getBookmarksInFolder(contentResolver, bookmarkId);
cursor.setNotificationUri(context.getContentResolver(), uri);
return new FilteredCursor(this, cursor);
case URI_MATCH_ICON:
return getIcon(contentResolver, ContentUris.parseId(uri));
default:
throw new UnsupportedOperationException("Unknown URI " + uri.toString());
}
}
@Override
public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
final int match = uriMatcher.match(uri);
switch (match) {
case URI_MATCH_BOOKMARK:
rememberRemovedBookmark(ContentUris.parseId(uri));
notifyBookmarkChange();
return 1;
default:
throw new UnsupportedOperationException("Unknown URI " + uri.toString());
}
}
private void notifyBookmarkChange() {
final Context context = assertAndGetContext();
context.getContentResolver().notifyChange(
new Uri.Builder()
.scheme("content")
.authority(getAuthority(context))
.appendPath("bookmarks")
.build(),
null);
}
private synchronized void rememberRemovedBookmark(long bookmarkId) {
Set<String> deletedIds = getRemovedBookmarkIds();
deletedIds.add(String.valueOf(bookmarkId));
GeckoSharedPrefs.forProfile(assertAndGetContext())
.edit()
.putStringSet(PREF_DELETED_PARTNER_BOOKMARKS, deletedIds)
.apply();
}
private synchronized Set<String> getRemovedBookmarkIds() {
SharedPreferences preferences = GeckoSharedPrefs.forProfile(assertAndGetContext());
return preferences.getStringSet(PREF_DELETED_PARTNER_BOOKMARKS, new HashSet<String>());
}
private Cursor getBookmarksInFolder(ContentResolver contentResolver, long folderId) {
// Use root folder id or transform negative id into actual (positive) folder id.
final long actualFolderId = folderId == BrowserContract.Bookmarks.FIXED_ROOT_ID
? PartnerContract.PARENT_ROOT_ID
: BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START - folderId;
final String removedBookmarkIds = TextUtils.join(",", getRemovedBookmarkIds());
return contentResolver.query(
PartnerContract.CONTENT_URI,
new String[] {
// Transform ids into negative values starting with FAKE_PARTNER_BOOKMARKS_START.
"(" + BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START + " - " + PartnerContract.ID + ") as " + BrowserContract.Bookmarks._ID,
"(" + BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START + " - " + PartnerContract.ID + ") as " + BrowserContract.Combined.BOOKMARK_ID,
PartnerContract.TITLE + " as " + BrowserContract.Bookmarks.TITLE,
PartnerContract.URL + " as " + BrowserContract.Bookmarks.URL,
// Transform parent ids to negative ids as well
"(" + BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START + " - " + PartnerContract.PARENT + ") as " + BrowserContract.Bookmarks.PARENT,
// Convert types (we use 0-1 and the partner provider 1-2)
"(2 - " + PartnerContract.TYPE + ") as " + BrowserContract.Bookmarks.TYPE,
// Use the ID of the entry as GUID
PartnerContract.ID + " as " + BrowserContract.Bookmarks.GUID
},
PartnerContract.PARENT + " = ?"
// We only want to read bookmarks or folders from the content provider
+ " AND " + BrowserContract.Bookmarks.TYPE + " IN (?,?)"
// Only select entries with non empty title
+ " AND " + BrowserContract.Bookmarks.TITLE + " <> ''"
// Filter all "deleted" ids
+ " AND " + BrowserContract.Combined.BOOKMARK_ID + " NOT IN (" + removedBookmarkIds + ")",
new String[] {
String.valueOf(actualFolderId),
String.valueOf(PartnerContract.TYPE_BOOKMARK),
String.valueOf(PartnerContract.TYPE_FOLDER)
},
// Same order we use in our content provider (without position)
BrowserContract.Bookmarks.TYPE + " ASC, " + BrowserContract.Bookmarks._ID + " ASC");
}
private boolean isFolderEmpty(long folderId) {
final Context context = assertAndGetContext();
final Cursor cursor = getBookmarksInFolder(context.getContentResolver(), folderId);
if (cursor == null) {
return true;
}
try {
return cursor.getCount() == 0;
} finally {
cursor.close();
}
}
private Cursor getIcon(ContentResolver contentResolver, long bookmarkId) {
final long actualId = BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START - bookmarkId;
return contentResolver.query(
PartnerContract.CONTENT_URI,
new String[] {
PartnerContract.TOUCHICON,
PartnerContract.FAVICON
},
PartnerContract.ID + " = ?",
new String[] {
String.valueOf(actualId)
},
null);
}
private Context assertAndGetContext() {
final Context context = super.getContext();
if (context == null) {
throw new AssertionError("Context is null");
}
return context;
}
@Override
public String getType(@NonNull Uri uri) {
throw new UnsupportedOperationException();
}
@Override
public Uri insert(@NonNull Uri uri, ContentValues values) {
throw new UnsupportedOperationException();
}
@Override
public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
throw new UnsupportedOperationException();
}
}

Просмотреть файл

@ -6,12 +6,16 @@ package org.mozilla.gecko.favicons;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.distribution.PartnerBookmarksProviderProxy;
import org.mozilla.gecko.favicons.decoders.FaviconDecoder;
import org.mozilla.gecko.favicons.decoders.LoadFaviconResult;
import org.mozilla.gecko.util.GeckoJarReader;
@ -57,6 +61,12 @@ public class LoadFaviconTask {
*/
public static final int FLAG_BYPASS_CACHE_WHEN_DOWNLOADING_ICONS = 2;
/**
* If downloading from the favicon URL failed then do NOT try to guess the default URL and
* download from the default URL.
*/
public static final int FLAG_NO_DOWNLOAD_FROM_GUESSED_DEFAULT_URL = 4;
private static final int MAX_REDIRECTS_TO_FOLLOW = 5;
// The default size of the buffer to use for downloading Favicons in the event no size is given
// by the server.
@ -198,6 +208,73 @@ public class LoadFaviconTask {
return null;
}
/**
* Fetch icon from a content provider following the partner bookmarks provider contract.
*/
private Bitmap fetchContentProviderFavicon(String uri, int targetWidthAndHeight) {
if (TextUtils.isEmpty(uri)) {
return null;
}
if (!uri.startsWith("content://")) {
return null;
}
Cursor cursor = context.getContentResolver().query(
Uri.parse(uri),
new String[] {
PartnerBookmarksProviderProxy.PartnerContract.TOUCHICON,
PartnerBookmarksProviderProxy.PartnerContract.FAVICON,
},
null,
null,
null
);
if (cursor == null) {
return null;
}
try {
if (!cursor.moveToFirst()) {
return null;
}
Bitmap icon = decodeFromCursor(cursor, PartnerBookmarksProviderProxy.PartnerContract.TOUCHICON, targetWidthAndHeight);
if (icon != null) {
return icon;
}
icon = decodeFromCursor(cursor, PartnerBookmarksProviderProxy.PartnerContract.FAVICON, targetWidthAndHeight);
if (icon != null) {
return icon;
}
} finally {
cursor.close();
}
return null;
}
private Bitmap decodeFromCursor(Cursor cursor, String column, int targetWidthAndHeight) {
final int index = cursor.getColumnIndex(column);
if (index == -1) {
return null;
}
if (cursor.isNull(index)) {
return null;
}
final byte[] data = cursor.getBlob(index);
LoadFaviconResult result = FaviconDecoder.decodeFavicon(data, 0, data.length);
if (result == null) {
return null;
}
return result.getBestBitmap(targetWidthAndHeight);
}
// Runs in background thread.
// Does not attempt to fetch from JARs.
private LoadFaviconResult downloadFavicon(URI targetFaviconURI) {
@ -423,6 +500,14 @@ public class LoadFaviconTask {
return image;
}
// Download from a content provider
image = fetchContentProviderFavicon(faviconURL, targetWidthAndHeight);
if (imageIsValid(image)) {
// We don't want to put this into the DB.
Favicons.putFaviconInMemCache(faviconURL, image);
return image;
}
try {
loadedBitmaps = downloadFavicon(new URI(faviconURL));
} catch (URISyntaxException e) {
@ -440,32 +525,14 @@ public class LoadFaviconTask {
saveFaviconToDb(db, loadedBitmaps.getBytesForDatabaseStorage());
return pushToCacheAndGetResult(loadedBitmaps);
} else {
final Map<Integer, Bitmap> iconMap = new HashMap<>();
final List<Integer> sizes = new ArrayList<>();
while (loadedBitmaps.getBitmaps().hasNext()) {
final Bitmap b = loadedBitmaps.getBitmaps().next();
// It's possible to receive null, most likely due to OOM or a zero-sized image,
// from BitmapUtils.decodeByteArray(byte[], int, int, BitmapFactory.Options)
if (b != null) {
iconMap.put(b.getWidth(), b);
sizes.add(b.getWidth());
}
}
int bestSize = Favicons.selectBestSizeFromList(sizes, targetWidthAndHeight);
if (bestSize == -1) {
// No icons found: this could occur if we weren't able to process any of the
// supplied icons.
return null;
}
return iconMap.get(bestSize);
return loadedBitmaps.getBestBitmap(targetWidthAndHeight);
}
}
if ((FLAG_NO_DOWNLOAD_FROM_GUESSED_DEFAULT_URL & flags) == FLAG_NO_DOWNLOAD_FROM_GUESSED_DEFAULT_URL) {
return null;
}
if (isUsingDefaultURL) {
Favicons.putFaviconInFailedCache(faviconURL);
return null;

Просмотреть файл

@ -6,9 +6,16 @@ package org.mozilla.gecko.favicons.decoders;
import android.graphics.Bitmap;
import android.util.Log;
import android.util.SparseArray;
import org.mozilla.gecko.favicons.Favicons;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* Class representing the result of loading a favicon.
@ -73,4 +80,29 @@ public class LoadFaviconResult {
return null;
}
public Bitmap getBestBitmap(int targetWidthAndHeight) {
final SparseArray<Bitmap> iconMap = new SparseArray<>();
final List<Integer> sizes = new ArrayList<>();
while (bitmapsDecoded.hasNext()) {
final Bitmap b = bitmapsDecoded.next();
// It's possible to receive null, most likely due to OOM or a zero-sized image,
// from BitmapUtils.decodeByteArray(byte[], int, int, BitmapFactory.Options)
if (b != null) {
iconMap.put(b.getWidth(), b);
sizes.add(b.getWidth());
}
}
int bestSize = Favicons.selectBestSizeFromList(sizes, targetWidthAndHeight);
if (bestSize == -1) {
// No icons found: this could occur if we weren't able to process any of the
// supplied icons.
return null;
}
return iconMap.get(bestSize);
}
}

Просмотреть файл

@ -15,7 +15,7 @@ import org.mozilla.gecko.R;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.BrowserContract.Bookmarks;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.distribution.PartnerBookmarksProviderClient;
import org.mozilla.gecko.distribution.PartnerBookmarksProviderProxy;
import org.mozilla.gecko.home.BookmarksListAdapter.FolderInfo;
import org.mozilla.gecko.home.BookmarksListAdapter.OnRefreshFolderListener;
import org.mozilla.gecko.home.BookmarksListAdapter.RefreshType;
@ -235,7 +235,7 @@ public class BookmarksPanel extends HomeFragment {
if (GeckoSharedPrefs.forProfile(getContext()).getBoolean(GeckoPreferences.PREFS_READ_PARTNER_BOOKMARKS_PROVIDER, false)
&& (isRootFolder || mFolderInfo.id <= Bookmarks.FAKE_PARTNER_BOOKMARKS_START)) {
partnerCursor = PartnerBookmarksProviderClient.getBookmarksInFolder(contentResolver, mFolderInfo.id);
partnerCursor = contentResolver.query(PartnerBookmarksProviderProxy.getUriForBookmarks(getContext(), mFolderInfo.id), null, null, null, null, null);
}
if (isRootFolder || mFolderInfo.id > Bookmarks.FAKE_PARTNER_BOOKMARKS_START) {

Просмотреть файл

@ -5,6 +5,7 @@
package org.mozilla.gecko.home;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.util.StringUtils;
import android.database.Cursor;
@ -43,8 +44,12 @@ public class HomeContextMenuInfo extends AdapterContextMenuInfo {
return historyId > -1;
}
public boolean hasPartnerBookmarkId() {
return bookmarkId <= BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START;
}
public boolean canRemove() {
return hasBookmarkId() || hasHistoryId();
return hasBookmarkId() || hasHistoryId() || hasPartnerBookmarkId();
}
public String getDisplayTitle() {

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше