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