mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-08 12:37:37 +00:00
819 lines
23 KiB
JavaScript
819 lines
23 KiB
JavaScript
/* 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 {Cc, Ci, Cu} = require("chrome");
|
|
const {rgbToHsl} = require("devtools/css-color").colorUtils;
|
|
const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js");
|
|
const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
|
|
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
|
|
|
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, "XULRuntime", function() {
|
|
return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime);
|
|
});
|
|
|
|
loader.lazyGetter(this, "l10n", () => Services.strings
|
|
.createBundle("chrome://browser/locale/devtools/eyedropper.properties"));
|
|
|
|
const EYEDROPPER_URL = "chrome://browser/content/devtools/eyedropper.xul";
|
|
const CROSSHAIRS_URL = "chrome://browser/content/devtools/eyedropper/crosshairs.css";
|
|
const NOCURSOR_URL = "chrome://browser/content/devtools/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.
|
|
*/
|
|
let EyedropperManager = {
|
|
_instances: new WeakMap(),
|
|
|
|
getInstance: function(chromeWindow) {
|
|
return this._instances.get(chromeWindow);
|
|
},
|
|
|
|
createInstance: function(chromeWindow) {
|
|
let dropper = this.getInstance(chromeWindow);
|
|
if (dropper) {
|
|
return dropper;
|
|
}
|
|
|
|
dropper = new Eyedropper(chromeWindow);
|
|
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'
|
|
*/
|
|
function Eyedropper(chromeWindow, opts = { copyOnSelect: true }) {
|
|
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 = XULRuntime.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
|
|
};
|
|
|
|
let mm = this._contentTab.linkedBrowser.messageManager;
|
|
mm.loadFrameScript("resource:///modules/devtools/eyedropper/eyedropper-child.js", true);
|
|
|
|
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 = (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.selectedTab;
|
|
},
|
|
|
|
/**
|
|
* Fetch a screenshot of the content.
|
|
*
|
|
* @return {promise}
|
|
* Promise that resolves with the screenshot as a dataURL
|
|
*/
|
|
getContentScreenshot: function() {
|
|
let deferred = promise.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();
|
|
}
|
|
let deferred = promise.defer();
|
|
|
|
this.isOpen = true;
|
|
|
|
this._showCrosshairs();
|
|
|
|
// Get screenshot of content so we can inspect colors
|
|
this.getContentScreenshot().then((dataURL) => {
|
|
this._contentImage = new this._chromeWindow.Image();
|
|
this._contentImage.src = dataURL;
|
|
|
|
// Wait for screenshot to load
|
|
this._contentImage.onload = () => {
|
|
// Then start showing the eyedropper UI
|
|
this._chromeDocument.addEventListener("mousemove", this._onFirstMouseMove);
|
|
deferred.resolve();
|
|
|
|
this.isStarted = true;
|
|
this.emit("started");
|
|
}
|
|
});
|
|
|
|
return deferred.promise;
|
|
},
|
|
|
|
/**
|
|
* 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.linkedBrowser.getBoundingClientRect();
|
|
if (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) {
|
|
Services.appShell.hiddenDOMWindow.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 = Services.appShell.hiddenDOMWindow.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 = 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 = DOMUtils.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();
|
|
}
|