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 */ 35 WebInspector.OverviewGrid = function(prefix) 36 { 37 this.element = document.createElement("div"); 38 this.element.className = "fill"; 39 this.element.id = prefix + "-overview-container"; 40 41 this._grid = new WebInspector.TimelineGrid(); 42 this._grid.element.id = prefix + "-overview-grid"; 43 this._grid.setScrollAndDividerTop(0, 0); 44 45 this.element.appendChild(this._grid.element); 46 47 this._window = new WebInspector.OverviewGrid.Window(this.element, this._grid.dividersLabelBarElement); 48 } 49 50 WebInspector.OverviewGrid.prototype = { 51 /** 52 * @return {number} 53 */ 54 clientWidth: function() 55 { 56 return this.element.clientWidth; 57 }, 58 59 /** 60 * @param {!WebInspector.TimelineGrid.Calculator} calculator 61 */ 62 updateDividers: function(calculator) 63 { 64 this._grid.updateDividers(calculator); 65 }, 66 67 /** 68 * @param {!Array.<Element>} dividers 69 */ 70 addEventDividers: function(dividers) 71 { 72 this._grid.addEventDividers(dividers); 73 }, 74 75 removeEventDividers: function() 76 { 77 this._grid.removeEventDividers(); 78 }, 79 80 /** 81 * @param {?number} start 82 * @param {?number} end 83 */ 84 setWindowPosition: function(start, end) 85 { 86 this._window._setWindowPosition(start, end); 87 }, 88 89 reset: function() 90 { 91 this._window.reset(); 92 }, 93 94 /** 95 * @return {number} 96 */ 97 windowLeft: function() 98 { 99 return this._window.windowLeft; 100 }, 101 102 /** 103 * @return {number} 104 */ 105 windowRight: function() 106 { 107 return this._window.windowRight; 108 }, 109 110 /** 111 * @param {number} left 112 * @param {number} right 113 */ 114 setWindow: function(left, right) 115 { 116 this._window._setWindow(left, right); 117 }, 118 119 /** 120 * @param {string} eventType 121 * @param {function(WebInspector.Event)} listener 122 * @param {Object=} thisObject 123 */ 124 addEventListener: function(eventType, listener, thisObject) 125 { 126 this._window.addEventListener(eventType, listener, thisObject); 127 }, 128 129 /** 130 * @param {!number} zoomFactor 131 * @param {!number} referencePoint 132 */ 133 zoom: function(zoomFactor, referencePoint) 134 { 135 this._window._zoom(zoomFactor, referencePoint); 136 }, 137 138 /** 139 * @param {boolean} enabled 140 */ 141 setResizeEnabled: function(enabled) 142 { 143 this._window._setEnabled(!!enabled); 144 } 145 } 146 147 148 WebInspector.OverviewGrid.MinSelectableSize = 14; 149 150 WebInspector.OverviewGrid.WindowScrollSpeedFactor = .3; 151 152 WebInspector.OverviewGrid.ResizerOffset = 3.5; // half pixel because offset values are not rounded but ceiled 153 154 /** 155 * @constructor 156 * @extends {WebInspector.Object} 157 * @param {Element} parentElement 158 * @param {Element} dividersLabelBarElement 159 */ 160 WebInspector.OverviewGrid.Window = function(parentElement, dividersLabelBarElement) 161 { 162 this._parentElement = parentElement; 163 this._dividersLabelBarElement = dividersLabelBarElement; 164 165 WebInspector.installDragHandle(this._parentElement, this._startWindowSelectorDragging.bind(this), this._windowSelectorDragging.bind(this), this._endWindowSelectorDragging.bind(this), "ew-resize"); 166 WebInspector.installDragHandle(this._dividersLabelBarElement, this._startWindowDragging.bind(this), this._windowDragging.bind(this), null, "move"); 167 168 this.windowLeft = 0.0; 169 this.windowRight = 1.0; 170 171 this._parentElement.addEventListener("mousewheel", this._onMouseWheel.bind(this), true); 172 this._parentElement.addEventListener("dblclick", this._resizeWindowMaximum.bind(this), true); 173 174 this._overviewWindowElement = parentElement.createChild("div", "overview-grid-window"); 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 188 WebInspector.OverviewGrid.Events = { 189 WindowChanged: "WindowChanged" 190 } 191 192 WebInspector.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 this._parentElement.enableStyleClass("resize-enabled", enabled); 217 }, 218 219 /** 220 * @param {Event} event 221 */ 222 _resizerElementStartDragging: function(event) 223 { 224 if (!this._enabled) 225 return false; 226 this._resizerParentOffsetLeft = event.pageX - event.offsetX - event.target.offsetLeft; 227 event.preventDefault(); 228 return true; 229 }, 230 231 /** 232 * @param {Event} event 233 */ 234 _leftResizeElementDragging: function(event) 235 { 236 this._resizeWindowLeft(event.pageX - this._resizerParentOffsetLeft); 237 event.preventDefault(); 238 }, 239 240 /** 241 * @param {Event} event 242 */ 243 _rightResizeElementDragging: function(event) 244 { 245 this._resizeWindowRight(event.pageX - this._resizerParentOffsetLeft); 246 event.preventDefault(); 247 }, 248 249 /** 250 * @param {Event} event 251 * @return {boolean} 252 */ 253 _startWindowSelectorDragging: function(event) 254 { 255 if (!this._enabled) 256 return false; 257 this._offsetLeft = event.pageX - event.offsetX; 258 var position = event.pageX - this._offsetLeft; 259 this._overviewWindowSelector = new WebInspector.OverviewGrid.WindowSelector(this._parentElement, position); 260 return true; 261 }, 262 263 /** 264 * @param {Event} event 265 */ 266 _windowSelectorDragging: function(event) 267 { 268 this._overviewWindowSelector._updatePosition(event.pageX - this._offsetLeft); 269 event.preventDefault(); 270 }, 271 272 /** 273 * @param {Event} event 274 */ 275 _endWindowSelectorDragging: function(event) 276 { 277 var window = this._overviewWindowSelector._close(event.pageX - this._offsetLeft); 278 delete this._overviewWindowSelector; 279 if (window.end === window.start) { // Click, not drag. 280 var middle = window.end; 281 window.start = Math.max(0, middle - WebInspector.OverviewGrid.MinSelectableSize / 2); 282 window.end = Math.min(this._parentElement.clientWidth, middle + WebInspector.OverviewGrid.MinSelectableSize / 2); 283 } else if (window.end - window.start < WebInspector.OverviewGrid.MinSelectableSize) { 284 if (this._parentElement.clientWidth - window.end > WebInspector.OverviewGrid.MinSelectableSize) 285 window.end = window.start + WebInspector.OverviewGrid.MinSelectableSize; 286 else 287 window.start = window.end - WebInspector.OverviewGrid.MinSelectableSize; 288 } 289 this._setWindowPosition(window.start, window.end); 290 }, 291 292 /** 293 * @param {Event} event 294 * @return {boolean} 295 */ 296 _startWindowDragging: function(event) 297 { 298 this._dragStartPoint = event.pageX; 299 this._dragStartLeft = this.windowLeft; 300 this._dragStartRight = this.windowRight; 301 return true; 302 }, 303 304 /** 305 * @param {Event} event 306 */ 307 _windowDragging: function(event) 308 { 309 event.preventDefault(); 310 var delta = (event.pageX - this._dragStartPoint) / this._parentElement.clientWidth; 311 if (this._dragStartLeft + delta < 0) 312 delta = -this._dragStartLeft; 313 314 if (this._dragStartRight + delta > 1) 315 delta = 1 - this._dragStartRight; 316 317 this._setWindow(this._dragStartLeft + delta, this._dragStartRight + delta); 318 }, 319 320 /** 321 * @param {number} start 322 */ 323 _resizeWindowLeft: function(start) 324 { 325 // Glue to edge. 326 if (start < 10) 327 start = 0; 328 else if (start > this._rightResizeElement.offsetLeft - 4) 329 start = this._rightResizeElement.offsetLeft - 4; 330 this._setWindowPosition(start, null); 331 }, 332 333 /** 334 * @param {number} end 335 */ 336 _resizeWindowRight: function(end) 337 { 338 // Glue to edge. 339 if (end > this._parentElement.clientWidth - 10) 340 end = this._parentElement.clientWidth; 341 else if (end < this._leftResizeElement.offsetLeft + WebInspector.OverviewGrid.MinSelectableSize) 342 end = this._leftResizeElement.offsetLeft + WebInspector.OverviewGrid.MinSelectableSize; 343 this._setWindowPosition(null, end); 344 }, 345 346 _resizeWindowMaximum: function() 347 { 348 this._setWindowPosition(0, this._parentElement.clientWidth); 349 }, 350 351 /** 352 * @param {number} windowLeft 353 * @param {number} windowRight 354 */ 355 _setWindow: function(windowLeft, windowRight) 356 { 357 var left = windowLeft; 358 var right = windowRight; 359 var width = windowRight - windowLeft; 360 361 // We allow actual time window to be arbitrarily small but don't want the UI window to be too small. 362 var widthInPixels = width * this._parentElement.clientWidth; 363 var minWidthInPixels = WebInspector.OverviewGrid.MinSelectableSize / 2; 364 if (widthInPixels < minWidthInPixels) { 365 var factor = minWidthInPixels / widthInPixels; 366 left = ((windowRight + windowLeft) - width * factor) / 2; 367 right = ((windowRight + windowLeft) + width * factor) / 2; 368 } 369 370 this.windowLeft = windowLeft; 371 this._leftResizeElement.style.left = left * 100 + "%"; 372 this.windowRight = windowRight; 373 this._rightResizeElement.style.left = right * 100 + "%"; 374 375 this._overviewWindowElement.style.left = left * 100 + "%"; 376 this._overviewWindowBordersElement.style.left = left * 100 + "%"; 377 this._overviewWindowElement.style.width = (right - left) * 100 + "%"; 378 this._overviewWindowBordersElement.style.right = (1 - right) * 100 + "%"; 379 380 this.dispatchEventToListeners(WebInspector.OverviewGrid.Events.WindowChanged); 381 }, 382 383 /** 384 * @param {?number} start 385 * @param {?number} end 386 */ 387 _setWindowPosition: function(start, end) 388 { 389 var clientWidth = this._parentElement.clientWidth; 390 var windowLeft = typeof start === "number" ? start / clientWidth : this.windowLeft; 391 var windowRight = typeof end === "number" ? end / clientWidth : this.windowRight; 392 this._setWindow(windowLeft, windowRight); 393 }, 394 395 /** 396 * @param {Event} event 397 */ 398 _onMouseWheel: function(event) 399 { 400 if (typeof event.wheelDeltaY === "number" && event.wheelDeltaY) { 401 const zoomFactor = 1.1; 402 const mouseWheelZoomSpeed = 1 / 120; 403 404 var reference = event.offsetX / event.target.clientWidth; 405 this._zoom(Math.pow(zoomFactor, -event.wheelDeltaY * mouseWheelZoomSpeed), reference); 406 } 407 if (typeof event.wheelDeltaX === "number" && event.wheelDeltaX) { 408 var offset = Math.round(event.wheelDeltaX * WebInspector.OverviewGrid.WindowScrollSpeedFactor); 409 var windowLeft = this._leftResizeElement.offsetLeft + WebInspector.OverviewGrid.ResizerOffset; 410 var windowRight = this._rightResizeElement.offsetLeft + WebInspector.OverviewGrid.ResizerOffset; 411 412 if (windowLeft - offset < 0) 413 offset = windowLeft; 414 415 if (windowRight - offset > this._parentElement.clientWidth) 416 offset = windowRight - this._parentElement.clientWidth; 417 418 this._setWindowPosition(windowLeft - offset, windowRight - offset); 419 420 event.preventDefault(); 421 } 422 }, 423 424 /** 425 * @param {number} factor 426 * @param {number} reference 427 */ 428 _zoom: function(factor, reference) 429 { 430 var left = this.windowLeft; 431 var right = this.windowRight; 432 var windowSize = right - left; 433 var newWindowSize = factor * windowSize; 434 if (newWindowSize > 1) { 435 newWindowSize = 1; 436 factor = newWindowSize / windowSize; 437 } 438 left = reference + (left - reference) * factor; 439 left = Number.constrain(left, 0, 1 - newWindowSize); 440 441 right = reference + (right - reference) * factor; 442 right = Number.constrain(right, newWindowSize, 1); 443 this._setWindow(left, right); 444 }, 445 446 __proto__: WebInspector.Object.prototype 447 } 448 449 /** 450 * @constructor 451 */ 452 WebInspector.OverviewGrid.WindowSelector = function(parent, position) 453 { 454 this._startPosition = position; 455 this._width = parent.offsetWidth; 456 this._windowSelector = document.createElement("div"); 457 this._windowSelector.className = "overview-grid-window-selector"; 458 this._windowSelector.style.left = this._startPosition + "px"; 459 this._windowSelector.style.right = this._width - this._startPosition + "px"; 460 parent.appendChild(this._windowSelector); 461 } 462 463 WebInspector.OverviewGrid.WindowSelector.prototype = { 464 _createSelectorElement: function(parent, left, width, height) 465 { 466 var selectorElement = document.createElement("div"); 467 selectorElement.className = "overview-grid-window-selector"; 468 selectorElement.style.left = left + "px"; 469 selectorElement.style.width = width + "px"; 470 selectorElement.style.top = "0px"; 471 selectorElement.style.height = height + "px"; 472 parent.appendChild(selectorElement); 473 return selectorElement; 474 }, 475 476 _close: function(position) 477 { 478 position = Math.max(0, Math.min(position, this._width)); 479 this._windowSelector.remove(); 480 return this._startPosition < position ? {start: this._startPosition, end: position} : {start: position, end: this._startPosition}; 481 }, 482 483 _updatePosition: function(position) 484 { 485 position = Math.max(0, Math.min(position, this._width)); 486 if (position < this._startPosition) { 487 this._windowSelector.style.left = position + "px"; 488 this._windowSelector.style.right = this._width - this._startPosition + "px"; 489 } else { 490 this._windowSelector.style.left = this._startPosition + "px"; 491 this._windowSelector.style.right = this._width - position + "px"; 492 } 493 } 494 } 495