browser/devtools/eyedropper/eyedropper.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 const {Cc, Ci, Cu} = require("chrome");
michael@0 6 const {rgbToHsl} = require("devtools/css-color").colorUtils;
michael@0 7 const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js");
michael@0 8
michael@0 9 Cu.import("resource://gre/modules/Services.jsm");
michael@0 10
michael@0 11 loader.lazyGetter(this, "clipboardHelper", function() {
michael@0 12 return Cc["@mozilla.org/widget/clipboardhelper;1"]
michael@0 13 .getService(Ci.nsIClipboardHelper);
michael@0 14 });
michael@0 15
michael@0 16 loader.lazyGetter(this, "ssService", function() {
michael@0 17 return Cc["@mozilla.org/content/style-sheet-service;1"]
michael@0 18 .getService(Ci.nsIStyleSheetService);
michael@0 19 });
michael@0 20
michael@0 21 loader.lazyGetter(this, "ioService", function() {
michael@0 22 return Cc["@mozilla.org/network/io-service;1"]
michael@0 23 .getService(Ci.nsIIOService);
michael@0 24 });
michael@0 25
michael@0 26 loader.lazyGetter(this, "DOMUtils", function () {
michael@0 27 return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
michael@0 28 });
michael@0 29
michael@0 30 loader.lazyGetter(this, "XULRuntime", function() {
michael@0 31 return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime);
michael@0 32 });
michael@0 33
michael@0 34 loader.lazyGetter(this, "l10n", () => Services.strings
michael@0 35 .createBundle("chrome://browser/locale/devtools/eyedropper.properties"));
michael@0 36
michael@0 37 const EYEDROPPER_URL = "chrome://browser/content/devtools/eyedropper.xul";
michael@0 38 const CROSSHAIRS_URL = "chrome://browser/content/devtools/eyedropper/crosshairs.css";
michael@0 39 const NOCURSOR_URL = "chrome://browser/content/devtools/eyedropper/nocursor.css";
michael@0 40
michael@0 41 const ZOOM_PREF = "devtools.eyedropper.zoom";
michael@0 42 const FORMAT_PREF = "devtools.defaultColorUnit";
michael@0 43
michael@0 44 const CANVAS_WIDTH = 96;
michael@0 45 const CANVAS_OFFSET = 3; // equals the border width of the canvas.
michael@0 46 const CLOSE_DELAY = 750;
michael@0 47
michael@0 48 const HEX_BOX_WIDTH = CANVAS_WIDTH + CANVAS_OFFSET * 2;
michael@0 49 const HSL_BOX_WIDTH = 158;
michael@0 50
michael@0 51 /**
michael@0 52 * Manage instances of eyedroppers for windows. Registering here isn't
michael@0 53 * necessary for creating an eyedropper, but can be used for testing.
michael@0 54 */
michael@0 55 let EyedropperManager = {
michael@0 56 _instances: new WeakMap(),
michael@0 57
michael@0 58 getInstance: function(chromeWindow) {
michael@0 59 return this._instances.get(chromeWindow);
michael@0 60 },
michael@0 61
michael@0 62 createInstance: function(chromeWindow) {
michael@0 63 let dropper = this.getInstance(chromeWindow);
michael@0 64 if (dropper) {
michael@0 65 return dropper;
michael@0 66 }
michael@0 67
michael@0 68 dropper = new Eyedropper(chromeWindow);
michael@0 69 this._instances.set(chromeWindow, dropper);
michael@0 70
michael@0 71 dropper.on("destroy", () => {
michael@0 72 this.deleteInstance(chromeWindow);
michael@0 73 });
michael@0 74
michael@0 75 return dropper;
michael@0 76 },
michael@0 77
michael@0 78 deleteInstance: function(chromeWindow) {
michael@0 79 this._instances.delete(chromeWindow);
michael@0 80 }
michael@0 81 }
michael@0 82
michael@0 83 exports.EyedropperManager = EyedropperManager;
michael@0 84
michael@0 85 /**
michael@0 86 * Eyedropper widget. Once opened, shows zoomed area above current pixel and
michael@0 87 * displays the color value of the center pixel. Clicking on the window will
michael@0 88 * close the widget and fire a 'select' event. If 'copyOnSelect' is true, the color
michael@0 89 * will also be copied to the clipboard.
michael@0 90 *
michael@0 91 * let eyedropper = new Eyedropper(window);
michael@0 92 * eyedropper.open();
michael@0 93 *
michael@0 94 * eyedropper.once("select", (ev, color) => {
michael@0 95 * console.log(color); // "rgb(20, 50, 230)"
michael@0 96 * })
michael@0 97 *
michael@0 98 * @param {DOMWindow} chromeWindow
michael@0 99 * window to inspect
michael@0 100 * @param {object} opts
michael@0 101 * optional options object, with 'copyOnSelect'
michael@0 102 */
michael@0 103 function Eyedropper(chromeWindow, opts = { copyOnSelect: true }) {
michael@0 104 this.copyOnSelect = opts.copyOnSelect;
michael@0 105
michael@0 106 this._onFirstMouseMove = this._onFirstMouseMove.bind(this);
michael@0 107 this._onMouseMove = this._onMouseMove.bind(this);
michael@0 108 this._onMouseDown = this._onMouseDown.bind(this);
michael@0 109 this._onKeyDown = this._onKeyDown.bind(this);
michael@0 110 this._onFrameLoaded = this._onFrameLoaded.bind(this);
michael@0 111
michael@0 112 this._chromeWindow = chromeWindow;
michael@0 113 this._chromeDocument = chromeWindow.document;
michael@0 114
michael@0 115 this._dragging = true;
michael@0 116 this.loaded = false;
michael@0 117
michael@0 118 this._mouseMoveCounter = 0;
michael@0 119
michael@0 120 this.format = Services.prefs.getCharPref(FORMAT_PREF); // color value format
michael@0 121 this.zoom = Services.prefs.getIntPref(ZOOM_PREF); // zoom level - integer
michael@0 122
michael@0 123 this._zoomArea = {
michael@0 124 x: 0, // the left coordinate of the center of the inspected region
michael@0 125 y: 0, // the top coordinate of the center of the inspected region
michael@0 126 width: CANVAS_WIDTH, // width of canvas to draw zoomed area onto
michael@0 127 height: CANVAS_WIDTH // height of canvas
michael@0 128 };
michael@0 129 EventEmitter.decorate(this);
michael@0 130 }
michael@0 131
michael@0 132 exports.Eyedropper = Eyedropper;
michael@0 133
michael@0 134 Eyedropper.prototype = {
michael@0 135 /**
michael@0 136 * Get the number of cells (blown-up pixels) per direction in the grid.
michael@0 137 */
michael@0 138 get cellsWide() {
michael@0 139 // Canvas will render whole "pixels" (cells) only, and an even
michael@0 140 // number at that. Round up to the nearest even number of pixels.
michael@0 141 let cellsWide = Math.ceil(this._zoomArea.width / this.zoom);
michael@0 142 cellsWide += cellsWide % 2;
michael@0 143
michael@0 144 return cellsWide;
michael@0 145 },
michael@0 146
michael@0 147 /**
michael@0 148 * Get the size of each cell (blown-up pixel) in the grid.
michael@0 149 */
michael@0 150 get cellSize() {
michael@0 151 return this._zoomArea.width / this.cellsWide;
michael@0 152 },
michael@0 153
michael@0 154 /**
michael@0 155 * Get index of cell in the center of the grid.
michael@0 156 */
michael@0 157 get centerCell() {
michael@0 158 return Math.floor(this.cellsWide / 2);
michael@0 159 },
michael@0 160
michael@0 161 /**
michael@0 162 * Get color of center cell in the grid.
michael@0 163 */
michael@0 164 get centerColor() {
michael@0 165 let x = y = (this.centerCell * this.cellSize) + (this.cellSize / 2);
michael@0 166 let rgb = this._ctx.getImageData(x, y, 1, 1).data;
michael@0 167 return rgb;
michael@0 168 },
michael@0 169
michael@0 170 /**
michael@0 171 * Start the eyedropper. Add listeners for a mouse move in the window to
michael@0 172 * show the eyedropper.
michael@0 173 */
michael@0 174 open: function() {
michael@0 175 if (this.isOpen) {
michael@0 176 // the eyedropper is aready open, don't create another panel.
michael@0 177 return;
michael@0 178 }
michael@0 179 this.isOpen = true;
michael@0 180
michael@0 181 this._OS = XULRuntime.OS;
michael@0 182
michael@0 183 this._chromeDocument.addEventListener("mousemove", this._onFirstMouseMove);
michael@0 184
michael@0 185 this._showCrosshairs();
michael@0 186 },
michael@0 187
michael@0 188 /**
michael@0 189 * Called on the first mouse move over the window. Opens the eyedropper
michael@0 190 * panel where the mouse is.
michael@0 191 */
michael@0 192 _onFirstMouseMove: function(event) {
michael@0 193 this._chromeDocument.removeEventListener("mousemove", this._onFirstMouseMove);
michael@0 194
michael@0 195 this._panel = this._buildPanel();
michael@0 196
michael@0 197 let popupSet = this._chromeDocument.querySelector("#mainPopupSet");
michael@0 198 popupSet.appendChild(this._panel);
michael@0 199
michael@0 200 let { panelX, panelY } = this._getPanelCoordinates(event);
michael@0 201 this._panel.openPopupAtScreen(panelX, panelY);
michael@0 202
michael@0 203 this._setCoordinates(event);
michael@0 204
michael@0 205 this._addListeners();
michael@0 206
michael@0 207 // hide cursor as we'll be showing the panel over the mouse instead.
michael@0 208 this._hideCrosshairs();
michael@0 209 this._hideCursor();
michael@0 210 },
michael@0 211
michael@0 212 /**
michael@0 213 * Set the current coordinates to inspect from where a mousemove originated.
michael@0 214 *
michael@0 215 * @param {MouseEvent} event
michael@0 216 * Event for the mouse move.
michael@0 217 */
michael@0 218 _setCoordinates: function(event) {
michael@0 219 let win = this._chromeWindow;
michael@0 220
michael@0 221 let x, y;
michael@0 222 if (this._OS == "Linux") {
michael@0 223 // event.clientX is off on Linux, so calculate it by hand
michael@0 224 let windowX = win.screenX + (win.outerWidth - win.innerWidth);
michael@0 225 x = event.screenX - windowX;
michael@0 226
michael@0 227 let windowY = win.screenY + (win.outerHeight - win.innerHeight);
michael@0 228 y = event.screenY - windowY;
michael@0 229 }
michael@0 230 else {
michael@0 231 x = event.clientX;
michael@0 232 y = event.clientY;
michael@0 233 }
michael@0 234
michael@0 235 // don't let it inspect outside the browser window
michael@0 236 x = Math.max(0, Math.min(x, win.outerWidth - 1));
michael@0 237 y = Math.max(0, Math.min(y, win.outerHeight - 1));
michael@0 238
michael@0 239 this._zoomArea.x = x;
michael@0 240 this._zoomArea.y = y;
michael@0 241 },
michael@0 242
michael@0 243 /**
michael@0 244 * Build and add a new eyedropper panel to the window.
michael@0 245 *
michael@0 246 * @return {Panel}
michael@0 247 * The XUL panel holding the eyedropper UI.
michael@0 248 */
michael@0 249 _buildPanel: function() {
michael@0 250 let panel = this._chromeDocument.createElement("panel");
michael@0 251 panel.setAttribute("noautofocus", true);
michael@0 252 panel.setAttribute("noautohide", true);
michael@0 253 panel.setAttribute("level", "floating");
michael@0 254 panel.setAttribute("class", "devtools-eyedropper-panel");
michael@0 255
michael@0 256 let iframe = this._iframe = this._chromeDocument.createElement("iframe");
michael@0 257 iframe.addEventListener("load", this._onFrameLoaded, true);
michael@0 258 iframe.setAttribute("flex", "1");
michael@0 259 iframe.setAttribute("transparent", "transparent");
michael@0 260 iframe.setAttribute("allowTransparency", true);
michael@0 261 iframe.setAttribute("class", "devtools-eyedropper-iframe");
michael@0 262 iframe.setAttribute("src", EYEDROPPER_URL);
michael@0 263 iframe.setAttribute("width", CANVAS_WIDTH);
michael@0 264 iframe.setAttribute("height", CANVAS_WIDTH);
michael@0 265
michael@0 266 panel.appendChild(iframe);
michael@0 267
michael@0 268 return panel;
michael@0 269 },
michael@0 270
michael@0 271 /**
michael@0 272 * Event handler for the panel's iframe's load event. Emits
michael@0 273 * a "load" event from this eyedropper object.
michael@0 274 */
michael@0 275 _onFrameLoaded: function() {
michael@0 276 this._iframe.removeEventListener("load", this._onFrameLoaded, true);
michael@0 277
michael@0 278 this._iframeDocument = this._iframe.contentDocument;
michael@0 279 this._colorPreview = this._iframeDocument.querySelector("#color-preview");
michael@0 280 this._colorValue = this._iframeDocument.querySelector("#color-value");
michael@0 281
michael@0 282 // value box will be too long for hex values and too short for hsl
michael@0 283 let valueBox = this._iframeDocument.querySelector("#color-value-box");
michael@0 284 if (this.format == "hex") {
michael@0 285 valueBox.style.width = HEX_BOX_WIDTH + "px";
michael@0 286 }
michael@0 287 else if (this.format == "hsl") {
michael@0 288 valueBox.style.width = HSL_BOX_WIDTH + "px";
michael@0 289 }
michael@0 290
michael@0 291 this._canvas = this._iframeDocument.querySelector("#canvas");
michael@0 292 this._ctx = this._canvas.getContext("2d");
michael@0 293
michael@0 294 // so we preserve the clear pixel boundaries
michael@0 295 this._ctx.mozImageSmoothingEnabled = false;
michael@0 296
michael@0 297 this._drawWindow();
michael@0 298
michael@0 299 this._addPanelListeners();
michael@0 300 this._iframe.focus();
michael@0 301
michael@0 302 this.loaded = true;
michael@0 303 this.emit("load");
michael@0 304 },
michael@0 305
michael@0 306 /**
michael@0 307 * Add key listeners to the panel.
michael@0 308 */
michael@0 309 _addPanelListeners: function() {
michael@0 310 this._iframeDocument.addEventListener("keydown", this._onKeyDown);
michael@0 311
michael@0 312 let closeCmd = this._iframeDocument.getElementById("eyedropper-cmd-close");
michael@0 313 closeCmd.addEventListener("command", this.destroy.bind(this), true);
michael@0 314
michael@0 315 let copyCmd = this._iframeDocument.getElementById("eyedropper-cmd-copy");
michael@0 316 copyCmd.addEventListener("command", this.selectColor.bind(this), true);
michael@0 317 },
michael@0 318
michael@0 319 /**
michael@0 320 * Remove listeners from the panel.
michael@0 321 */
michael@0 322 _removePanelListeners: function() {
michael@0 323 this._iframeDocument.removeEventListener("keydown", this._onKeyDown);
michael@0 324 },
michael@0 325
michael@0 326 /**
michael@0 327 * Add mouse event listeners to the document we're inspecting.
michael@0 328 */
michael@0 329 _addListeners: function() {
michael@0 330 this._chromeDocument.addEventListener("mousemove", this._onMouseMove);
michael@0 331 this._chromeDocument.addEventListener("mousedown", this._onMouseDown);
michael@0 332 },
michael@0 333
michael@0 334 /**
michael@0 335 * Remove mouse event listeners from the document we're inspecting.
michael@0 336 */
michael@0 337 _removeListeners: function() {
michael@0 338 this._chromeDocument.removeEventListener("mousemove", this._onFirstMouseMove);
michael@0 339 this._chromeDocument.removeEventListener("mousemove", this._onMouseMove);
michael@0 340 this._chromeDocument.removeEventListener("mousedown", this._onMouseDown);
michael@0 341 },
michael@0 342
michael@0 343 /**
michael@0 344 * Hide the cursor.
michael@0 345 */
michael@0 346 _hideCursor: function() {
michael@0 347 registerStyleSheet(NOCURSOR_URL);
michael@0 348 },
michael@0 349
michael@0 350 /**
michael@0 351 * Reset the cursor back to default.
michael@0 352 */
michael@0 353 _resetCursor: function() {
michael@0 354 unregisterStyleSheet(NOCURSOR_URL);
michael@0 355 },
michael@0 356
michael@0 357 /**
michael@0 358 * Show a crosshairs as the mouse cursor
michael@0 359 */
michael@0 360 _showCrosshairs: function() {
michael@0 361 registerStyleSheet(CROSSHAIRS_URL);
michael@0 362 },
michael@0 363
michael@0 364 /**
michael@0 365 * Reset cursor.
michael@0 366 */
michael@0 367 _hideCrosshairs: function() {
michael@0 368 unregisterStyleSheet(CROSSHAIRS_URL);
michael@0 369 },
michael@0 370
michael@0 371 /**
michael@0 372 * Event handler for a mouse move over the page we're inspecting.
michael@0 373 * Preview the area under the cursor, and move panel to be under the cursor.
michael@0 374 *
michael@0 375 * @param {DOMEvent} event
michael@0 376 * MouseEvent for the mouse moving
michael@0 377 */
michael@0 378 _onMouseMove: function(event) {
michael@0 379 if (!this._dragging || !this._panel || !this._canvas) {
michael@0 380 return;
michael@0 381 }
michael@0 382
michael@0 383 if (this._OS == "Linux" && ++this._mouseMoveCounter % 2 == 0) {
michael@0 384 // skip every other mousemove to preserve performance.
michael@0 385 return;
michael@0 386 }
michael@0 387
michael@0 388 this._setCoordinates(event);
michael@0 389 this._drawWindow();
michael@0 390
michael@0 391 let { panelX, panelY } = this._getPanelCoordinates(event);
michael@0 392 this._movePanel(panelX, panelY);
michael@0 393 },
michael@0 394
michael@0 395 /**
michael@0 396 * Get coordinates of where the eyedropper panel should go based on
michael@0 397 * the current coordinates of the mouse cursor.
michael@0 398 *
michael@0 399 * @param {MouseEvent} event
michael@0 400 * object with properties 'screenX' and 'screenY'
michael@0 401 *
michael@0 402 * @return {object}
michael@0 403 * object with properties 'panelX', 'panelY'
michael@0 404 */
michael@0 405 _getPanelCoordinates: function({screenX, screenY}) {
michael@0 406 let win = this._chromeWindow;
michael@0 407 let offset = CANVAS_WIDTH / 2 + CANVAS_OFFSET;
michael@0 408
michael@0 409 let panelX = screenX - offset;
michael@0 410 let windowX = win.screenX + (win.outerWidth - win.innerWidth);
michael@0 411 let maxX = win.screenX + win.outerWidth - offset - 1;
michael@0 412
michael@0 413 let panelY = screenY - offset;
michael@0 414 let windowY = win.screenY + (win.outerHeight - win.innerHeight);
michael@0 415 let maxY = win.screenY + win.outerHeight - offset - 1;
michael@0 416
michael@0 417 // don't let the panel move outside the browser window
michael@0 418 panelX = Math.max(windowX - offset, Math.min(panelX, maxX));
michael@0 419 panelY = Math.max(windowY - offset, Math.min(panelY, maxY));
michael@0 420
michael@0 421 return { panelX: panelX, panelY: panelY };
michael@0 422 },
michael@0 423
michael@0 424 /**
michael@0 425 * Move the eyedropper panel to the given coordinates.
michael@0 426 *
michael@0 427 * @param {number} screenX
michael@0 428 * left coordinate on the screen
michael@0 429 * @param {number} screenY
michael@0 430 * top coordinate
michael@0 431 */
michael@0 432 _movePanel: function(screenX, screenY) {
michael@0 433 this._panelX = screenX;
michael@0 434 this._panelY = screenY;
michael@0 435
michael@0 436 this._panel.moveTo(screenX, screenY);
michael@0 437 },
michael@0 438
michael@0 439 /**
michael@0 440 * Handler for the mouse down event on the inspected page. This means a
michael@0 441 * click, so we'll select the color that's currently hovered.
michael@0 442 *
michael@0 443 * @param {Event} event
michael@0 444 * DOM MouseEvent object
michael@0 445 */
michael@0 446 _onMouseDown: function(event) {
michael@0 447 event.preventDefault();
michael@0 448 event.stopPropagation();
michael@0 449
michael@0 450 this.selectColor();
michael@0 451 },
michael@0 452
michael@0 453 /**
michael@0 454 * Select the current color that's being previewed. Fire a
michael@0 455 * "select" event with the color as an rgb string.
michael@0 456 */
michael@0 457 selectColor: function() {
michael@0 458 if (this._isSelecting) {
michael@0 459 return;
michael@0 460 }
michael@0 461 this._isSelecting = true;
michael@0 462 this._dragging = false;
michael@0 463
michael@0 464 this.emit("select", this._colorValue.value);
michael@0 465
michael@0 466 if (this.copyOnSelect) {
michael@0 467 this.copyColor(this.destroy.bind(this));
michael@0 468 }
michael@0 469 else {
michael@0 470 this.destroy();
michael@0 471 }
michael@0 472 },
michael@0 473
michael@0 474 /**
michael@0 475 * Copy the currently inspected color to the clipboard.
michael@0 476 *
michael@0 477 * @param {Function} callback
michael@0 478 * Callback to be called when the color is in the clipboard.
michael@0 479 */
michael@0 480 copyColor: function(callback) {
michael@0 481 Services.appShell.hiddenDOMWindow.clearTimeout(this._copyTimeout);
michael@0 482
michael@0 483 let color = this._colorValue.value;
michael@0 484 clipboardHelper.copyString(color);
michael@0 485
michael@0 486 this._colorValue.classList.add("highlight");
michael@0 487 this._colorValue.value = "✓ " + l10n.GetStringFromName("colorValue.copied");
michael@0 488
michael@0 489 this._copyTimeout = Services.appShell.hiddenDOMWindow.setTimeout(() => {
michael@0 490 this._colorValue.classList.remove("highlight");
michael@0 491 this._colorValue.value = color;
michael@0 492
michael@0 493 if (callback) {
michael@0 494 callback();
michael@0 495 }
michael@0 496 }, CLOSE_DELAY);
michael@0 497 },
michael@0 498
michael@0 499 /**
michael@0 500 * Handler for the keydown event on the panel. Either copy the color
michael@0 501 * or move the panel in a direction depending on the key pressed.
michael@0 502 *
michael@0 503 * @param {Event} event
michael@0 504 * DOM KeyboardEvent object
michael@0 505 */
michael@0 506 _onKeyDown: function(event) {
michael@0 507 if (event.metaKey && event.keyCode === event.DOM_VK_C) {
michael@0 508 this.copyColor();
michael@0 509 return;
michael@0 510 }
michael@0 511
michael@0 512 let offsetX = 0;
michael@0 513 let offsetY = 0;
michael@0 514 let modifier = 1;
michael@0 515
michael@0 516 if (event.keyCode === event.DOM_VK_LEFT) {
michael@0 517 offsetX = -1;
michael@0 518 }
michael@0 519 if (event.keyCode === event.DOM_VK_RIGHT) {
michael@0 520 offsetX = 1;
michael@0 521 }
michael@0 522 if (event.keyCode === event.DOM_VK_UP) {
michael@0 523 offsetY = -1;
michael@0 524 }
michael@0 525 if (event.keyCode === event.DOM_VK_DOWN) {
michael@0 526 offsetY = 1;
michael@0 527 }
michael@0 528 if (event.shiftKey) {
michael@0 529 modifier = 10;
michael@0 530 }
michael@0 531
michael@0 532 offsetY *= modifier;
michael@0 533 offsetX *= modifier;
michael@0 534
michael@0 535 if (offsetX !== 0 || offsetY !== 0) {
michael@0 536 this._zoomArea.x += offsetX;
michael@0 537 this._zoomArea.y += offsetY;
michael@0 538
michael@0 539 this._drawWindow();
michael@0 540
michael@0 541 this._movePanel(this._panelX + offsetX, this._panelY + offsetY);
michael@0 542
michael@0 543 event.preventDefault();
michael@0 544 }
michael@0 545 },
michael@0 546
michael@0 547 /**
michael@0 548 * Draw the inspected area onto the canvas using the zoom level.
michael@0 549 */
michael@0 550 _drawWindow: function() {
michael@0 551 let { width, height, x, y } = this._zoomArea;
michael@0 552
michael@0 553 let zoomedWidth = width / this.zoom;
michael@0 554 let zoomedHeight = height / this.zoom;
michael@0 555
michael@0 556 let drawX = x - (zoomedWidth / 2);
michael@0 557 let drawY = y - (zoomedHeight / 2);
michael@0 558
michael@0 559 // draw the portion of the window we're inspecting
michael@0 560 this._ctx.drawWindow(this._chromeWindow, drawX, drawY, zoomedWidth,
michael@0 561 zoomedHeight, "white");
michael@0 562
michael@0 563 // now scale it
michael@0 564 let sx = 0;
michael@0 565 let sy = 0;
michael@0 566 let sw = zoomedWidth;
michael@0 567 let sh = zoomedHeight;
michael@0 568 let dx = 0;
michael@0 569 let dy = 0;
michael@0 570 let dw = width;
michael@0 571 let dh = height;
michael@0 572
michael@0 573 this._ctx.drawImage(this._canvas, sx, sy, sw, sh, dx, dy, dw, dh);
michael@0 574
michael@0 575 let rgb = this.centerColor;
michael@0 576 this._colorPreview.style.backgroundColor = toColorString(rgb, "rgb");
michael@0 577 this._colorValue.value = toColorString(rgb, this.format);
michael@0 578
michael@0 579 if (this.zoom > 2) {
michael@0 580 // grid at 2x is too busy
michael@0 581 this._drawGrid();
michael@0 582 }
michael@0 583 this._drawCrosshair();
michael@0 584 },
michael@0 585
michael@0 586 /**
michael@0 587 * Draw a grid on the canvas representing pixel boundaries.
michael@0 588 */
michael@0 589 _drawGrid: function() {
michael@0 590 let { width, height } = this._zoomArea;
michael@0 591
michael@0 592 this._ctx.lineWidth = 1;
michael@0 593 this._ctx.strokeStyle = "rgba(143, 143, 143, 0.2)";
michael@0 594
michael@0 595 for (let i = 0; i < width; i += this.cellSize) {
michael@0 596 this._ctx.beginPath();
michael@0 597 this._ctx.moveTo(i - .5, 0);
michael@0 598 this._ctx.lineTo(i - .5, height);
michael@0 599 this._ctx.stroke();
michael@0 600
michael@0 601 this._ctx.beginPath();
michael@0 602 this._ctx.moveTo(0, i - .5);
michael@0 603 this._ctx.lineTo(width, i - .5);
michael@0 604 this._ctx.stroke();
michael@0 605 }
michael@0 606 },
michael@0 607
michael@0 608 /**
michael@0 609 * Draw a box on the canvas to highlight the center cell.
michael@0 610 */
michael@0 611 _drawCrosshair: function() {
michael@0 612 let x = y = this.centerCell * this.cellSize;
michael@0 613
michael@0 614 this._ctx.lineWidth = 1;
michael@0 615 this._ctx.lineJoin = 'miter';
michael@0 616 this._ctx.strokeStyle = "rgba(0, 0, 0, 1)";
michael@0 617 this._ctx.strokeRect(x - 1.5, y - 1.5, this.cellSize + 2, this.cellSize + 2);
michael@0 618
michael@0 619 this._ctx.strokeStyle = "rgba(255, 255, 255, 1)";
michael@0 620 this._ctx.strokeRect(x - 0.5, y - 0.5, this.cellSize, this.cellSize);
michael@0 621 },
michael@0 622
michael@0 623 /**
michael@0 624 * Destroy the eyedropper and clean up. Emits a "destroy" event.
michael@0 625 */
michael@0 626 destroy: function() {
michael@0 627 this._resetCursor();
michael@0 628 this._hideCrosshairs();
michael@0 629
michael@0 630 if (this._panel) {
michael@0 631 this._panel.hidePopup();
michael@0 632 this._panel.remove();
michael@0 633 this._panel = null;
michael@0 634 }
michael@0 635 this._removePanelListeners();
michael@0 636 this._removeListeners();
michael@0 637
michael@0 638 this.isOpen = false;
michael@0 639 this._isSelecting = false;
michael@0 640
michael@0 641 this.emit("destroy");
michael@0 642 }
michael@0 643 }
michael@0 644
michael@0 645 /**
michael@0 646 * Add a user style sheet that applies to all documents.
michael@0 647 */
michael@0 648 function registerStyleSheet(url) {
michael@0 649 var uri = ioService.newURI(url, null, null);
michael@0 650 if (!ssService.sheetRegistered(uri, ssService.AGENT_SHEET)) {
michael@0 651 ssService.loadAndRegisterSheet(uri, ssService.AGENT_SHEET);
michael@0 652 }
michael@0 653 }
michael@0 654
michael@0 655 /**
michael@0 656 * Remove a user style sheet.
michael@0 657 */
michael@0 658 function unregisterStyleSheet(url) {
michael@0 659 var uri = ioService.newURI(url, null, null);
michael@0 660 if (ssService.sheetRegistered(uri, ssService.AGENT_SHEET)) {
michael@0 661 ssService.unregisterSheet(uri, ssService.AGENT_SHEET);
michael@0 662 }
michael@0 663 }
michael@0 664
michael@0 665 /**
michael@0 666 * Get a formatted CSS color string from a color value.
michael@0 667 *
michael@0 668 * @param {array} rgb
michael@0 669 * Rgb values of a color to format
michael@0 670 * @param {string} format
michael@0 671 * Format of string. One of "hex", "rgb", "hsl", "name"
michael@0 672 *
michael@0 673 * @return {string}
michael@0 674 * Formatted color value, e.g. "#FFF" or "hsl(20, 10%, 10%)"
michael@0 675 */
michael@0 676 function toColorString(rgb, format) {
michael@0 677 let [r,g,b] = rgb;
michael@0 678
michael@0 679 switch(format) {
michael@0 680 case "hex":
michael@0 681 return hexString(rgb);
michael@0 682 case "rgb":
michael@0 683 return "rgb(" + r + ", " + g + ", " + b + ")";
michael@0 684 case "hsl":
michael@0 685 let [h,s,l] = rgbToHsl(rgb);
michael@0 686 return "hsl(" + h + ", " + s + "%, " + l + "%)";
michael@0 687 case "name":
michael@0 688 let str;
michael@0 689 try {
michael@0 690 str = DOMUtils.rgbToColorName(r, g, b);
michael@0 691 } catch(e) {
michael@0 692 str = hexString(rgb);
michael@0 693 }
michael@0 694 return str;
michael@0 695 default:
michael@0 696 return hexString(rgb);
michael@0 697 }
michael@0 698 }
michael@0 699
michael@0 700 /**
michael@0 701 * Produce a hex-formatted color string from rgb values.
michael@0 702 *
michael@0 703 * @param {array} rgb
michael@0 704 * Rgb values of color to stringify
michael@0 705 *
michael@0 706 * @return {string}
michael@0 707 * Hex formatted string for color, e.g. "#FFEE00"
michael@0 708 */
michael@0 709 function hexString([r,g,b]) {
michael@0 710 let val = (1 << 24) + (r << 16) + (g << 8) + (b << 0);
michael@0 711 return "#" + val.toString(16).substr(-6).toUpperCase();
michael@0 712 }

mercurial