1/*
2 * Copyright (C) 2014 Apple Inc.  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 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
29WebInspector.GradientSlider = function(delegate)
30{
31    this.delegate = delegate;
32
33    this._element = null;
34    this._stops = [];
35    this._knobs = [];
36
37    this._selectedKnob = null;
38    this._canvas = document.createElement("canvas");
39
40    this._keyboardShortcutEsc = new WebInspector.KeyboardShortcut(null, WebInspector.KeyboardShortcut.Key.Escape);
41}
42
43WebInspector.GradientSlider.Width = 238;
44WebInspector.GradientSlider.Height = 19;
45
46WebInspector.GradientSlider.StyleClassName = "gradient-slider";
47WebInspector.GradientSlider.AddAreaClassName = "add-area";
48WebInspector.GradientSlider.DetachingClassName = "detaching";
49WebInspector.GradientSlider.ShadowClassName = "shadow";
50
51WebInspector.GradientSlider.prototype = {
52    constructor: WebInspector.GradientSlider,
53
54    // Public
55
56    get element()
57    {
58        if (!this._element) {
59            this._element = document.createElement("div");
60            this._element.className = WebInspector.GradientSlider.StyleClassName;
61            this._element.appendChild(this._canvas);
62
63            this._addArea = this._element.appendChild(document.createElement("div"));
64            this._addArea.addEventListener("mouseover", this);
65            this._addArea.addEventListener("mousemove", this);
66            this._addArea.addEventListener("mouseout", this);
67            this._addArea.addEventListener("click", this);
68            this._addArea.className = WebInspector.GradientSlider.AddAreaClassName;
69        }
70        return this._element;
71    },
72
73    get stops()
74    {
75        return this._stops;
76    },
77
78    set stops(stops)
79    {
80        this._stops = stops;
81
82        this._updateStops();
83    },
84
85    get selectedStop()
86    {
87        return this._selectedKnob ? this._selectedKnob.stop : null;
88    },
89
90    // Protected
91
92    handleEvent: function(event)
93    {
94        switch (event.type) {
95        case "mouseover":
96            this._handleMouseover(event);
97            break;
98        case "mousemove":
99            this._handleMousemove(event);
100            break;
101        case "mouseout":
102            this._handleMouseout(event);
103            break;
104        case "click":
105            this._handleClick(event);
106            break;
107        }
108    },
109
110    handleKeydownEvent: function(event)
111    {
112        if (!this._keyboardShortcutEsc.matchesEvent(event) || !this._selectedKnob || !this._selectedKnob.selected)
113            return false;
114
115        this._selectedKnob.selected = false;
116
117        return true;
118    },
119
120    knobXDidChange: function(knob)
121    {
122        knob.stop.offset = knob.x / WebInspector.GradientSlider.Width;
123        this._sortStops();
124        this._updateCanvas();
125    },
126
127    knobCanDetach: function(knob)
128    {
129        return this._knobs.length > 2;
130    },
131
132    knobWillDetach: function(knob)
133    {
134        knob.element.classList.add(WebInspector.GradientSlider.DetachingClassName);
135
136        this._stops.remove(knob.stop);
137        this._knobs.remove(knob);
138        this._sortStops();
139        this._updateCanvas();
140    },
141
142    knobSelectionChanged: function(knob)
143    {
144        if (this._selectedKnob && this._selectedKnob !== knob && knob.selected)
145            this._selectedKnob.selected = false;
146
147        this._selectedKnob = knob.selected ? knob : null;
148
149        if (this.delegate && typeof this.delegate.gradientSliderStopWasSelected === "function")
150            this.delegate.gradientSliderStopWasSelected(this, knob.stop);
151
152        if (this._selectedKnob)
153            WebInspector.addWindowKeydownListener(this);
154        else
155            WebInspector.removeWindowKeydownListener(this);
156    },
157
158    // Private
159
160    _handleMouseover: function(event)
161    {
162        this._updateShadowKnob(event);
163    },
164
165    _handleMousemove: function(event)
166    {
167        this._updateShadowKnob(event);
168    },
169
170    _handleMouseout: function(event)
171    {
172        if (!this._shadowKnob)
173            return;
174
175        this._shadowKnob.element.remove();
176        delete this._shadowKnob;
177    },
178
179    _handleClick: function(event)
180    {
181        this._updateShadowKnob(event);
182
183        this._knobs.push(this._shadowKnob);
184
185        this._shadowKnob.element.classList.remove(WebInspector.GradientSlider.ShadowClassName);
186
187        var stop = {offset: this._shadowKnob.x / WebInspector.GradientSlider.Width, color: this._shadowKnob.wellColor};
188        this._stops.push(stop);
189        this._sortStops();
190        this._updateStops();
191
192        this._knobs[this._stops.indexOf(stop)].selected = true;
193
194        delete this._shadowKnob;
195    },
196
197    _updateShadowKnob: function(event)
198    {
199        if (!this._shadowKnob) {
200            this._shadowKnob = new WebInspector.GradientSliderKnob(this);
201            this._shadowKnob.element.classList.add(WebInspector.GradientSlider.ShadowClassName);
202            this.element.appendChild(this._shadowKnob.element);
203        }
204
205        this._shadowKnob.x = window.webkitConvertPointFromPageToNode(this.element, new WebKitPoint(event.pageX, event.pageY)).x;
206
207        var colorData = this._canvas.getContext("2d").getImageData(this._shadowKnob.x - 1, 0, 1, 1).data;
208        this._shadowKnob.wellColor = new WebInspector.Color(WebInspector.Color.Format.RGB, [colorData[0], colorData[1], colorData[2], colorData[3] / 255]);
209    },
210
211    _sortStops: function()
212    {
213        this._stops.sort(function(a, b) {
214            return a.offset - b.offset;
215        });
216    },
217
218    _updateStops: function()
219    {
220        this._updateCanvas();
221        this._updateKnobs();
222    },
223
224    _updateCanvas: function()
225    {
226        var w = WebInspector.GradientSlider.Width;
227        var h = WebInspector.GradientSlider.Height;
228
229        this._canvas.width = w;
230        this._canvas.height = h;
231
232        var ctx = this._canvas.getContext("2d");
233        var gradient = ctx.createLinearGradient(0, 0, w, 0);
234        for (var stop of this._stops)
235            gradient.addColorStop(stop.offset, stop.color);
236
237        ctx.clearRect(0, 0, w, h);
238        ctx.fillStyle = gradient;
239        ctx.fillRect(0, 0, w, h);
240
241        if (this.delegate && typeof this.delegate.gradientSliderStopsDidChange === "function")
242            this.delegate.gradientSliderStopsDidChange(this);
243    },
244
245    _updateKnobs: function()
246    {
247        var selectedStop = this._selectedKnob ? this._selectedKnob.stop : null;
248
249        while (this._knobs.length > this._stops.length)
250            this._knobs.pop().element.remove();
251
252        while (this._knobs.length < this._stops.length) {
253            var knob = new WebInspector.GradientSliderKnob(this);
254            this.element.appendChild(knob.element);
255            this._knobs.push(knob);
256        }
257
258        for (var i = 0; i < this._stops.length; ++i) {
259            var stop = this._stops[i];
260            var knob = this._knobs[i];
261
262            knob.stop = stop;
263            knob.x = Math.round(stop.offset * WebInspector.GradientSlider.Width);
264            knob.selected = stop === selectedStop;
265        }
266    }
267}
268
269WebInspector.GradientSliderKnob = function(delegate)
270{
271    this._x = 0;
272    this._y = 0;
273    this._stop = null;
274
275    this.delegate = delegate;
276
277    this._element = document.createElement("div");
278    this._element.className = WebInspector.GradientSliderKnob.StyleClassName;
279
280    // Checkers pattern.
281    this._element.appendChild(document.createElement("img"));
282
283    this._well = this._element.appendChild(document.createElement("div"));
284
285    this._element.addEventListener("mousedown", this);
286};
287
288WebInspector.GradientSliderKnob.StyleClassName = "gradient-slider-knob";
289WebInspector.GradientSliderKnob.SelectedClassName = "selected";
290WebInspector.GradientSliderKnob.FadeOutClassName = "fade-out";
291
292WebInspector.GradientSliderKnob.prototype = {
293    constructor: WebInspector.GradientSliderKnob,
294
295    // Public
296
297    get element()
298    {
299        return this._element;
300    },
301
302    get stop()
303    {
304        return this._stop;
305    },
306
307    set stop(stop)
308    {
309        this.wellColor = stop.color;
310        this._stop = stop;
311    },
312
313    get x()
314    {
315        return this._x;
316    },
317
318    set x(x) {
319        this._x = x;
320        this._updateTransform();
321    },
322
323    get y()
324    {
325        return this._x;
326    },
327
328    set y(y) {
329        this._y = y;
330        this._updateTransform();
331    },
332
333    get wellColor(color)
334    {
335        return this._wellColor;
336    },
337
338    set wellColor(color)
339    {
340        this._wellColor = color;
341        this._well.style.backgroundColor = color;
342    },
343
344    get selected()
345    {
346        return this._element.classList.contains(WebInspector.GradientSliderKnob.SelectedClassName);
347    },
348
349    set selected(selected)
350    {
351        if (this.selected === selected)
352            return;
353
354        this._element.classList.toggle(WebInspector.GradientSliderKnob.SelectedClassName, selected);
355
356        if (this.delegate && typeof this.delegate.knobSelectionChanged === "function")
357            this.delegate.knobSelectionChanged(this);
358    },
359
360    // Protected
361
362    handleEvent: function(event)
363    {
364        event.preventDefault();
365        event.stopPropagation();
366
367        switch (event.type) {
368        case "mousedown":
369            this._handleMousedown(event);
370            break;
371        case "mousemove":
372            this._handleMousemove(event);
373            break;
374        case "mouseup":
375            this._handleMouseup(event);
376            break;
377        case "transitionend":
378            this._handleTransitionEnd(event);
379            break;
380        }
381    },
382
383    // Private
384
385    _handleMousedown: function(event)
386    {
387        this._moved = false;
388        this._detaching = false;
389
390        window.addEventListener("mousemove", this, true);
391        window.addEventListener("mouseup", this, true);
392
393        this._startX = this.x;
394        this._startMouseX = event.pageX;
395        this._startMouseY = event.pageY;
396    },
397
398    _handleMousemove: function(event)
399    {
400        var w = WebInspector.GradientSlider.Width;
401
402        this._moved = true;
403
404        if (!this._detaching && Math.abs(event.pageY - this._startMouseY) > 50) {
405            this._detaching = this.delegate && typeof this.delegate.knobCanDetach === "function" && this.delegate.knobCanDetach(this);
406            if (this._detaching && this.delegate && typeof this.delegate.knobWillDetach === "function") {
407                var translationFromParentToBody = window.webkitConvertPointFromNodeToPage(this.element.parentNode, new WebKitPoint(0, 0));
408                this._startMouseX -= translationFromParentToBody.x;
409                this._startMouseY -= translationFromParentToBody.y;
410                document.body.appendChild(this.element);
411                this.delegate.knobWillDetach(this);
412            }
413        }
414
415        var x = this._startX + event.pageX - this._startMouseX;
416        if (!this._detaching)
417            x = Math.min(Math.max(0, x), w);
418        this.x = x;
419
420        if (this._detaching)
421            this.y = event.pageY - this._startMouseY;
422        else if (this.delegate && typeof this.delegate.knobXDidChange === "function")
423            this.delegate.knobXDidChange(this);
424    },
425
426    _handleMouseup: function(event)
427    {
428        window.removeEventListener("mousemove", this, true);
429        window.removeEventListener("mouseup", this, true);
430
431        if (this._detaching) {
432            this.element.addEventListener("transitionend", this);
433            this.element.classList.add(WebInspector.GradientSliderKnob.FadeOutClassName);
434            this.selected = false;
435        } else if (!this._moved)
436            this.selected = !this.selected;
437    },
438
439    _handleTransitionEnd: function(event)
440    {
441        this.element.removeEventListener("transitionend", this);
442        this.element.classList.remove(WebInspector.GradientSliderKnob.FadeOutClassName);
443        this.element.remove();
444    },
445
446    _updateTransform: function()
447    {
448        this.element.style.webkitTransform = "translate3d(" + this._x + "px, " + this._y + "px, 0)";
449    }
450}
451