1/*
2 * Copyright (C) 2011 Brian Grinstead All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 *
8 * 1.  Redistributions of source code must retain the above copyright
9 *     notice, this list of conditions and the following disclaimer.
10 * 2.  Redistributions in binary form must reproduce the above copyright
11 *     notice, this list of conditions and the following disclaimer in the
12 *     documentation and/or other materials provided with the distribution.
13 * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
14 *     its contributors may be used to endorse or promote products derived
15 *     from this software without specific prior written permission.
16 *
17 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 */
28
29/**
30 * @constructor
31 * @extends {WebInspector.VBox}
32 */
33WebInspector.Spectrum = function()
34{
35    WebInspector.VBox.call(this);
36    this.registerRequiredCSS("spectrum.css");
37
38    this.element.classList.add("spectrum-container");
39    this.element.tabIndex = 0;
40
41    this._draggerElement = this.element.createChild("div", "spectrum-color");
42    this._dragHelperElement = this._draggerElement.createChild("div", "spectrum-sat fill").createChild("div", "spectrum-val fill").createChild("div", "spectrum-dragger");
43
44    this._sliderElement = this.element.createChild("div", "spectrum-hue");
45    this.slideHelper = this._sliderElement.createChild("div", "spectrum-slider");
46
47    var rangeContainer = this.element.createChild("div", "spectrum-range-container");
48    var alphaLabel = rangeContainer.createChild("label");
49    alphaLabel.textContent = WebInspector.UIString("\u03B1:");
50
51    this._alphaElement = rangeContainer.createChild("input", "spectrum-range");
52    this._alphaElement.setAttribute("type", "range");
53    this._alphaElement.setAttribute("min", "0");
54    this._alphaElement.setAttribute("max", "100");
55    this._alphaElement.addEventListener("input", alphaDrag.bind(this), false);
56    this._alphaElement.addEventListener("change", alphaDrag.bind(this), false);
57
58    var displayContainer = this.element.createChild("div", "spectrum-text");
59    var swatchElement = displayContainer.createChild("span", "swatch");
60    this._swatchInnerElement = swatchElement.createChild("span", "swatch-inner");
61    this._displayElement = displayContainer.createChild("span", "source-code spectrum-display-value");
62
63    WebInspector.Spectrum.draggable(this._sliderElement, hueDrag.bind(this));
64    WebInspector.Spectrum.draggable(this._draggerElement, colorDrag.bind(this), colorDragStart.bind(this));
65
66    /**
67     * @param {!Element} element
68     * @param {number} dragX
69     * @param {number} dragY
70     * @this {WebInspector.Spectrum}
71     */
72    function hueDrag(element, dragX, dragY)
73    {
74        this._hsv[0] = (this.slideHeight - dragY) / this.slideHeight;
75
76        this._onchange();
77    }
78
79    var initialHelperOffset;
80
81    /**
82     * @this {WebInspector.Spectrum}
83     */
84    function colorDragStart()
85    {
86        initialHelperOffset = { x: this._dragHelperElement.offsetLeft, y: this._dragHelperElement.offsetTop };
87    }
88
89    /**
90     * @param {!Element} element
91     * @param {number} dragX
92     * @param {number} dragY
93     * @param {!MouseEvent} event
94     * @this {WebInspector.Spectrum}
95     */
96    function colorDrag(element, dragX, dragY, event)
97    {
98        if (event.shiftKey) {
99            if (Math.abs(dragX - initialHelperOffset.x) >= Math.abs(dragY - initialHelperOffset.y))
100                dragY = initialHelperOffset.y;
101            else
102                dragX = initialHelperOffset.x;
103        }
104
105        this._hsv[1] = dragX / this.dragWidth;
106        this._hsv[2] = (this.dragHeight - dragY) / this.dragHeight;
107
108        this._onchange();
109    }
110
111    /**
112     * @this {WebInspector.Spectrum}
113     */
114    function alphaDrag()
115    {
116        this._hsv[3] = this._alphaElement.value / 100;
117
118        this._onchange();
119    }
120};
121
122WebInspector.Spectrum.Events = {
123    ColorChanged: "ColorChanged"
124};
125
126/**
127 * @param {!Element} element
128 * @param {function(!Element, number, number, !MouseEvent)=} onmove
129 * @param {function(!Element, !MouseEvent)=} onstart
130 * @param {function(!Element, !MouseEvent)=} onstop
131 */
132WebInspector.Spectrum.draggable = function(element, onmove, onstart, onstop) {
133
134    var doc = document;
135    var dragging;
136    var offset;
137    var scrollOffset;
138    var maxHeight;
139    var maxWidth;
140
141    /**
142     * @param {!Event} e
143     */
144    function consume(e)
145    {
146        e.consume(true);
147    }
148
149    /**
150     * @param {!Event} e
151     */
152    function move(e)
153    {
154        if (dragging) {
155            var dragX = Math.max(0, Math.min(e.pageX - offset.left + scrollOffset.left, maxWidth));
156            var dragY = Math.max(0, Math.min(e.pageY - offset.top + scrollOffset.top, maxHeight));
157
158            if (onmove)
159                onmove(element, dragX, dragY, /** @type {!MouseEvent} */ (e));
160        }
161    }
162
163    /**
164     * @param {!Event} e
165     */
166    function start(e)
167    {
168        var mouseEvent = /** @type {!MouseEvent} */ (e);
169        var rightClick = mouseEvent.which ? (mouseEvent.which === 3) : (mouseEvent.button === 2);
170
171        if (!rightClick && !dragging) {
172
173            if (onstart)
174                onstart(element, mouseEvent);
175
176            dragging = true;
177            maxHeight = element.clientHeight;
178            maxWidth = element.clientWidth;
179
180            scrollOffset = element.scrollOffset();
181            offset = element.totalOffset();
182
183            doc.addEventListener("selectstart", consume, false);
184            doc.addEventListener("dragstart", consume, false);
185            doc.addEventListener("mousemove", move, false);
186            doc.addEventListener("mouseup", stop, false);
187
188            move(mouseEvent);
189            consume(mouseEvent);
190        }
191    }
192
193    /**
194     * @param {!Event} e
195     */
196    function stop(e)
197    {
198        if (dragging) {
199            doc.removeEventListener("selectstart", consume, false);
200            doc.removeEventListener("dragstart", consume, false);
201            doc.removeEventListener("mousemove", move, false);
202            doc.removeEventListener("mouseup", stop, false);
203
204            if (onstop)
205                onstop(element, /** @type {!MouseEvent} */ (e));
206        }
207
208        dragging = false;
209    }
210
211    element.addEventListener("mousedown", start, false);
212};
213
214WebInspector.Spectrum.prototype = {
215    /**
216     * @param {!WebInspector.Color} color
217     */
218    setColor: function(color)
219    {
220        this._hsv = color.hsva();
221    },
222
223    /**
224     * @return {!WebInspector.Color}
225     */
226    color: function()
227    {
228        return WebInspector.Color.fromHSVA(this._hsv);
229    },
230
231    _colorString: function()
232    {
233        var cf = WebInspector.Color.Format;
234        var format = this._originalFormat;
235        var color = this.color();
236        var originalFormatString = color.toString(this._originalFormat);
237        if (originalFormatString)
238            return originalFormatString;
239
240        if (color.hasAlpha()) {
241            // Everything except HSL(A) should be returned as RGBA if transparency is involved.
242            if (format === cf.HSLA || format === cf.HSL)
243                return color.toString(cf.HSLA);
244            else
245                return color.toString(cf.RGBA);
246        }
247
248        if (format === cf.ShortHEX)
249            return color.toString(cf.HEX);
250        console.assert(format === cf.Nickname);
251        return color.toString(cf.RGB);
252    },
253
254
255    set displayText(text)
256    {
257        this._displayElement.textContent = text;
258    },
259
260    _onchange: function()
261    {
262        this._updateUI();
263        this.dispatchEventToListeners(WebInspector.Spectrum.Events.ColorChanged, this._colorString());
264    },
265
266    _updateHelperLocations: function()
267    {
268        var h = this._hsv[0];
269        var s = this._hsv[1];
270        var v = this._hsv[2];
271
272        // Where to show the little circle that displays your current selected color.
273        var dragX = s * this.dragWidth;
274        var dragY = this.dragHeight - (v * this.dragHeight);
275
276        dragX = Math.max(-this._dragHelperElementHeight,
277                        Math.min(this.dragWidth - this._dragHelperElementHeight, dragX - this._dragHelperElementHeight));
278        dragY = Math.max(-this._dragHelperElementHeight,
279                        Math.min(this.dragHeight - this._dragHelperElementHeight, dragY - this._dragHelperElementHeight));
280
281        this._dragHelperElement.positionAt(dragX, dragY);
282
283        // Where to show the bar that displays your current selected hue.
284        var slideY = this.slideHeight - ((h * this.slideHeight) + this.slideHelperHeight);
285        this.slideHelper.style.top = slideY + "px";
286
287        this._alphaElement.value = this._hsv[3] * 100;
288    },
289
290    _updateUI: function()
291    {
292        this._updateHelperLocations();
293
294        this._draggerElement.style.backgroundColor = /** @type {string} */ (WebInspector.Color.fromHSVA([this._hsv[0], 1, 1, 1]).toString(WebInspector.Color.Format.RGB));
295        this._swatchInnerElement.style.backgroundColor = /** @type {string} */ (this.color().toString(WebInspector.Color.Format.RGBA));
296
297        this._alphaElement.value = this._hsv[3] * 100;
298    },
299
300    wasShown: function()
301    {
302        this.slideHeight = this._sliderElement.offsetHeight;
303        this.dragWidth = this._draggerElement.offsetWidth;
304        this.dragHeight = this._draggerElement.offsetHeight;
305        this._dragHelperElementHeight = this._dragHelperElement.offsetHeight / 2;
306        this.slideHelperHeight = this.slideHelper.offsetHeight / 2;
307        this._updateUI();
308    },
309
310    __proto__: WebInspector.VBox.prototype
311}
312
313/**
314 * @constructor
315 * @extends {WebInspector.Object}
316 */
317WebInspector.SpectrumPopupHelper = function()
318{
319    this._spectrum = new WebInspector.Spectrum();
320    this._spectrum.element.addEventListener("keydown", this._onKeyDown.bind(this), false);
321
322    this._popover = new WebInspector.Popover();
323    this._popover.setCanShrink(false);
324    this._popover.element.addEventListener("mousedown", consumeEvent, false);
325
326    this._hideProxy = this.hide.bind(this, true);
327}
328
329WebInspector.SpectrumPopupHelper.Events = {
330    Hidden: "Hidden"
331};
332
333WebInspector.SpectrumPopupHelper.prototype = {
334    /**
335     * @return {!WebInspector.Spectrum}
336     */
337    spectrum: function()
338    {
339        return this._spectrum;
340    },
341
342    /**
343     * @return {boolean}
344     */
345    toggle: function(element, color, format)
346    {
347        if (this._popover.isShowing())
348            this.hide(true);
349        else
350            this.show(element, color, format);
351
352        return this._popover.isShowing();
353    },
354
355    /**
356     * @return {boolean}
357     */
358    show: function(element, color, format)
359    {
360        if (this._popover.isShowing()) {
361            if (this._anchorElement === element)
362                return false;
363
364            // Reopen the picker for another anchor element.
365            this.hide(true);
366        }
367
368        delete this._isHidden;
369        this._anchorElement = element;
370
371        this._spectrum.setColor(color);
372        this._spectrum._originalFormat = format !== WebInspector.Color.Format.Original ? format : color.format();
373        this.reposition(element);
374
375        document.addEventListener("mousedown", this._hideProxy, false);
376        window.addEventListener("resize", this._hideProxy, false);
377
378        WebInspector.targetManager.addModelListener(WebInspector.ResourceTreeModel, WebInspector.ResourceTreeModel.EventTypes.ColorPicked, this._colorPicked, this);
379        PageAgent.setColorPickerEnabled(true);
380        return true;
381    },
382
383    reposition: function(element)
384    {
385        if (!this._previousFocusElement)
386            this._previousFocusElement = WebInspector.currentFocusElement();
387        this._popover.showView(this._spectrum, element);
388        WebInspector.setCurrentFocusElement(this._spectrum.element);
389    },
390
391    /**
392     * @param {boolean=} commitEdit
393     */
394    hide: function(commitEdit)
395    {
396        if (this._isHidden)
397            return;
398        this._isHidden = true;
399        this._popover.hide();
400
401        document.removeEventListener("mousedown", this._hideProxy, false);
402        window.removeEventListener("resize", this._hideProxy, false);
403
404        PageAgent.setColorPickerEnabled(false);
405        WebInspector.targetManager.removeModelListener(WebInspector.ResourceTreeModel, WebInspector.ResourceTreeModel.EventTypes.ColorPicked, this._colorPicked, this);
406
407        this.dispatchEventToListeners(WebInspector.SpectrumPopupHelper.Events.Hidden, !!commitEdit);
408
409        WebInspector.setCurrentFocusElement(this._previousFocusElement);
410        delete this._previousFocusElement;
411
412        delete this._anchorElement;
413    },
414
415    _onKeyDown: function(event)
416    {
417        if (event.keyIdentifier === "Enter") {
418            this.hide(true);
419            event.consume(true);
420            return;
421        }
422        if (event.keyIdentifier === "U+001B") { // Escape key
423            this.hide(false);
424            event.consume(true);
425        }
426    },
427
428    /**
429     * @param {!WebInspector.Event} event
430     */
431    _colorPicked: function(event)
432    {
433        var color = /** @type {!DOMAgent.RGBA} */ (event.data);
434        var rgba = [color.r, color.g, color.b, (color.a / 2.55 | 0) / 100];
435        this._spectrum.setColor(WebInspector.Color.fromRGBA(rgba));
436        this._spectrum._onchange();
437        InspectorFrontendHost.bringToFront();
438    },
439
440    __proto__: WebInspector.Object.prototype
441}
442
443/**
444 * @constructor
445 * @param {boolean=} readOnly
446 */
447WebInspector.ColorSwatch = function(readOnly)
448{
449    this.element = document.createElementWithClass("span", "swatch");
450    this._swatchInnerElement = this.element.createChild("span", "swatch-inner");
451    var shiftClickMessage = WebInspector.UIString("Shift-click to change color format.");
452    this.element.title = readOnly ? shiftClickMessage : String.sprintf("%s\n%s", WebInspector.UIString("Click to open a colorpicker."), shiftClickMessage);
453    this.element.addEventListener("mousedown", consumeEvent, false);
454    this.element.addEventListener("dblclick", consumeEvent, false);
455}
456
457WebInspector.ColorSwatch.prototype = {
458    /**
459     * @param {string} colorString
460     */
461    setColorString: function(colorString)
462    {
463        this._swatchInnerElement.style.backgroundColor = colorString;
464    }
465}
466