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