michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: const {Cc, Ci, Cu} = require("chrome"); michael@0: const {rgbToHsl} = require("devtools/css-color").colorUtils; michael@0: const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js"); michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: michael@0: loader.lazyGetter(this, "clipboardHelper", function() { michael@0: return Cc["@mozilla.org/widget/clipboardhelper;1"] michael@0: .getService(Ci.nsIClipboardHelper); michael@0: }); michael@0: michael@0: loader.lazyGetter(this, "ssService", function() { michael@0: return Cc["@mozilla.org/content/style-sheet-service;1"] michael@0: .getService(Ci.nsIStyleSheetService); michael@0: }); michael@0: michael@0: loader.lazyGetter(this, "ioService", function() { michael@0: return Cc["@mozilla.org/network/io-service;1"] michael@0: .getService(Ci.nsIIOService); michael@0: }); michael@0: michael@0: loader.lazyGetter(this, "DOMUtils", function () { michael@0: return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); michael@0: }); michael@0: michael@0: loader.lazyGetter(this, "XULRuntime", function() { michael@0: return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime); michael@0: }); michael@0: michael@0: loader.lazyGetter(this, "l10n", () => Services.strings michael@0: .createBundle("chrome://browser/locale/devtools/eyedropper.properties")); michael@0: michael@0: const EYEDROPPER_URL = "chrome://browser/content/devtools/eyedropper.xul"; michael@0: const CROSSHAIRS_URL = "chrome://browser/content/devtools/eyedropper/crosshairs.css"; michael@0: const NOCURSOR_URL = "chrome://browser/content/devtools/eyedropper/nocursor.css"; michael@0: michael@0: const ZOOM_PREF = "devtools.eyedropper.zoom"; michael@0: const FORMAT_PREF = "devtools.defaultColorUnit"; michael@0: michael@0: const CANVAS_WIDTH = 96; michael@0: const CANVAS_OFFSET = 3; // equals the border width of the canvas. michael@0: const CLOSE_DELAY = 750; michael@0: michael@0: const HEX_BOX_WIDTH = CANVAS_WIDTH + CANVAS_OFFSET * 2; michael@0: const HSL_BOX_WIDTH = 158; michael@0: michael@0: /** michael@0: * Manage instances of eyedroppers for windows. Registering here isn't michael@0: * necessary for creating an eyedropper, but can be used for testing. michael@0: */ michael@0: let EyedropperManager = { michael@0: _instances: new WeakMap(), michael@0: michael@0: getInstance: function(chromeWindow) { michael@0: return this._instances.get(chromeWindow); michael@0: }, michael@0: michael@0: createInstance: function(chromeWindow) { michael@0: let dropper = this.getInstance(chromeWindow); michael@0: if (dropper) { michael@0: return dropper; michael@0: } michael@0: michael@0: dropper = new Eyedropper(chromeWindow); michael@0: this._instances.set(chromeWindow, dropper); michael@0: michael@0: dropper.on("destroy", () => { michael@0: this.deleteInstance(chromeWindow); michael@0: }); michael@0: michael@0: return dropper; michael@0: }, michael@0: michael@0: deleteInstance: function(chromeWindow) { michael@0: this._instances.delete(chromeWindow); michael@0: } michael@0: } michael@0: michael@0: exports.EyedropperManager = EyedropperManager; michael@0: michael@0: /** michael@0: * Eyedropper widget. Once opened, shows zoomed area above current pixel and michael@0: * displays the color value of the center pixel. Clicking on the window will michael@0: * close the widget and fire a 'select' event. If 'copyOnSelect' is true, the color michael@0: * will also be copied to the clipboard. michael@0: * michael@0: * let eyedropper = new Eyedropper(window); michael@0: * eyedropper.open(); michael@0: * michael@0: * eyedropper.once("select", (ev, color) => { michael@0: * console.log(color); // "rgb(20, 50, 230)" michael@0: * }) michael@0: * michael@0: * @param {DOMWindow} chromeWindow michael@0: * window to inspect michael@0: * @param {object} opts michael@0: * optional options object, with 'copyOnSelect' michael@0: */ michael@0: function Eyedropper(chromeWindow, opts = { copyOnSelect: true }) { michael@0: this.copyOnSelect = opts.copyOnSelect; michael@0: michael@0: this._onFirstMouseMove = this._onFirstMouseMove.bind(this); michael@0: this._onMouseMove = this._onMouseMove.bind(this); michael@0: this._onMouseDown = this._onMouseDown.bind(this); michael@0: this._onKeyDown = this._onKeyDown.bind(this); michael@0: this._onFrameLoaded = this._onFrameLoaded.bind(this); michael@0: michael@0: this._chromeWindow = chromeWindow; michael@0: this._chromeDocument = chromeWindow.document; michael@0: michael@0: this._dragging = true; michael@0: this.loaded = false; michael@0: michael@0: this._mouseMoveCounter = 0; michael@0: michael@0: this.format = Services.prefs.getCharPref(FORMAT_PREF); // color value format michael@0: this.zoom = Services.prefs.getIntPref(ZOOM_PREF); // zoom level - integer michael@0: michael@0: this._zoomArea = { michael@0: x: 0, // the left coordinate of the center of the inspected region michael@0: y: 0, // the top coordinate of the center of the inspected region michael@0: width: CANVAS_WIDTH, // width of canvas to draw zoomed area onto michael@0: height: CANVAS_WIDTH // height of canvas michael@0: }; michael@0: EventEmitter.decorate(this); michael@0: } michael@0: michael@0: exports.Eyedropper = Eyedropper; michael@0: michael@0: Eyedropper.prototype = { michael@0: /** michael@0: * Get the number of cells (blown-up pixels) per direction in the grid. michael@0: */ michael@0: get cellsWide() { michael@0: // Canvas will render whole "pixels" (cells) only, and an even michael@0: // number at that. Round up to the nearest even number of pixels. michael@0: let cellsWide = Math.ceil(this._zoomArea.width / this.zoom); michael@0: cellsWide += cellsWide % 2; michael@0: michael@0: return cellsWide; michael@0: }, michael@0: michael@0: /** michael@0: * Get the size of each cell (blown-up pixel) in the grid. michael@0: */ michael@0: get cellSize() { michael@0: return this._zoomArea.width / this.cellsWide; michael@0: }, michael@0: michael@0: /** michael@0: * Get index of cell in the center of the grid. michael@0: */ michael@0: get centerCell() { michael@0: return Math.floor(this.cellsWide / 2); michael@0: }, michael@0: michael@0: /** michael@0: * Get color of center cell in the grid. michael@0: */ michael@0: get centerColor() { michael@0: let x = y = (this.centerCell * this.cellSize) + (this.cellSize / 2); michael@0: let rgb = this._ctx.getImageData(x, y, 1, 1).data; michael@0: return rgb; michael@0: }, michael@0: michael@0: /** michael@0: * Start the eyedropper. Add listeners for a mouse move in the window to michael@0: * show the eyedropper. michael@0: */ michael@0: open: function() { michael@0: if (this.isOpen) { michael@0: // the eyedropper is aready open, don't create another panel. michael@0: return; michael@0: } michael@0: this.isOpen = true; michael@0: michael@0: this._OS = XULRuntime.OS; michael@0: michael@0: this._chromeDocument.addEventListener("mousemove", this._onFirstMouseMove); michael@0: michael@0: this._showCrosshairs(); michael@0: }, michael@0: michael@0: /** michael@0: * Called on the first mouse move over the window. Opens the eyedropper michael@0: * panel where the mouse is. michael@0: */ michael@0: _onFirstMouseMove: function(event) { michael@0: this._chromeDocument.removeEventListener("mousemove", this._onFirstMouseMove); michael@0: michael@0: this._panel = this._buildPanel(); michael@0: michael@0: let popupSet = this._chromeDocument.querySelector("#mainPopupSet"); michael@0: popupSet.appendChild(this._panel); michael@0: michael@0: let { panelX, panelY } = this._getPanelCoordinates(event); michael@0: this._panel.openPopupAtScreen(panelX, panelY); michael@0: michael@0: this._setCoordinates(event); michael@0: michael@0: this._addListeners(); michael@0: michael@0: // hide cursor as we'll be showing the panel over the mouse instead. michael@0: this._hideCrosshairs(); michael@0: this._hideCursor(); michael@0: }, michael@0: michael@0: /** michael@0: * Set the current coordinates to inspect from where a mousemove originated. michael@0: * michael@0: * @param {MouseEvent} event michael@0: * Event for the mouse move. michael@0: */ michael@0: _setCoordinates: function(event) { michael@0: let win = this._chromeWindow; michael@0: michael@0: let x, y; michael@0: if (this._OS == "Linux") { michael@0: // event.clientX is off on Linux, so calculate it by hand michael@0: let windowX = win.screenX + (win.outerWidth - win.innerWidth); michael@0: x = event.screenX - windowX; michael@0: michael@0: let windowY = win.screenY + (win.outerHeight - win.innerHeight); michael@0: y = event.screenY - windowY; michael@0: } michael@0: else { michael@0: x = event.clientX; michael@0: y = event.clientY; michael@0: } michael@0: michael@0: // don't let it inspect outside the browser window michael@0: x = Math.max(0, Math.min(x, win.outerWidth - 1)); michael@0: y = Math.max(0, Math.min(y, win.outerHeight - 1)); michael@0: michael@0: this._zoomArea.x = x; michael@0: this._zoomArea.y = y; michael@0: }, michael@0: michael@0: /** michael@0: * Build and add a new eyedropper panel to the window. michael@0: * michael@0: * @return {Panel} michael@0: * The XUL panel holding the eyedropper UI. michael@0: */ michael@0: _buildPanel: function() { michael@0: let panel = this._chromeDocument.createElement("panel"); michael@0: panel.setAttribute("noautofocus", true); michael@0: panel.setAttribute("noautohide", true); michael@0: panel.setAttribute("level", "floating"); michael@0: panel.setAttribute("class", "devtools-eyedropper-panel"); michael@0: michael@0: let iframe = this._iframe = this._chromeDocument.createElement("iframe"); michael@0: iframe.addEventListener("load", this._onFrameLoaded, true); michael@0: iframe.setAttribute("flex", "1"); michael@0: iframe.setAttribute("transparent", "transparent"); michael@0: iframe.setAttribute("allowTransparency", true); michael@0: iframe.setAttribute("class", "devtools-eyedropper-iframe"); michael@0: iframe.setAttribute("src", EYEDROPPER_URL); michael@0: iframe.setAttribute("width", CANVAS_WIDTH); michael@0: iframe.setAttribute("height", CANVAS_WIDTH); michael@0: michael@0: panel.appendChild(iframe); michael@0: michael@0: return panel; michael@0: }, michael@0: michael@0: /** michael@0: * Event handler for the panel's iframe's load event. Emits michael@0: * a "load" event from this eyedropper object. michael@0: */ michael@0: _onFrameLoaded: function() { michael@0: this._iframe.removeEventListener("load", this._onFrameLoaded, true); michael@0: michael@0: this._iframeDocument = this._iframe.contentDocument; michael@0: this._colorPreview = this._iframeDocument.querySelector("#color-preview"); michael@0: this._colorValue = this._iframeDocument.querySelector("#color-value"); michael@0: michael@0: // value box will be too long for hex values and too short for hsl michael@0: let valueBox = this._iframeDocument.querySelector("#color-value-box"); michael@0: if (this.format == "hex") { michael@0: valueBox.style.width = HEX_BOX_WIDTH + "px"; michael@0: } michael@0: else if (this.format == "hsl") { michael@0: valueBox.style.width = HSL_BOX_WIDTH + "px"; michael@0: } michael@0: michael@0: this._canvas = this._iframeDocument.querySelector("#canvas"); michael@0: this._ctx = this._canvas.getContext("2d"); michael@0: michael@0: // so we preserve the clear pixel boundaries michael@0: this._ctx.mozImageSmoothingEnabled = false; michael@0: michael@0: this._drawWindow(); michael@0: michael@0: this._addPanelListeners(); michael@0: this._iframe.focus(); michael@0: michael@0: this.loaded = true; michael@0: this.emit("load"); michael@0: }, michael@0: michael@0: /** michael@0: * Add key listeners to the panel. michael@0: */ michael@0: _addPanelListeners: function() { michael@0: this._iframeDocument.addEventListener("keydown", this._onKeyDown); michael@0: michael@0: let closeCmd = this._iframeDocument.getElementById("eyedropper-cmd-close"); michael@0: closeCmd.addEventListener("command", this.destroy.bind(this), true); michael@0: michael@0: let copyCmd = this._iframeDocument.getElementById("eyedropper-cmd-copy"); michael@0: copyCmd.addEventListener("command", this.selectColor.bind(this), true); michael@0: }, michael@0: michael@0: /** michael@0: * Remove listeners from the panel. michael@0: */ michael@0: _removePanelListeners: function() { michael@0: this._iframeDocument.removeEventListener("keydown", this._onKeyDown); michael@0: }, michael@0: michael@0: /** michael@0: * Add mouse event listeners to the document we're inspecting. michael@0: */ michael@0: _addListeners: function() { michael@0: this._chromeDocument.addEventListener("mousemove", this._onMouseMove); michael@0: this._chromeDocument.addEventListener("mousedown", this._onMouseDown); michael@0: }, michael@0: michael@0: /** michael@0: * Remove mouse event listeners from the document we're inspecting. michael@0: */ michael@0: _removeListeners: function() { michael@0: this._chromeDocument.removeEventListener("mousemove", this._onFirstMouseMove); michael@0: this._chromeDocument.removeEventListener("mousemove", this._onMouseMove); michael@0: this._chromeDocument.removeEventListener("mousedown", this._onMouseDown); michael@0: }, michael@0: michael@0: /** michael@0: * Hide the cursor. michael@0: */ michael@0: _hideCursor: function() { michael@0: registerStyleSheet(NOCURSOR_URL); michael@0: }, michael@0: michael@0: /** michael@0: * Reset the cursor back to default. michael@0: */ michael@0: _resetCursor: function() { michael@0: unregisterStyleSheet(NOCURSOR_URL); michael@0: }, michael@0: michael@0: /** michael@0: * Show a crosshairs as the mouse cursor michael@0: */ michael@0: _showCrosshairs: function() { michael@0: registerStyleSheet(CROSSHAIRS_URL); michael@0: }, michael@0: michael@0: /** michael@0: * Reset cursor. michael@0: */ michael@0: _hideCrosshairs: function() { michael@0: unregisterStyleSheet(CROSSHAIRS_URL); michael@0: }, michael@0: michael@0: /** michael@0: * Event handler for a mouse move over the page we're inspecting. michael@0: * Preview the area under the cursor, and move panel to be under the cursor. michael@0: * michael@0: * @param {DOMEvent} event michael@0: * MouseEvent for the mouse moving michael@0: */ michael@0: _onMouseMove: function(event) { michael@0: if (!this._dragging || !this._panel || !this._canvas) { michael@0: return; michael@0: } michael@0: michael@0: if (this._OS == "Linux" && ++this._mouseMoveCounter % 2 == 0) { michael@0: // skip every other mousemove to preserve performance. michael@0: return; michael@0: } michael@0: michael@0: this._setCoordinates(event); michael@0: this._drawWindow(); michael@0: michael@0: let { panelX, panelY } = this._getPanelCoordinates(event); michael@0: this._movePanel(panelX, panelY); michael@0: }, michael@0: michael@0: /** michael@0: * Get coordinates of where the eyedropper panel should go based on michael@0: * the current coordinates of the mouse cursor. michael@0: * michael@0: * @param {MouseEvent} event michael@0: * object with properties 'screenX' and 'screenY' michael@0: * michael@0: * @return {object} michael@0: * object with properties 'panelX', 'panelY' michael@0: */ michael@0: _getPanelCoordinates: function({screenX, screenY}) { michael@0: let win = this._chromeWindow; michael@0: let offset = CANVAS_WIDTH / 2 + CANVAS_OFFSET; michael@0: michael@0: let panelX = screenX - offset; michael@0: let windowX = win.screenX + (win.outerWidth - win.innerWidth); michael@0: let maxX = win.screenX + win.outerWidth - offset - 1; michael@0: michael@0: let panelY = screenY - offset; michael@0: let windowY = win.screenY + (win.outerHeight - win.innerHeight); michael@0: let maxY = win.screenY + win.outerHeight - offset - 1; michael@0: michael@0: // don't let the panel move outside the browser window michael@0: panelX = Math.max(windowX - offset, Math.min(panelX, maxX)); michael@0: panelY = Math.max(windowY - offset, Math.min(panelY, maxY)); michael@0: michael@0: return { panelX: panelX, panelY: panelY }; michael@0: }, michael@0: michael@0: /** michael@0: * Move the eyedropper panel to the given coordinates. michael@0: * michael@0: * @param {number} screenX michael@0: * left coordinate on the screen michael@0: * @param {number} screenY michael@0: * top coordinate michael@0: */ michael@0: _movePanel: function(screenX, screenY) { michael@0: this._panelX = screenX; michael@0: this._panelY = screenY; michael@0: michael@0: this._panel.moveTo(screenX, screenY); michael@0: }, michael@0: michael@0: /** michael@0: * Handler for the mouse down event on the inspected page. This means a michael@0: * click, so we'll select the color that's currently hovered. michael@0: * michael@0: * @param {Event} event michael@0: * DOM MouseEvent object michael@0: */ michael@0: _onMouseDown: function(event) { michael@0: event.preventDefault(); michael@0: event.stopPropagation(); michael@0: michael@0: this.selectColor(); michael@0: }, michael@0: michael@0: /** michael@0: * Select the current color that's being previewed. Fire a michael@0: * "select" event with the color as an rgb string. michael@0: */ michael@0: selectColor: function() { michael@0: if (this._isSelecting) { michael@0: return; michael@0: } michael@0: this._isSelecting = true; michael@0: this._dragging = false; michael@0: michael@0: this.emit("select", this._colorValue.value); michael@0: michael@0: if (this.copyOnSelect) { michael@0: this.copyColor(this.destroy.bind(this)); michael@0: } michael@0: else { michael@0: this.destroy(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Copy the currently inspected color to the clipboard. michael@0: * michael@0: * @param {Function} callback michael@0: * Callback to be called when the color is in the clipboard. michael@0: */ michael@0: copyColor: function(callback) { michael@0: Services.appShell.hiddenDOMWindow.clearTimeout(this._copyTimeout); michael@0: michael@0: let color = this._colorValue.value; michael@0: clipboardHelper.copyString(color); michael@0: michael@0: this._colorValue.classList.add("highlight"); michael@0: this._colorValue.value = "✓ " + l10n.GetStringFromName("colorValue.copied"); michael@0: michael@0: this._copyTimeout = Services.appShell.hiddenDOMWindow.setTimeout(() => { michael@0: this._colorValue.classList.remove("highlight"); michael@0: this._colorValue.value = color; michael@0: michael@0: if (callback) { michael@0: callback(); michael@0: } michael@0: }, CLOSE_DELAY); michael@0: }, michael@0: michael@0: /** michael@0: * Handler for the keydown event on the panel. Either copy the color michael@0: * or move the panel in a direction depending on the key pressed. michael@0: * michael@0: * @param {Event} event michael@0: * DOM KeyboardEvent object michael@0: */ michael@0: _onKeyDown: function(event) { michael@0: if (event.metaKey && event.keyCode === event.DOM_VK_C) { michael@0: this.copyColor(); michael@0: return; michael@0: } michael@0: michael@0: let offsetX = 0; michael@0: let offsetY = 0; michael@0: let modifier = 1; michael@0: michael@0: if (event.keyCode === event.DOM_VK_LEFT) { michael@0: offsetX = -1; michael@0: } michael@0: if (event.keyCode === event.DOM_VK_RIGHT) { michael@0: offsetX = 1; michael@0: } michael@0: if (event.keyCode === event.DOM_VK_UP) { michael@0: offsetY = -1; michael@0: } michael@0: if (event.keyCode === event.DOM_VK_DOWN) { michael@0: offsetY = 1; michael@0: } michael@0: if (event.shiftKey) { michael@0: modifier = 10; michael@0: } michael@0: michael@0: offsetY *= modifier; michael@0: offsetX *= modifier; michael@0: michael@0: if (offsetX !== 0 || offsetY !== 0) { michael@0: this._zoomArea.x += offsetX; michael@0: this._zoomArea.y += offsetY; michael@0: michael@0: this._drawWindow(); michael@0: michael@0: this._movePanel(this._panelX + offsetX, this._panelY + offsetY); michael@0: michael@0: event.preventDefault(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Draw the inspected area onto the canvas using the zoom level. michael@0: */ michael@0: _drawWindow: function() { michael@0: let { width, height, x, y } = this._zoomArea; michael@0: michael@0: let zoomedWidth = width / this.zoom; michael@0: let zoomedHeight = height / this.zoom; michael@0: michael@0: let drawX = x - (zoomedWidth / 2); michael@0: let drawY = y - (zoomedHeight / 2); michael@0: michael@0: // draw the portion of the window we're inspecting michael@0: this._ctx.drawWindow(this._chromeWindow, drawX, drawY, zoomedWidth, michael@0: zoomedHeight, "white"); michael@0: michael@0: // now scale it michael@0: let sx = 0; michael@0: let sy = 0; michael@0: let sw = zoomedWidth; michael@0: let sh = zoomedHeight; michael@0: let dx = 0; michael@0: let dy = 0; michael@0: let dw = width; michael@0: let dh = height; michael@0: michael@0: this._ctx.drawImage(this._canvas, sx, sy, sw, sh, dx, dy, dw, dh); michael@0: michael@0: let rgb = this.centerColor; michael@0: this._colorPreview.style.backgroundColor = toColorString(rgb, "rgb"); michael@0: this._colorValue.value = toColorString(rgb, this.format); michael@0: michael@0: if (this.zoom > 2) { michael@0: // grid at 2x is too busy michael@0: this._drawGrid(); michael@0: } michael@0: this._drawCrosshair(); michael@0: }, michael@0: michael@0: /** michael@0: * Draw a grid on the canvas representing pixel boundaries. michael@0: */ michael@0: _drawGrid: function() { michael@0: let { width, height } = this._zoomArea; michael@0: michael@0: this._ctx.lineWidth = 1; michael@0: this._ctx.strokeStyle = "rgba(143, 143, 143, 0.2)"; michael@0: michael@0: for (let i = 0; i < width; i += this.cellSize) { michael@0: this._ctx.beginPath(); michael@0: this._ctx.moveTo(i - .5, 0); michael@0: this._ctx.lineTo(i - .5, height); michael@0: this._ctx.stroke(); michael@0: michael@0: this._ctx.beginPath(); michael@0: this._ctx.moveTo(0, i - .5); michael@0: this._ctx.lineTo(width, i - .5); michael@0: this._ctx.stroke(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Draw a box on the canvas to highlight the center cell. michael@0: */ michael@0: _drawCrosshair: function() { michael@0: let x = y = this.centerCell * this.cellSize; michael@0: michael@0: this._ctx.lineWidth = 1; michael@0: this._ctx.lineJoin = 'miter'; michael@0: this._ctx.strokeStyle = "rgba(0, 0, 0, 1)"; michael@0: this._ctx.strokeRect(x - 1.5, y - 1.5, this.cellSize + 2, this.cellSize + 2); michael@0: michael@0: this._ctx.strokeStyle = "rgba(255, 255, 255, 1)"; michael@0: this._ctx.strokeRect(x - 0.5, y - 0.5, this.cellSize, this.cellSize); michael@0: }, michael@0: michael@0: /** michael@0: * Destroy the eyedropper and clean up. Emits a "destroy" event. michael@0: */ michael@0: destroy: function() { michael@0: this._resetCursor(); michael@0: this._hideCrosshairs(); michael@0: michael@0: if (this._panel) { michael@0: this._panel.hidePopup(); michael@0: this._panel.remove(); michael@0: this._panel = null; michael@0: } michael@0: this._removePanelListeners(); michael@0: this._removeListeners(); michael@0: michael@0: this.isOpen = false; michael@0: this._isSelecting = false; michael@0: michael@0: this.emit("destroy"); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Add a user style sheet that applies to all documents. michael@0: */ michael@0: function registerStyleSheet(url) { michael@0: var uri = ioService.newURI(url, null, null); michael@0: if (!ssService.sheetRegistered(uri, ssService.AGENT_SHEET)) { michael@0: ssService.loadAndRegisterSheet(uri, ssService.AGENT_SHEET); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Remove a user style sheet. michael@0: */ michael@0: function unregisterStyleSheet(url) { michael@0: var uri = ioService.newURI(url, null, null); michael@0: if (ssService.sheetRegistered(uri, ssService.AGENT_SHEET)) { michael@0: ssService.unregisterSheet(uri, ssService.AGENT_SHEET); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Get a formatted CSS color string from a color value. michael@0: * michael@0: * @param {array} rgb michael@0: * Rgb values of a color to format michael@0: * @param {string} format michael@0: * Format of string. One of "hex", "rgb", "hsl", "name" michael@0: * michael@0: * @return {string} michael@0: * Formatted color value, e.g. "#FFF" or "hsl(20, 10%, 10%)" michael@0: */ michael@0: function toColorString(rgb, format) { michael@0: let [r,g,b] = rgb; michael@0: michael@0: switch(format) { michael@0: case "hex": michael@0: return hexString(rgb); michael@0: case "rgb": michael@0: return "rgb(" + r + ", " + g + ", " + b + ")"; michael@0: case "hsl": michael@0: let [h,s,l] = rgbToHsl(rgb); michael@0: return "hsl(" + h + ", " + s + "%, " + l + "%)"; michael@0: case "name": michael@0: let str; michael@0: try { michael@0: str = DOMUtils.rgbToColorName(r, g, b); michael@0: } catch(e) { michael@0: str = hexString(rgb); michael@0: } michael@0: return str; michael@0: default: michael@0: return hexString(rgb); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Produce a hex-formatted color string from rgb values. michael@0: * michael@0: * @param {array} rgb michael@0: * Rgb values of color to stringify michael@0: * michael@0: * @return {string} michael@0: * Hex formatted string for color, e.g. "#FFEE00" michael@0: */ michael@0: function hexString([r,g,b]) { michael@0: let val = (1 << 24) + (r << 16) + (g << 8) + (b << 0); michael@0: return "#" + val.toString(16).substr(-6).toUpperCase(); michael@0: }