зеркало из https://github.com/mozilla/gecko-dev.git
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:
Коммит
42933ba381
|
@ -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() {
|
||||
|
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче