Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
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 | } |