1/* 2 * Copyright (C) 2013 Google 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 are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * * Neither the name of Google Inc. nor the names of its 15 * contributors may be used to endorse or promote products derived from 16 * this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 31/** 32 * @constructor 33 * @param {string} prefix 34 */ 35WebInspector.OverviewGrid = function(prefix) 36{ 37 this.element = document.createElement("div"); 38 this.element.id = prefix + "-overview-container"; 39 40 this._grid = new WebInspector.TimelineGrid(); 41 this._grid.element.id = prefix + "-overview-grid"; 42 this._grid.setScrollAndDividerTop(0, 0); 43 44 this.element.appendChild(this._grid.element); 45 46 this._window = new WebInspector.OverviewGrid.Window(this.element, this._grid.dividersLabelBarElement); 47} 48 49WebInspector.OverviewGrid.prototype = { 50 /** 51 * @return {number} 52 */ 53 clientWidth: function() 54 { 55 return this.element.clientWidth; 56 }, 57 58 /** 59 * @param {!WebInspector.TimelineGrid.Calculator} calculator 60 */ 61 updateDividers: function(calculator) 62 { 63 this._grid.updateDividers(calculator); 64 }, 65 66 /** 67 * @param {!Array.<!Element>} dividers 68 */ 69 addEventDividers: function(dividers) 70 { 71 this._grid.addEventDividers(dividers); 72 }, 73 74 removeEventDividers: function() 75 { 76 this._grid.removeEventDividers(); 77 }, 78 79 /** 80 * @param {?number} start 81 * @param {?number} end 82 */ 83 setWindowPosition: function(start, end) 84 { 85 this._window._setWindowPosition(start, end); 86 }, 87 88 reset: function() 89 { 90 this._window.reset(); 91 }, 92 93 /** 94 * @return {number} 95 */ 96 windowLeft: function() 97 { 98 return this._window.windowLeft; 99 }, 100 101 /** 102 * @return {number} 103 */ 104 windowRight: function() 105 { 106 return this._window.windowRight; 107 }, 108 109 /** 110 * @param {number} left 111 * @param {number} right 112 */ 113 setWindow: function(left, right) 114 { 115 this._window._setWindow(left, right); 116 }, 117 118 /** 119 * @param {string} eventType 120 * @param {function(!WebInspector.Event)} listener 121 * @param {!Object=} thisObject 122 */ 123 addEventListener: function(eventType, listener, thisObject) 124 { 125 this._window.addEventListener(eventType, listener, thisObject); 126 }, 127 128 /** 129 * @param {!number} zoomFactor 130 * @param {!number} referencePoint 131 */ 132 zoom: function(zoomFactor, referencePoint) 133 { 134 this._window._zoom(zoomFactor, referencePoint); 135 }, 136 137 /** 138 * @param {boolean} enabled 139 */ 140 setResizeEnabled: function(enabled) 141 { 142 this._window._setEnabled(!!enabled); 143 } 144} 145 146 147WebInspector.OverviewGrid.MinSelectableSize = 14; 148 149WebInspector.OverviewGrid.WindowScrollSpeedFactor = .3; 150 151WebInspector.OverviewGrid.ResizerOffset = 3.5; // half pixel because offset values are not rounded but ceiled 152 153/** 154 * @constructor 155 * @extends {WebInspector.Object} 156 * @param {!Element} parentElement 157 * @param {!Element} dividersLabelBarElement 158 */ 159WebInspector.OverviewGrid.Window = function(parentElement, dividersLabelBarElement) 160{ 161 this._parentElement = parentElement; 162 this._dividersLabelBarElement = dividersLabelBarElement; 163 164 WebInspector.installDragHandle(this._parentElement, this._startWindowSelectorDragging.bind(this), this._windowSelectorDragging.bind(this), this._endWindowSelectorDragging.bind(this), "ew-resize", null); 165 WebInspector.installDragHandle(this._dividersLabelBarElement, this._startWindowDragging.bind(this), this._windowDragging.bind(this), null, "move"); 166 167 this.windowLeft = 0.0; 168 this.windowRight = 1.0; 169 170 this._parentElement.addEventListener("mousewheel", this._onMouseWheel.bind(this), true); 171 this._parentElement.addEventListener("dblclick", this._resizeWindowMaximum.bind(this), true); 172 173 this._overviewWindowElement = parentElement.createChild("div", "overview-grid-window"); 174 this._overviewWindowBordersElement = parentElement.createChild("div", "overview-grid-window-rulers"); 175 parentElement.createChild("div", "overview-grid-dividers-background"); 176 177 this._leftResizeElement = parentElement.createChild("div", "overview-grid-window-resizer"); 178 this._leftResizeElement.style.left = 0; 179 WebInspector.installDragHandle(this._leftResizeElement, this._resizerElementStartDragging.bind(this), this._leftResizeElementDragging.bind(this), null, "ew-resize"); 180 181 this._rightResizeElement = parentElement.createChild("div", "overview-grid-window-resizer overview-grid-window-resizer-right"); 182 this._rightResizeElement.style.right = 0; 183 WebInspector.installDragHandle(this._rightResizeElement, this._resizerElementStartDragging.bind(this), this._rightResizeElementDragging.bind(this), null, "ew-resize"); 184 this._setEnabled(true); 185} 186 187WebInspector.OverviewGrid.Events = { 188 WindowChanged: "WindowChanged" 189} 190 191WebInspector.OverviewGrid.Window.prototype = { 192 reset: function() 193 { 194 this.windowLeft = 0.0; 195 this.windowRight = 1.0; 196 197 this._overviewWindowElement.style.left = "0%"; 198 this._overviewWindowElement.style.width = "100%"; 199 this._overviewWindowBordersElement.style.left = "0%"; 200 this._overviewWindowBordersElement.style.right = "0%"; 201 this._leftResizeElement.style.left = "0%"; 202 this._rightResizeElement.style.left = "100%"; 203 this._setEnabled(true); 204 }, 205 206 /** 207 * @param {boolean} enabled 208 */ 209 _setEnabled: function(enabled) 210 { 211 enabled = !!enabled; 212 if (this._enabled === enabled) 213 return; 214 this._enabled = enabled; 215 }, 216 217 /** 218 * @param {!Event} event 219 */ 220 _resizerElementStartDragging: function(event) 221 { 222 if (!this._enabled) 223 return false; 224 this._resizerParentOffsetLeft = event.pageX - event.offsetX - event.target.offsetLeft; 225 event.preventDefault(); 226 return true; 227 }, 228 229 /** 230 * @param {!Event} event 231 */ 232 _leftResizeElementDragging: function(event) 233 { 234 this._resizeWindowLeft(event.pageX - this._resizerParentOffsetLeft); 235 event.preventDefault(); 236 }, 237 238 /** 239 * @param {!Event} event 240 */ 241 _rightResizeElementDragging: function(event) 242 { 243 this._resizeWindowRight(event.pageX - this._resizerParentOffsetLeft); 244 event.preventDefault(); 245 }, 246 247 /** 248 * @param {!Event} event 249 * @return {boolean} 250 */ 251 _startWindowSelectorDragging: function(event) 252 { 253 if (!this._enabled) 254 return false; 255 this._offsetLeft = event.pageX - event.offsetX; 256 var position = event.pageX - this._offsetLeft; 257 this._overviewWindowSelector = new WebInspector.OverviewGrid.WindowSelector(this._parentElement, position); 258 return true; 259 }, 260 261 /** 262 * @param {!Event} event 263 */ 264 _windowSelectorDragging: function(event) 265 { 266 this._overviewWindowSelector._updatePosition(event.pageX - this._offsetLeft); 267 event.preventDefault(); 268 }, 269 270 /** 271 * @param {!Event} event 272 */ 273 _endWindowSelectorDragging: function(event) 274 { 275 var window = this._overviewWindowSelector._close(event.pageX - this._offsetLeft); 276 delete this._overviewWindowSelector; 277 if (window.end === window.start) { // Click, not drag. 278 var middle = window.end; 279 window.start = Math.max(0, middle - WebInspector.OverviewGrid.MinSelectableSize / 2); 280 window.end = Math.min(this._parentElement.clientWidth, middle + WebInspector.OverviewGrid.MinSelectableSize / 2); 281 } else if (window.end - window.start < WebInspector.OverviewGrid.MinSelectableSize) { 282 if (this._parentElement.clientWidth - window.end > WebInspector.OverviewGrid.MinSelectableSize) 283 window.end = window.start + WebInspector.OverviewGrid.MinSelectableSize; 284 else 285 window.start = window.end - WebInspector.OverviewGrid.MinSelectableSize; 286 } 287 this._setWindowPosition(window.start, window.end); 288 }, 289 290 /** 291 * @param {!Event} event 292 * @return {boolean} 293 */ 294 _startWindowDragging: function(event) 295 { 296 this._dragStartPoint = event.pageX; 297 this._dragStartLeft = this.windowLeft; 298 this._dragStartRight = this.windowRight; 299 return true; 300 }, 301 302 /** 303 * @param {!Event} event 304 */ 305 _windowDragging: function(event) 306 { 307 event.preventDefault(); 308 var delta = (event.pageX - this._dragStartPoint) / this._parentElement.clientWidth; 309 if (this._dragStartLeft + delta < 0) 310 delta = -this._dragStartLeft; 311 312 if (this._dragStartRight + delta > 1) 313 delta = 1 - this._dragStartRight; 314 315 this._setWindow(this._dragStartLeft + delta, this._dragStartRight + delta); 316 }, 317 318 /** 319 * @param {number} start 320 */ 321 _resizeWindowLeft: function(start) 322 { 323 // Glue to edge. 324 if (start < 10) 325 start = 0; 326 else if (start > this._rightResizeElement.offsetLeft - 4) 327 start = this._rightResizeElement.offsetLeft - 4; 328 this._setWindowPosition(start, null); 329 }, 330 331 /** 332 * @param {number} end 333 */ 334 _resizeWindowRight: function(end) 335 { 336 // Glue to edge. 337 if (end > this._parentElement.clientWidth - 10) 338 end = this._parentElement.clientWidth; 339 else if (end < this._leftResizeElement.offsetLeft + WebInspector.OverviewGrid.MinSelectableSize) 340 end = this._leftResizeElement.offsetLeft + WebInspector.OverviewGrid.MinSelectableSize; 341 this._setWindowPosition(null, end); 342 }, 343 344 _resizeWindowMaximum: function() 345 { 346 this._setWindowPosition(0, this._parentElement.clientWidth); 347 }, 348 349 /** 350 * @param {number} windowLeft 351 * @param {number} windowRight 352 */ 353 _setWindow: function(windowLeft, windowRight) 354 { 355 var left = windowLeft; 356 var right = windowRight; 357 var width = windowRight - windowLeft; 358 359 // We allow actual time window to be arbitrarily small but don't want the UI window to be too small. 360 var widthInPixels = width * this._parentElement.clientWidth; 361 var minWidthInPixels = WebInspector.OverviewGrid.MinSelectableSize / 2; 362 if (widthInPixels < minWidthInPixels) { 363 var factor = minWidthInPixels / widthInPixels; 364 left = ((windowRight + windowLeft) - width * factor) / 2; 365 right = ((windowRight + windowLeft) + width * factor) / 2; 366 } 367 368 this.windowLeft = windowLeft; 369 this._leftResizeElement.style.left = left * 100 + "%"; 370 this.windowRight = windowRight; 371 this._rightResizeElement.style.left = right * 100 + "%"; 372 373 this._overviewWindowElement.style.left = left * 100 + "%"; 374 this._overviewWindowBordersElement.style.left = left * 100 + "%"; 375 this._overviewWindowElement.style.width = (right - left) * 100 + "%"; 376 this._overviewWindowBordersElement.style.right = (1 - right) * 100 + "%"; 377 378 this.dispatchEventToListeners(WebInspector.OverviewGrid.Events.WindowChanged); 379 }, 380 381 /** 382 * @param {?number} start 383 * @param {?number} end 384 */ 385 _setWindowPosition: function(start, end) 386 { 387 var clientWidth = this._parentElement.clientWidth; 388 var windowLeft = typeof start === "number" ? start / clientWidth : this.windowLeft; 389 var windowRight = typeof end === "number" ? end / clientWidth : this.windowRight; 390 this._setWindow(windowLeft, windowRight); 391 }, 392 393 /** 394 * @param {?Event} event 395 */ 396 _onMouseWheel: function(event) 397 { 398 if (typeof event.wheelDeltaY === "number" && event.wheelDeltaY) { 399 const zoomFactor = 1.1; 400 const mouseWheelZoomSpeed = 1 / 120; 401 402 var reference = event.offsetX / event.target.clientWidth; 403 this._zoom(Math.pow(zoomFactor, -event.wheelDeltaY * mouseWheelZoomSpeed), reference); 404 } 405 if (typeof event.wheelDeltaX === "number" && event.wheelDeltaX) { 406 var offset = Math.round(event.wheelDeltaX * WebInspector.OverviewGrid.WindowScrollSpeedFactor); 407 var windowLeft = this._leftResizeElement.offsetLeft + WebInspector.OverviewGrid.ResizerOffset; 408 var windowRight = this._rightResizeElement.offsetLeft + WebInspector.OverviewGrid.ResizerOffset; 409 410 if (windowLeft - offset < 0) 411 offset = windowLeft; 412 413 if (windowRight - offset > this._parentElement.clientWidth) 414 offset = windowRight - this._parentElement.clientWidth; 415 416 this._setWindowPosition(windowLeft - offset, windowRight - offset); 417 418 event.preventDefault(); 419 } 420 }, 421 422 /** 423 * @param {number} factor 424 * @param {number} reference 425 */ 426 _zoom: function(factor, reference) 427 { 428 var left = this.windowLeft; 429 var right = this.windowRight; 430 var windowSize = right - left; 431 var newWindowSize = factor * windowSize; 432 if (newWindowSize > 1) { 433 newWindowSize = 1; 434 factor = newWindowSize / windowSize; 435 } 436 left = reference + (left - reference) * factor; 437 left = Number.constrain(left, 0, 1 - newWindowSize); 438 439 right = reference + (right - reference) * factor; 440 right = Number.constrain(right, newWindowSize, 1); 441 this._setWindow(left, right); 442 }, 443 444 __proto__: WebInspector.Object.prototype 445} 446 447/** 448 * @constructor 449 */ 450WebInspector.OverviewGrid.WindowSelector = function(parent, position) 451{ 452 this._startPosition = position; 453 this._width = parent.offsetWidth; 454 this._windowSelector = document.createElement("div"); 455 this._windowSelector.className = "overview-grid-window-selector"; 456 this._windowSelector.style.left = this._startPosition + "px"; 457 this._windowSelector.style.right = this._width - this._startPosition + "px"; 458 parent.appendChild(this._windowSelector); 459} 460 461WebInspector.OverviewGrid.WindowSelector.prototype = { 462 _createSelectorElement: function(parent, left, width, height) 463 { 464 var selectorElement = document.createElement("div"); 465 selectorElement.className = "overview-grid-window-selector"; 466 selectorElement.style.left = left + "px"; 467 selectorElement.style.width = width + "px"; 468 selectorElement.style.top = "0px"; 469 selectorElement.style.height = height + "px"; 470 parent.appendChild(selectorElement); 471 return selectorElement; 472 }, 473 474 _close: function(position) 475 { 476 position = Math.max(0, Math.min(position, this._width)); 477 this._windowSelector.remove(); 478 return this._startPosition < position ? {start: this._startPosition, end: position} : {start: position, end: this._startPosition}; 479 }, 480 481 _updatePosition: function(position) 482 { 483 position = Math.max(0, Math.min(position, this._width)); 484 if (position < this._startPosition) { 485 this._windowSelector.style.left = position + "px"; 486 this._windowSelector.style.right = this._width - this._startPosition + "px"; 487 } else { 488 this._windowSelector.style.left = this._startPosition + "px"; 489 this._windowSelector.style.right = this._width - position + "px"; 490 } 491 } 492} 493