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 163 WebInspector.installDragHandle(this._parentElement, this._startWindowSelectorDragging.bind(this), this._windowSelectorDragging.bind(this), this._endWindowSelectorDragging.bind(this), "ew-resize", null); 164 if (dividersLabelBarElement) 165 WebInspector.installDragHandle(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._overviewWindowElement.appendChild(WebInspector.View.createStyleElement("overviewGrid.css")); 175 this._overviewWindowBordersElement = parentElement.createChild("div", "overview-grid-window-rulers"); 176 parentElement.createChild("div", "overview-grid-dividers-background"); 177 178 this._leftResizeElement = parentElement.createChild("div", "overview-grid-window-resizer"); 179 this._leftResizeElement.style.left = 0; 180 WebInspector.installDragHandle(this._leftResizeElement, this._resizerElementStartDragging.bind(this), this._leftResizeElementDragging.bind(this), null, "ew-resize"); 181 182 this._rightResizeElement = parentElement.createChild("div", "overview-grid-window-resizer overview-grid-window-resizer-right"); 183 this._rightResizeElement.style.right = 0; 184 WebInspector.installDragHandle(this._rightResizeElement, this._resizerElementStartDragging.bind(this), this._rightResizeElementDragging.bind(this), null, "ew-resize"); 185 this._setEnabled(true); 186} 187 188WebInspector.OverviewGrid.Events = { 189 WindowChanged: "WindowChanged" 190} 191 192WebInspector.OverviewGrid.Window.prototype = { 193 reset: function() 194 { 195 this.windowLeft = 0.0; 196 this.windowRight = 1.0; 197 198 this._overviewWindowElement.style.left = "0%"; 199 this._overviewWindowElement.style.width = "100%"; 200 this._overviewWindowBordersElement.style.left = "0%"; 201 this._overviewWindowBordersElement.style.right = "0%"; 202 this._leftResizeElement.style.left = "0%"; 203 this._rightResizeElement.style.left = "100%"; 204 this._setEnabled(true); 205 }, 206 207 /** 208 * @param {boolean} enabled 209 */ 210 _setEnabled: function(enabled) 211 { 212 enabled = !!enabled; 213 if (this._enabled === enabled) 214 return; 215 this._enabled = enabled; 216 }, 217 218 /** 219 * @param {!Event} event 220 */ 221 _resizerElementStartDragging: function(event) 222 { 223 if (!this._enabled) 224 return false; 225 this._resizerParentOffsetLeft = event.pageX - event.offsetX - event.target.offsetLeft; 226 event.preventDefault(); 227 return true; 228 }, 229 230 /** 231 * @param {!Event} event 232 */ 233 _leftResizeElementDragging: function(event) 234 { 235 this._resizeWindowLeft(event.pageX - this._resizerParentOffsetLeft); 236 event.preventDefault(); 237 }, 238 239 /** 240 * @param {!Event} event 241 */ 242 _rightResizeElementDragging: function(event) 243 { 244 this._resizeWindowRight(event.pageX - this._resizerParentOffsetLeft); 245 event.preventDefault(); 246 }, 247 248 /** 249 * @param {!Event} event 250 * @return {boolean} 251 */ 252 _startWindowSelectorDragging: function(event) 253 { 254 if (!this._enabled) 255 return false; 256 this._offsetLeft = this._parentElement.totalOffsetLeft(); 257 var position = event.x - this._offsetLeft; 258 this._overviewWindowSelector = new WebInspector.OverviewGrid.WindowSelector(this._parentElement, position); 259 return true; 260 }, 261 262 /** 263 * @param {!Event} event 264 */ 265 _windowSelectorDragging: function(event) 266 { 267 this._overviewWindowSelector._updatePosition(event.x - this._offsetLeft); 268 event.preventDefault(); 269 }, 270 271 /** 272 * @param {!Event} event 273 */ 274 _endWindowSelectorDragging: function(event) 275 { 276 var window = this._overviewWindowSelector._close(event.x - this._offsetLeft); 277 delete this._overviewWindowSelector; 278 if (window.end === window.start) { // Click, not drag. 279 var middle = window.end; 280 window.start = Math.max(0, middle - WebInspector.OverviewGrid.MinSelectableSize / 2); 281 window.end = Math.min(this._parentElement.clientWidth, middle + WebInspector.OverviewGrid.MinSelectableSize / 2); 282 } else if (window.end - window.start < WebInspector.OverviewGrid.MinSelectableSize) { 283 if (this._parentElement.clientWidth - window.end > WebInspector.OverviewGrid.MinSelectableSize) 284 window.end = window.start + WebInspector.OverviewGrid.MinSelectableSize; 285 else 286 window.start = window.end - WebInspector.OverviewGrid.MinSelectableSize; 287 } 288 this._setWindowPosition(window.start, window.end); 289 }, 290 291 /** 292 * @param {!Event} event 293 * @return {boolean} 294 */ 295 _startWindowDragging: function(event) 296 { 297 this._dragStartPoint = event.pageX; 298 this._dragStartLeft = this.windowLeft; 299 this._dragStartRight = this.windowRight; 300 return true; 301 }, 302 303 /** 304 * @param {!Event} event 305 */ 306 _windowDragging: function(event) 307 { 308 event.preventDefault(); 309 var delta = (event.pageX - this._dragStartPoint) / this._parentElement.clientWidth; 310 if (this._dragStartLeft + delta < 0) 311 delta = -this._dragStartLeft; 312 313 if (this._dragStartRight + delta > 1) 314 delta = 1 - this._dragStartRight; 315 316 this._setWindow(this._dragStartLeft + delta, this._dragStartRight + delta); 317 }, 318 319 /** 320 * @param {number} start 321 */ 322 _resizeWindowLeft: function(start) 323 { 324 // Glue to edge. 325 if (start < 10) 326 start = 0; 327 else if (start > this._rightResizeElement.offsetLeft - 4) 328 start = this._rightResizeElement.offsetLeft - 4; 329 this._setWindowPosition(start, null); 330 }, 331 332 /** 333 * @param {number} end 334 */ 335 _resizeWindowRight: function(end) 336 { 337 // Glue to edge. 338 if (end > this._parentElement.clientWidth - 10) 339 end = this._parentElement.clientWidth; 340 else if (end < this._leftResizeElement.offsetLeft + WebInspector.OverviewGrid.MinSelectableSize) 341 end = this._leftResizeElement.offsetLeft + WebInspector.OverviewGrid.MinSelectableSize; 342 this._setWindowPosition(null, end); 343 }, 344 345 _resizeWindowMaximum: function() 346 { 347 this._setWindowPosition(0, this._parentElement.clientWidth); 348 }, 349 350 /** 351 * @param {number} windowLeft 352 * @param {number} windowRight 353 */ 354 _setWindow: function(windowLeft, windowRight) 355 { 356 var left = windowLeft; 357 var right = windowRight; 358 var width = windowRight - windowLeft; 359 360 // We allow actual time window to be arbitrarily small but don't want the UI window to be too small. 361 var widthInPixels = width * this._parentElement.clientWidth; 362 var minWidthInPixels = WebInspector.OverviewGrid.MinSelectableSize / 2; 363 if (widthInPixels < minWidthInPixels) { 364 var factor = minWidthInPixels / widthInPixels; 365 left = ((windowRight + windowLeft) - width * factor) / 2; 366 right = ((windowRight + windowLeft) + width * factor) / 2; 367 } 368 369 this.windowLeft = windowLeft; 370 this._leftResizeElement.style.left = left * 100 + "%"; 371 this.windowRight = windowRight; 372 this._rightResizeElement.style.left = right * 100 + "%"; 373 374 this._overviewWindowElement.style.left = left * 100 + "%"; 375 this._overviewWindowBordersElement.style.left = left * 100 + "%"; 376 this._overviewWindowElement.style.width = (right - left) * 100 + "%"; 377 this._overviewWindowBordersElement.style.right = (1 - right) * 100 + "%"; 378 379 this.dispatchEventToListeners(WebInspector.OverviewGrid.Events.WindowChanged); 380 }, 381 382 /** 383 * @param {?number} start 384 * @param {?number} end 385 */ 386 _setWindowPosition: function(start, end) 387 { 388 var clientWidth = this._parentElement.clientWidth; 389 var windowLeft = typeof start === "number" ? start / clientWidth : this.windowLeft; 390 var windowRight = typeof end === "number" ? end / clientWidth : this.windowRight; 391 this._setWindow(windowLeft, windowRight); 392 }, 393 394 /** 395 * @param {!Event} event 396 */ 397 _onMouseWheel: function(event) 398 { 399 if (typeof event.wheelDeltaY === "number" && event.wheelDeltaY) { 400 const zoomFactor = 1.1; 401 const mouseWheelZoomSpeed = 1 / 120; 402 403 var reference = event.offsetX / event.target.clientWidth; 404 this._zoom(Math.pow(zoomFactor, -event.wheelDeltaY * mouseWheelZoomSpeed), reference); 405 } 406 if (typeof event.wheelDeltaX === "number" && event.wheelDeltaX) { 407 var offset = Math.round(event.wheelDeltaX * WebInspector.OverviewGrid.WindowScrollSpeedFactor); 408 var windowLeft = this._leftResizeElement.offsetLeft + WebInspector.OverviewGrid.ResizerOffset; 409 var windowRight = this._rightResizeElement.offsetLeft + WebInspector.OverviewGrid.ResizerOffset; 410 411 if (windowLeft - offset < 0) 412 offset = windowLeft; 413 414 if (windowRight - offset > this._parentElement.clientWidth) 415 offset = windowRight - this._parentElement.clientWidth; 416 417 this._setWindowPosition(windowLeft - offset, windowRight - offset); 418 419 event.preventDefault(); 420 } 421 }, 422 423 /** 424 * @param {number} factor 425 * @param {number} reference 426 */ 427 _zoom: function(factor, reference) 428 { 429 var left = this.windowLeft; 430 var right = this.windowRight; 431 var windowSize = right - left; 432 var newWindowSize = factor * windowSize; 433 if (newWindowSize > 1) { 434 newWindowSize = 1; 435 factor = newWindowSize / windowSize; 436 } 437 left = reference + (left - reference) * factor; 438 left = Number.constrain(left, 0, 1 - newWindowSize); 439 440 right = reference + (right - reference) * factor; 441 right = Number.constrain(right, newWindowSize, 1); 442 this._setWindow(left, right); 443 }, 444 445 __proto__: WebInspector.Object.prototype 446} 447 448/** 449 * @constructor 450 */ 451WebInspector.OverviewGrid.WindowSelector = function(parent, position) 452{ 453 this._startPosition = position; 454 this._width = parent.offsetWidth; 455 this._windowSelector = document.createElement("div"); 456 this._windowSelector.className = "overview-grid-window-selector"; 457 this._windowSelector.style.left = this._startPosition + "px"; 458 this._windowSelector.style.right = this._width - this._startPosition + "px"; 459 parent.appendChild(this._windowSelector); 460} 461 462WebInspector.OverviewGrid.WindowSelector.prototype = { 463 _close: function(position) 464 { 465 position = Math.max(0, Math.min(position, this._width)); 466 this._windowSelector.remove(); 467 return this._startPosition < position ? {start: this._startPosition, end: position} : {start: position, end: this._startPosition}; 468 }, 469 470 _updatePosition: function(position) 471 { 472 position = Math.max(0, Math.min(position, this._width)); 473 if (position < this._startPosition) { 474 this._windowSelector.style.left = position + "px"; 475 this._windowSelector.style.right = this._width - this._startPosition + "px"; 476 } else { 477 this._windowSelector.style.left = this._startPosition + "px"; 478 this._windowSelector.style.right = this._width - position + "px"; 479 } 480 } 481} 482