1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/devtools/eyedropper/eyedropper.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,712 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +const {Cc, Ci, Cu} = require("chrome"); 1.9 +const {rgbToHsl} = require("devtools/css-color").colorUtils; 1.10 +const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js"); 1.11 + 1.12 +Cu.import("resource://gre/modules/Services.jsm"); 1.13 + 1.14 +loader.lazyGetter(this, "clipboardHelper", function() { 1.15 + return Cc["@mozilla.org/widget/clipboardhelper;1"] 1.16 + .getService(Ci.nsIClipboardHelper); 1.17 +}); 1.18 + 1.19 +loader.lazyGetter(this, "ssService", function() { 1.20 + return Cc["@mozilla.org/content/style-sheet-service;1"] 1.21 + .getService(Ci.nsIStyleSheetService); 1.22 +}); 1.23 + 1.24 +loader.lazyGetter(this, "ioService", function() { 1.25 + return Cc["@mozilla.org/network/io-service;1"] 1.26 + .getService(Ci.nsIIOService); 1.27 +}); 1.28 + 1.29 +loader.lazyGetter(this, "DOMUtils", function () { 1.30 + return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); 1.31 +}); 1.32 + 1.33 +loader.lazyGetter(this, "XULRuntime", function() { 1.34 + return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime); 1.35 +}); 1.36 + 1.37 +loader.lazyGetter(this, "l10n", () => Services.strings 1.38 + .createBundle("chrome://browser/locale/devtools/eyedropper.properties")); 1.39 + 1.40 +const EYEDROPPER_URL = "chrome://browser/content/devtools/eyedropper.xul"; 1.41 +const CROSSHAIRS_URL = "chrome://browser/content/devtools/eyedropper/crosshairs.css"; 1.42 +const NOCURSOR_URL = "chrome://browser/content/devtools/eyedropper/nocursor.css"; 1.43 + 1.44 +const ZOOM_PREF = "devtools.eyedropper.zoom"; 1.45 +const FORMAT_PREF = "devtools.defaultColorUnit"; 1.46 + 1.47 +const CANVAS_WIDTH = 96; 1.48 +const CANVAS_OFFSET = 3; // equals the border width of the canvas. 1.49 +const CLOSE_DELAY = 750; 1.50 + 1.51 +const HEX_BOX_WIDTH = CANVAS_WIDTH + CANVAS_OFFSET * 2; 1.52 +const HSL_BOX_WIDTH = 158; 1.53 + 1.54 +/** 1.55 + * Manage instances of eyedroppers for windows. Registering here isn't 1.56 + * necessary for creating an eyedropper, but can be used for testing. 1.57 + */ 1.58 +let EyedropperManager = { 1.59 + _instances: new WeakMap(), 1.60 + 1.61 + getInstance: function(chromeWindow) { 1.62 + return this._instances.get(chromeWindow); 1.63 + }, 1.64 + 1.65 + createInstance: function(chromeWindow) { 1.66 + let dropper = this.getInstance(chromeWindow); 1.67 + if (dropper) { 1.68 + return dropper; 1.69 + } 1.70 + 1.71 + dropper = new Eyedropper(chromeWindow); 1.72 + this._instances.set(chromeWindow, dropper); 1.73 + 1.74 + dropper.on("destroy", () => { 1.75 + this.deleteInstance(chromeWindow); 1.76 + }); 1.77 + 1.78 + return dropper; 1.79 + }, 1.80 + 1.81 + deleteInstance: function(chromeWindow) { 1.82 + this._instances.delete(chromeWindow); 1.83 + } 1.84 +} 1.85 + 1.86 +exports.EyedropperManager = EyedropperManager; 1.87 + 1.88 +/** 1.89 + * Eyedropper widget. Once opened, shows zoomed area above current pixel and 1.90 + * displays the color value of the center pixel. Clicking on the window will 1.91 + * close the widget and fire a 'select' event. If 'copyOnSelect' is true, the color 1.92 + * will also be copied to the clipboard. 1.93 + * 1.94 + * let eyedropper = new Eyedropper(window); 1.95 + * eyedropper.open(); 1.96 + * 1.97 + * eyedropper.once("select", (ev, color) => { 1.98 + * console.log(color); // "rgb(20, 50, 230)" 1.99 + * }) 1.100 + * 1.101 + * @param {DOMWindow} chromeWindow 1.102 + * window to inspect 1.103 + * @param {object} opts 1.104 + * optional options object, with 'copyOnSelect' 1.105 + */ 1.106 +function Eyedropper(chromeWindow, opts = { copyOnSelect: true }) { 1.107 + this.copyOnSelect = opts.copyOnSelect; 1.108 + 1.109 + this._onFirstMouseMove = this._onFirstMouseMove.bind(this); 1.110 + this._onMouseMove = this._onMouseMove.bind(this); 1.111 + this._onMouseDown = this._onMouseDown.bind(this); 1.112 + this._onKeyDown = this._onKeyDown.bind(this); 1.113 + this._onFrameLoaded = this._onFrameLoaded.bind(this); 1.114 + 1.115 + this._chromeWindow = chromeWindow; 1.116 + this._chromeDocument = chromeWindow.document; 1.117 + 1.118 + this._dragging = true; 1.119 + this.loaded = false; 1.120 + 1.121 + this._mouseMoveCounter = 0; 1.122 + 1.123 + this.format = Services.prefs.getCharPref(FORMAT_PREF); // color value format 1.124 + this.zoom = Services.prefs.getIntPref(ZOOM_PREF); // zoom level - integer 1.125 + 1.126 + this._zoomArea = { 1.127 + x: 0, // the left coordinate of the center of the inspected region 1.128 + y: 0, // the top coordinate of the center of the inspected region 1.129 + width: CANVAS_WIDTH, // width of canvas to draw zoomed area onto 1.130 + height: CANVAS_WIDTH // height of canvas 1.131 + }; 1.132 + EventEmitter.decorate(this); 1.133 +} 1.134 + 1.135 +exports.Eyedropper = Eyedropper; 1.136 + 1.137 +Eyedropper.prototype = { 1.138 + /** 1.139 + * Get the number of cells (blown-up pixels) per direction in the grid. 1.140 + */ 1.141 + get cellsWide() { 1.142 + // Canvas will render whole "pixels" (cells) only, and an even 1.143 + // number at that. Round up to the nearest even number of pixels. 1.144 + let cellsWide = Math.ceil(this._zoomArea.width / this.zoom); 1.145 + cellsWide += cellsWide % 2; 1.146 + 1.147 + return cellsWide; 1.148 + }, 1.149 + 1.150 + /** 1.151 + * Get the size of each cell (blown-up pixel) in the grid. 1.152 + */ 1.153 + get cellSize() { 1.154 + return this._zoomArea.width / this.cellsWide; 1.155 + }, 1.156 + 1.157 + /** 1.158 + * Get index of cell in the center of the grid. 1.159 + */ 1.160 + get centerCell() { 1.161 + return Math.floor(this.cellsWide / 2); 1.162 + }, 1.163 + 1.164 + /** 1.165 + * Get color of center cell in the grid. 1.166 + */ 1.167 + get centerColor() { 1.168 + let x = y = (this.centerCell * this.cellSize) + (this.cellSize / 2); 1.169 + let rgb = this._ctx.getImageData(x, y, 1, 1).data; 1.170 + return rgb; 1.171 + }, 1.172 + 1.173 + /** 1.174 + * Start the eyedropper. Add listeners for a mouse move in the window to 1.175 + * show the eyedropper. 1.176 + */ 1.177 + open: function() { 1.178 + if (this.isOpen) { 1.179 + // the eyedropper is aready open, don't create another panel. 1.180 + return; 1.181 + } 1.182 + this.isOpen = true; 1.183 + 1.184 + this._OS = XULRuntime.OS; 1.185 + 1.186 + this._chromeDocument.addEventListener("mousemove", this._onFirstMouseMove); 1.187 + 1.188 + this._showCrosshairs(); 1.189 + }, 1.190 + 1.191 + /** 1.192 + * Called on the first mouse move over the window. Opens the eyedropper 1.193 + * panel where the mouse is. 1.194 + */ 1.195 + _onFirstMouseMove: function(event) { 1.196 + this._chromeDocument.removeEventListener("mousemove", this._onFirstMouseMove); 1.197 + 1.198 + this._panel = this._buildPanel(); 1.199 + 1.200 + let popupSet = this._chromeDocument.querySelector("#mainPopupSet"); 1.201 + popupSet.appendChild(this._panel); 1.202 + 1.203 + let { panelX, panelY } = this._getPanelCoordinates(event); 1.204 + this._panel.openPopupAtScreen(panelX, panelY); 1.205 + 1.206 + this._setCoordinates(event); 1.207 + 1.208 + this._addListeners(); 1.209 + 1.210 + // hide cursor as we'll be showing the panel over the mouse instead. 1.211 + this._hideCrosshairs(); 1.212 + this._hideCursor(); 1.213 + }, 1.214 + 1.215 + /** 1.216 + * Set the current coordinates to inspect from where a mousemove originated. 1.217 + * 1.218 + * @param {MouseEvent} event 1.219 + * Event for the mouse move. 1.220 + */ 1.221 + _setCoordinates: function(event) { 1.222 + let win = this._chromeWindow; 1.223 + 1.224 + let x, y; 1.225 + if (this._OS == "Linux") { 1.226 + // event.clientX is off on Linux, so calculate it by hand 1.227 + let windowX = win.screenX + (win.outerWidth - win.innerWidth); 1.228 + x = event.screenX - windowX; 1.229 + 1.230 + let windowY = win.screenY + (win.outerHeight - win.innerHeight); 1.231 + y = event.screenY - windowY; 1.232 + } 1.233 + else { 1.234 + x = event.clientX; 1.235 + y = event.clientY; 1.236 + } 1.237 + 1.238 + // don't let it inspect outside the browser window 1.239 + x = Math.max(0, Math.min(x, win.outerWidth - 1)); 1.240 + y = Math.max(0, Math.min(y, win.outerHeight - 1)); 1.241 + 1.242 + this._zoomArea.x = x; 1.243 + this._zoomArea.y = y; 1.244 + }, 1.245 + 1.246 + /** 1.247 + * Build and add a new eyedropper panel to the window. 1.248 + * 1.249 + * @return {Panel} 1.250 + * The XUL panel holding the eyedropper UI. 1.251 + */ 1.252 + _buildPanel: function() { 1.253 + let panel = this._chromeDocument.createElement("panel"); 1.254 + panel.setAttribute("noautofocus", true); 1.255 + panel.setAttribute("noautohide", true); 1.256 + panel.setAttribute("level", "floating"); 1.257 + panel.setAttribute("class", "devtools-eyedropper-panel"); 1.258 + 1.259 + let iframe = this._iframe = this._chromeDocument.createElement("iframe"); 1.260 + iframe.addEventListener("load", this._onFrameLoaded, true); 1.261 + iframe.setAttribute("flex", "1"); 1.262 + iframe.setAttribute("transparent", "transparent"); 1.263 + iframe.setAttribute("allowTransparency", true); 1.264 + iframe.setAttribute("class", "devtools-eyedropper-iframe"); 1.265 + iframe.setAttribute("src", EYEDROPPER_URL); 1.266 + iframe.setAttribute("width", CANVAS_WIDTH); 1.267 + iframe.setAttribute("height", CANVAS_WIDTH); 1.268 + 1.269 + panel.appendChild(iframe); 1.270 + 1.271 + return panel; 1.272 + }, 1.273 + 1.274 + /** 1.275 + * Event handler for the panel's iframe's load event. Emits 1.276 + * a "load" event from this eyedropper object. 1.277 + */ 1.278 + _onFrameLoaded: function() { 1.279 + this._iframe.removeEventListener("load", this._onFrameLoaded, true); 1.280 + 1.281 + this._iframeDocument = this._iframe.contentDocument; 1.282 + this._colorPreview = this._iframeDocument.querySelector("#color-preview"); 1.283 + this._colorValue = this._iframeDocument.querySelector("#color-value"); 1.284 + 1.285 + // value box will be too long for hex values and too short for hsl 1.286 + let valueBox = this._iframeDocument.querySelector("#color-value-box"); 1.287 + if (this.format == "hex") { 1.288 + valueBox.style.width = HEX_BOX_WIDTH + "px"; 1.289 + } 1.290 + else if (this.format == "hsl") { 1.291 + valueBox.style.width = HSL_BOX_WIDTH + "px"; 1.292 + } 1.293 + 1.294 + this._canvas = this._iframeDocument.querySelector("#canvas"); 1.295 + this._ctx = this._canvas.getContext("2d"); 1.296 + 1.297 + // so we preserve the clear pixel boundaries 1.298 + this._ctx.mozImageSmoothingEnabled = false; 1.299 + 1.300 + this._drawWindow(); 1.301 + 1.302 + this._addPanelListeners(); 1.303 + this._iframe.focus(); 1.304 + 1.305 + this.loaded = true; 1.306 + this.emit("load"); 1.307 + }, 1.308 + 1.309 + /** 1.310 + * Add key listeners to the panel. 1.311 + */ 1.312 + _addPanelListeners: function() { 1.313 + this._iframeDocument.addEventListener("keydown", this._onKeyDown); 1.314 + 1.315 + let closeCmd = this._iframeDocument.getElementById("eyedropper-cmd-close"); 1.316 + closeCmd.addEventListener("command", this.destroy.bind(this), true); 1.317 + 1.318 + let copyCmd = this._iframeDocument.getElementById("eyedropper-cmd-copy"); 1.319 + copyCmd.addEventListener("command", this.selectColor.bind(this), true); 1.320 + }, 1.321 + 1.322 + /** 1.323 + * Remove listeners from the panel. 1.324 + */ 1.325 + _removePanelListeners: function() { 1.326 + this._iframeDocument.removeEventListener("keydown", this._onKeyDown); 1.327 + }, 1.328 + 1.329 + /** 1.330 + * Add mouse event listeners to the document we're inspecting. 1.331 + */ 1.332 + _addListeners: function() { 1.333 + this._chromeDocument.addEventListener("mousemove", this._onMouseMove); 1.334 + this._chromeDocument.addEventListener("mousedown", this._onMouseDown); 1.335 + }, 1.336 + 1.337 + /** 1.338 + * Remove mouse event listeners from the document we're inspecting. 1.339 + */ 1.340 + _removeListeners: function() { 1.341 + this._chromeDocument.removeEventListener("mousemove", this._onFirstMouseMove); 1.342 + this._chromeDocument.removeEventListener("mousemove", this._onMouseMove); 1.343 + this._chromeDocument.removeEventListener("mousedown", this._onMouseDown); 1.344 + }, 1.345 + 1.346 + /** 1.347 + * Hide the cursor. 1.348 + */ 1.349 + _hideCursor: function() { 1.350 + registerStyleSheet(NOCURSOR_URL); 1.351 + }, 1.352 + 1.353 + /** 1.354 + * Reset the cursor back to default. 1.355 + */ 1.356 + _resetCursor: function() { 1.357 + unregisterStyleSheet(NOCURSOR_URL); 1.358 + }, 1.359 + 1.360 + /** 1.361 + * Show a crosshairs as the mouse cursor 1.362 + */ 1.363 + _showCrosshairs: function() { 1.364 + registerStyleSheet(CROSSHAIRS_URL); 1.365 + }, 1.366 + 1.367 + /** 1.368 + * Reset cursor. 1.369 + */ 1.370 + _hideCrosshairs: function() { 1.371 + unregisterStyleSheet(CROSSHAIRS_URL); 1.372 + }, 1.373 + 1.374 + /** 1.375 + * Event handler for a mouse move over the page we're inspecting. 1.376 + * Preview the area under the cursor, and move panel to be under the cursor. 1.377 + * 1.378 + * @param {DOMEvent} event 1.379 + * MouseEvent for the mouse moving 1.380 + */ 1.381 + _onMouseMove: function(event) { 1.382 + if (!this._dragging || !this._panel || !this._canvas) { 1.383 + return; 1.384 + } 1.385 + 1.386 + if (this._OS == "Linux" && ++this._mouseMoveCounter % 2 == 0) { 1.387 + // skip every other mousemove to preserve performance. 1.388 + return; 1.389 + } 1.390 + 1.391 + this._setCoordinates(event); 1.392 + this._drawWindow(); 1.393 + 1.394 + let { panelX, panelY } = this._getPanelCoordinates(event); 1.395 + this._movePanel(panelX, panelY); 1.396 + }, 1.397 + 1.398 + /** 1.399 + * Get coordinates of where the eyedropper panel should go based on 1.400 + * the current coordinates of the mouse cursor. 1.401 + * 1.402 + * @param {MouseEvent} event 1.403 + * object with properties 'screenX' and 'screenY' 1.404 + * 1.405 + * @return {object} 1.406 + * object with properties 'panelX', 'panelY' 1.407 + */ 1.408 + _getPanelCoordinates: function({screenX, screenY}) { 1.409 + let win = this._chromeWindow; 1.410 + let offset = CANVAS_WIDTH / 2 + CANVAS_OFFSET; 1.411 + 1.412 + let panelX = screenX - offset; 1.413 + let windowX = win.screenX + (win.outerWidth - win.innerWidth); 1.414 + let maxX = win.screenX + win.outerWidth - offset - 1; 1.415 + 1.416 + let panelY = screenY - offset; 1.417 + let windowY = win.screenY + (win.outerHeight - win.innerHeight); 1.418 + let maxY = win.screenY + win.outerHeight - offset - 1; 1.419 + 1.420 + // don't let the panel move outside the browser window 1.421 + panelX = Math.max(windowX - offset, Math.min(panelX, maxX)); 1.422 + panelY = Math.max(windowY - offset, Math.min(panelY, maxY)); 1.423 + 1.424 + return { panelX: panelX, panelY: panelY }; 1.425 + }, 1.426 + 1.427 + /** 1.428 + * Move the eyedropper panel to the given coordinates. 1.429 + * 1.430 + * @param {number} screenX 1.431 + * left coordinate on the screen 1.432 + * @param {number} screenY 1.433 + * top coordinate 1.434 + */ 1.435 + _movePanel: function(screenX, screenY) { 1.436 + this._panelX = screenX; 1.437 + this._panelY = screenY; 1.438 + 1.439 + this._panel.moveTo(screenX, screenY); 1.440 + }, 1.441 + 1.442 + /** 1.443 + * Handler for the mouse down event on the inspected page. This means a 1.444 + * click, so we'll select the color that's currently hovered. 1.445 + * 1.446 + * @param {Event} event 1.447 + * DOM MouseEvent object 1.448 + */ 1.449 + _onMouseDown: function(event) { 1.450 + event.preventDefault(); 1.451 + event.stopPropagation(); 1.452 + 1.453 + this.selectColor(); 1.454 + }, 1.455 + 1.456 + /** 1.457 + * Select the current color that's being previewed. Fire a 1.458 + * "select" event with the color as an rgb string. 1.459 + */ 1.460 + selectColor: function() { 1.461 + if (this._isSelecting) { 1.462 + return; 1.463 + } 1.464 + this._isSelecting = true; 1.465 + this._dragging = false; 1.466 + 1.467 + this.emit("select", this._colorValue.value); 1.468 + 1.469 + if (this.copyOnSelect) { 1.470 + this.copyColor(this.destroy.bind(this)); 1.471 + } 1.472 + else { 1.473 + this.destroy(); 1.474 + } 1.475 + }, 1.476 + 1.477 + /** 1.478 + * Copy the currently inspected color to the clipboard. 1.479 + * 1.480 + * @param {Function} callback 1.481 + * Callback to be called when the color is in the clipboard. 1.482 + */ 1.483 + copyColor: function(callback) { 1.484 + Services.appShell.hiddenDOMWindow.clearTimeout(this._copyTimeout); 1.485 + 1.486 + let color = this._colorValue.value; 1.487 + clipboardHelper.copyString(color); 1.488 + 1.489 + this._colorValue.classList.add("highlight"); 1.490 + this._colorValue.value = "✓ " + l10n.GetStringFromName("colorValue.copied"); 1.491 + 1.492 + this._copyTimeout = Services.appShell.hiddenDOMWindow.setTimeout(() => { 1.493 + this._colorValue.classList.remove("highlight"); 1.494 + this._colorValue.value = color; 1.495 + 1.496 + if (callback) { 1.497 + callback(); 1.498 + } 1.499 + }, CLOSE_DELAY); 1.500 + }, 1.501 + 1.502 + /** 1.503 + * Handler for the keydown event on the panel. Either copy the color 1.504 + * or move the panel in a direction depending on the key pressed. 1.505 + * 1.506 + * @param {Event} event 1.507 + * DOM KeyboardEvent object 1.508 + */ 1.509 + _onKeyDown: function(event) { 1.510 + if (event.metaKey && event.keyCode === event.DOM_VK_C) { 1.511 + this.copyColor(); 1.512 + return; 1.513 + } 1.514 + 1.515 + let offsetX = 0; 1.516 + let offsetY = 0; 1.517 + let modifier = 1; 1.518 + 1.519 + if (event.keyCode === event.DOM_VK_LEFT) { 1.520 + offsetX = -1; 1.521 + } 1.522 + if (event.keyCode === event.DOM_VK_RIGHT) { 1.523 + offsetX = 1; 1.524 + } 1.525 + if (event.keyCode === event.DOM_VK_UP) { 1.526 + offsetY = -1; 1.527 + } 1.528 + if (event.keyCode === event.DOM_VK_DOWN) { 1.529 + offsetY = 1; 1.530 + } 1.531 + if (event.shiftKey) { 1.532 + modifier = 10; 1.533 + } 1.534 + 1.535 + offsetY *= modifier; 1.536 + offsetX *= modifier; 1.537 + 1.538 + if (offsetX !== 0 || offsetY !== 0) { 1.539 + this._zoomArea.x += offsetX; 1.540 + this._zoomArea.y += offsetY; 1.541 + 1.542 + this._drawWindow(); 1.543 + 1.544 + this._movePanel(this._panelX + offsetX, this._panelY + offsetY); 1.545 + 1.546 + event.preventDefault(); 1.547 + } 1.548 + }, 1.549 + 1.550 + /** 1.551 + * Draw the inspected area onto the canvas using the zoom level. 1.552 + */ 1.553 + _drawWindow: function() { 1.554 + let { width, height, x, y } = this._zoomArea; 1.555 + 1.556 + let zoomedWidth = width / this.zoom; 1.557 + let zoomedHeight = height / this.zoom; 1.558 + 1.559 + let drawX = x - (zoomedWidth / 2); 1.560 + let drawY = y - (zoomedHeight / 2); 1.561 + 1.562 + // draw the portion of the window we're inspecting 1.563 + this._ctx.drawWindow(this._chromeWindow, drawX, drawY, zoomedWidth, 1.564 + zoomedHeight, "white"); 1.565 + 1.566 + // now scale it 1.567 + let sx = 0; 1.568 + let sy = 0; 1.569 + let sw = zoomedWidth; 1.570 + let sh = zoomedHeight; 1.571 + let dx = 0; 1.572 + let dy = 0; 1.573 + let dw = width; 1.574 + let dh = height; 1.575 + 1.576 + this._ctx.drawImage(this._canvas, sx, sy, sw, sh, dx, dy, dw, dh); 1.577 + 1.578 + let rgb = this.centerColor; 1.579 + this._colorPreview.style.backgroundColor = toColorString(rgb, "rgb"); 1.580 + this._colorValue.value = toColorString(rgb, this.format); 1.581 + 1.582 + if (this.zoom > 2) { 1.583 + // grid at 2x is too busy 1.584 + this._drawGrid(); 1.585 + } 1.586 + this._drawCrosshair(); 1.587 + }, 1.588 + 1.589 + /** 1.590 + * Draw a grid on the canvas representing pixel boundaries. 1.591 + */ 1.592 + _drawGrid: function() { 1.593 + let { width, height } = this._zoomArea; 1.594 + 1.595 + this._ctx.lineWidth = 1; 1.596 + this._ctx.strokeStyle = "rgba(143, 143, 143, 0.2)"; 1.597 + 1.598 + for (let i = 0; i < width; i += this.cellSize) { 1.599 + this._ctx.beginPath(); 1.600 + this._ctx.moveTo(i - .5, 0); 1.601 + this._ctx.lineTo(i - .5, height); 1.602 + this._ctx.stroke(); 1.603 + 1.604 + this._ctx.beginPath(); 1.605 + this._ctx.moveTo(0, i - .5); 1.606 + this._ctx.lineTo(width, i - .5); 1.607 + this._ctx.stroke(); 1.608 + } 1.609 + }, 1.610 + 1.611 + /** 1.612 + * Draw a box on the canvas to highlight the center cell. 1.613 + */ 1.614 + _drawCrosshair: function() { 1.615 + let x = y = this.centerCell * this.cellSize; 1.616 + 1.617 + this._ctx.lineWidth = 1; 1.618 + this._ctx.lineJoin = 'miter'; 1.619 + this._ctx.strokeStyle = "rgba(0, 0, 0, 1)"; 1.620 + this._ctx.strokeRect(x - 1.5, y - 1.5, this.cellSize + 2, this.cellSize + 2); 1.621 + 1.622 + this._ctx.strokeStyle = "rgba(255, 255, 255, 1)"; 1.623 + this._ctx.strokeRect(x - 0.5, y - 0.5, this.cellSize, this.cellSize); 1.624 + }, 1.625 + 1.626 + /** 1.627 + * Destroy the eyedropper and clean up. Emits a "destroy" event. 1.628 + */ 1.629 + destroy: function() { 1.630 + this._resetCursor(); 1.631 + this._hideCrosshairs(); 1.632 + 1.633 + if (this._panel) { 1.634 + this._panel.hidePopup(); 1.635 + this._panel.remove(); 1.636 + this._panel = null; 1.637 + } 1.638 + this._removePanelListeners(); 1.639 + this._removeListeners(); 1.640 + 1.641 + this.isOpen = false; 1.642 + this._isSelecting = false; 1.643 + 1.644 + this.emit("destroy"); 1.645 + } 1.646 +} 1.647 + 1.648 +/** 1.649 + * Add a user style sheet that applies to all documents. 1.650 + */ 1.651 +function registerStyleSheet(url) { 1.652 + var uri = ioService.newURI(url, null, null); 1.653 + if (!ssService.sheetRegistered(uri, ssService.AGENT_SHEET)) { 1.654 + ssService.loadAndRegisterSheet(uri, ssService.AGENT_SHEET); 1.655 + } 1.656 +} 1.657 + 1.658 +/** 1.659 + * Remove a user style sheet. 1.660 + */ 1.661 +function unregisterStyleSheet(url) { 1.662 + var uri = ioService.newURI(url, null, null); 1.663 + if (ssService.sheetRegistered(uri, ssService.AGENT_SHEET)) { 1.664 + ssService.unregisterSheet(uri, ssService.AGENT_SHEET); 1.665 + } 1.666 +} 1.667 + 1.668 +/** 1.669 + * Get a formatted CSS color string from a color value. 1.670 + * 1.671 + * @param {array} rgb 1.672 + * Rgb values of a color to format 1.673 + * @param {string} format 1.674 + * Format of string. One of "hex", "rgb", "hsl", "name" 1.675 + * 1.676 + * @return {string} 1.677 + * Formatted color value, e.g. "#FFF" or "hsl(20, 10%, 10%)" 1.678 + */ 1.679 +function toColorString(rgb, format) { 1.680 + let [r,g,b] = rgb; 1.681 + 1.682 + switch(format) { 1.683 + case "hex": 1.684 + return hexString(rgb); 1.685 + case "rgb": 1.686 + return "rgb(" + r + ", " + g + ", " + b + ")"; 1.687 + case "hsl": 1.688 + let [h,s,l] = rgbToHsl(rgb); 1.689 + return "hsl(" + h + ", " + s + "%, " + l + "%)"; 1.690 + case "name": 1.691 + let str; 1.692 + try { 1.693 + str = DOMUtils.rgbToColorName(r, g, b); 1.694 + } catch(e) { 1.695 + str = hexString(rgb); 1.696 + } 1.697 + return str; 1.698 + default: 1.699 + return hexString(rgb); 1.700 + } 1.701 +} 1.702 + 1.703 +/** 1.704 + * Produce a hex-formatted color string from rgb values. 1.705 + * 1.706 + * @param {array} rgb 1.707 + * Rgb values of color to stringify 1.708 + * 1.709 + * @return {string} 1.710 + * Hex formatted string for color, e.g. "#FFEE00" 1.711 + */ 1.712 +function hexString([r,g,b]) { 1.713 + let val = (1 << 24) + (r << 16) + (g << 8) + (b << 0); 1.714 + return "#" + val.toString(16).substr(-6).toUpperCase(); 1.715 +}