| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| 'use strict'; |
| |
| /** |
| * @fileoverview Interactive visualizaiton of TimelineModel objects |
| * based loosely on gantt charts. Each thread in the TimelineModel is given a |
| * set of TimelineTracks, one per subrow in the thread. The Timeline class |
| * acts as a controller, creating the individual tracks, while TimelineTracks |
| * do actual drawing. |
| * |
| * Visually, the Timeline produces (prettier) visualizations like the following: |
| * Thread1: AAAAAAAAAA AAAAA |
| * BBBB BB |
| * Thread2: CCCCCC CCCCC |
| * |
| */ |
| cr.define('tracing', function() { |
| |
| /** |
| * The TimelineViewport manages the transform used for navigating |
| * within the timeline. It is a simple transform: |
| * x' = (x+pan) * scale |
| * |
| * The timeline code tries to avoid directly accessing this transform, |
| * instead using this class to do conversion between world and view space, |
| * as well as the math for centering the viewport in various interesting |
| * ways. |
| * |
| * @constructor |
| * @extends {cr.EventTarget} |
| */ |
| function TimelineViewport(parentEl) { |
| this.parentEl_ = parentEl; |
| this.scaleX_ = 1; |
| this.panX_ = 0; |
| this.gridTimebase_ = 0; |
| this.gridStep_ = 1000 / 60; |
| this.gridEnabled_ = false; |
| this.hasCalledSetupFunction_ = false; |
| |
| this.onResizeBoundToThis_ = this.onResize_.bind(this); |
| |
| // The following code uses an interval to detect when the parent element |
| // is attached to the document. That is a trigger to run the setup function |
| // and install a resize listener. |
| this.checkForAttachInterval_ = setInterval( |
| this.checkForAttach_.bind(this), 250); |
| } |
| |
| TimelineViewport.prototype = { |
| __proto__: cr.EventTarget.prototype, |
| |
| /** |
| * Allows initialization of the viewport when the viewport's parent element |
| * has been attached to the document and given a size. |
| * @param {Function} fn Function to call when the viewport can be safely |
| * initialized. |
| */ |
| setWhenPossible: function(fn) { |
| this.pendingSetFunction_ = fn; |
| }, |
| |
| /** |
| * @return {boolean} Whether the current timeline is attached to the |
| * document. |
| */ |
| get isAttachedToDocument_() { |
| var cur = this.parentEl_; |
| while (cur.parentNode) |
| cur = cur.parentNode; |
| return cur == this.parentEl_.ownerDocument; |
| }, |
| |
| onResize_: function() { |
| this.dispatchChangeEvent(); |
| }, |
| |
| /** |
| * Checks whether the parentNode is attached to the document. |
| * When it is, it installs the iframe-based resize detection hook |
| * and then runs the pendingSetFunction_, if present. |
| */ |
| checkForAttach_: function() { |
| if (!this.isAttachedToDocument_ || this.clientWidth == 0) |
| return; |
| |
| if (!this.iframe_) { |
| this.iframe_ = document.createElement('iframe'); |
| this.iframe_.style.cssText = |
| 'position:absolute;width:100%;height:0;border:0;visibility:hidden;'; |
| this.parentEl_.appendChild(this.iframe_); |
| |
| this.iframe_.contentWindow.addEventListener('resize', |
| this.onResizeBoundToThis_); |
| } |
| |
| var curSize = this.clientWidth + 'x' + this.clientHeight; |
| if (this.pendingSetFunction_) { |
| this.lastSize_ = curSize; |
| this.pendingSetFunction_(); |
| this.pendingSetFunction_ = undefined; |
| } |
| |
| window.clearInterval(this.checkForAttachInterval_); |
| this.checkForAttachInterval_ = undefined; |
| }, |
| |
| /** |
| * Fires the change event on this viewport. Used to notify listeners |
| * to redraw when the underlying model has been mutated. |
| */ |
| dispatchChangeEvent: function() { |
| cr.dispatchSimpleEvent(this, 'change'); |
| }, |
| |
| detach: function() { |
| if (this.checkForAttachInterval_) { |
| window.clearInterval(this.checkForAttachInterval_); |
| this.checkForAttachInterval_ = undefined; |
| } |
| this.iframe_.removeEventListener('resize', this.onResizeBoundToThis_); |
| this.parentEl_.removeChild(this.iframe_); |
| }, |
| |
| get scaleX() { |
| return this.scaleX_; |
| }, |
| set scaleX(s) { |
| var changed = this.scaleX_ != s; |
| if (changed) { |
| this.scaleX_ = s; |
| this.dispatchChangeEvent(); |
| } |
| }, |
| |
| get panX() { |
| return this.panX_; |
| }, |
| set panX(p) { |
| var changed = this.panX_ != p; |
| if (changed) { |
| this.panX_ = p; |
| this.dispatchChangeEvent(); |
| } |
| }, |
| |
| setPanAndScale: function(p, s) { |
| var changed = this.scaleX_ != s || this.panX_ != p; |
| if (changed) { |
| this.scaleX_ = s; |
| this.panX_ = p; |
| this.dispatchChangeEvent(); |
| } |
| }, |
| |
| xWorldToView: function(x) { |
| return (x + this.panX_) * this.scaleX_; |
| }, |
| |
| xWorldVectorToView: function(x) { |
| return x * this.scaleX_; |
| }, |
| |
| xViewToWorld: function(x) { |
| return (x / this.scaleX_) - this.panX_; |
| }, |
| |
| xViewVectorToWorld: function(x) { |
| return x / this.scaleX_; |
| }, |
| |
| xPanWorldPosToViewPos: function(worldX, viewX, viewWidth) { |
| if (typeof viewX == 'string') { |
| if (viewX == 'left') { |
| viewX = 0; |
| } else if (viewX == 'center') { |
| viewX = viewWidth / 2; |
| } else if (viewX == 'right') { |
| viewX = viewWidth - 1; |
| } else { |
| throw Error('unrecognized string for viewPos. left|center|right'); |
| } |
| } |
| this.panX = (viewX / this.scaleX_) - worldX; |
| }, |
| |
| xPanWorldRangeIntoView: function(worldMin, worldMax, viewWidth) { |
| if (this.xWorldToView(worldMin) < 0) |
| this.xPanWorldPosToViewPos(worldMin, 'left', viewWidth); |
| else if (this.xWorldToView(worldMax) > viewWidth) |
| this.xPanWorldPosToViewPos(worldMax, 'right', viewWidth); |
| }, |
| |
| xSetWorldRange: function(worldMin, worldMax, viewWidth) { |
| var worldRange = worldMax - worldMin; |
| var scaleX = viewWidth / worldRange; |
| var panX = -worldMin; |
| this.setPanAndScale(panX, scaleX); |
| }, |
| |
| get gridEnabled() { |
| return this.gridEnabled_; |
| }, |
| |
| set gridEnabled(enabled) { |
| if (this.gridEnabled_ == enabled) |
| return; |
| this.gridEnabled_ = enabled && true; |
| this.dispatchChangeEvent(); |
| }, |
| |
| get gridTimebase() { |
| return this.gridTimebase_; |
| }, |
| |
| set gridTimebase(timebase) { |
| if (this.gridTimebase_ == timebase) |
| return; |
| this.gridTimebase_ = timebase; |
| cr.dispatchSimpleEvent(this, 'change'); |
| }, |
| |
| get gridStep() { |
| return this.gridStep_; |
| }, |
| |
| applyTransformToCanavs: function(ctx) { |
| ctx.transform(this.scaleX_, 0, 0, 1, this.panX_ * this.scaleX_, 0); |
| } |
| }; |
| |
| function TimelineSelectionSliceHit(track, slice) { |
| this.track = track; |
| this.slice = slice; |
| } |
| TimelineSelectionSliceHit.prototype = { |
| get selected() { |
| return this.slice.selected; |
| }, |
| set selected(v) { |
| this.slice.selected = v; |
| } |
| }; |
| |
| function TimelineSelectionCounterSampleHit(track, counter, sampleIndex) { |
| this.track = track; |
| this.counter = counter; |
| this.sampleIndex = sampleIndex; |
| } |
| TimelineSelectionCounterSampleHit.prototype = { |
| get selected() { |
| return this.track.selectedSamples[this.sampleIndex] == true; |
| }, |
| set selected(v) { |
| if (v) |
| this.track.selectedSamples[this.sampleIndex] = true; |
| else |
| this.track.selectedSamples[this.sampleIndex] = false; |
| this.track.invalidate(); |
| } |
| }; |
| |
| |
| /** |
| * Represents a selection within a Timeline and its associated set of tracks. |
| * @constructor |
| */ |
| function TimelineSelection() { |
| this.range_dirty_ = true; |
| this.range_ = {}; |
| this.length_ = 0; |
| } |
| TimelineSelection.prototype = { |
| __proto__: Object.prototype, |
| |
| get range() { |
| if (this.range_dirty_) { |
| var wmin = Infinity; |
| var wmax = -wmin; |
| for (var i = 0; i < this.length_; i++) { |
| var hit = this[i]; |
| if (hit.slice) { |
| wmin = Math.min(wmin, hit.slice.start); |
| wmax = Math.max(wmax, hit.slice.end); |
| } |
| } |
| this.range_ = { |
| min: wmin, |
| max: wmax |
| }; |
| this.range_dirty_ = false; |
| } |
| return this.range_; |
| }, |
| |
| get duration() { |
| return this.range.max - this.range.min; |
| }, |
| |
| get length() { |
| return this.length_; |
| }, |
| |
| clear: function() { |
| for (var i = 0; i < this.length_; ++i) |
| delete this[i]; |
| this.length_ = 0; |
| this.range_dirty_ = true; |
| }, |
| |
| push_: function(hit) { |
| this[this.length_++] = hit; |
| this.range_dirty_ = true; |
| return hit; |
| }, |
| |
| addSlice: function(track, slice) { |
| return this.push_(new TimelineSelectionSliceHit(track, slice)); |
| }, |
| |
| addCounterSample: function(track, counter, sampleIndex) { |
| return this.push_( |
| new TimelineSelectionCounterSampleHit( |
| track, counter, sampleIndex)); |
| }, |
| |
| subSelection: function(index, count) { |
| count = count || 1; |
| |
| var selection = new TimelineSelection(); |
| selection.range_dirty_ = true; |
| if (index < 0 || index + count > this.length_) |
| throw 'Index out of bounds'; |
| |
| for (var i = index; i < index + count; i++) |
| selection.push_(this[i]); |
| |
| return selection; |
| }, |
| |
| getCounterSampleHits: function() { |
| var selection = new TimelineSelection(); |
| for (var i = 0; i < this.length_; i++) |
| if (this[i] instanceof TimelineSelectionCounterSampleHit) |
| selection.push_(this[i]); |
| return selection; |
| }, |
| |
| getSliceHits: function() { |
| var selection = new TimelineSelection(); |
| for (var i = 0; i < this.length_; i++) |
| if (this[i] instanceof TimelineSelectionSliceHit) |
| selection.push_(this[i]); |
| return selection; |
| }, |
| |
| map: function(fn) { |
| for (var i = 0; i < this.length_; i++) |
| fn(this[i]); |
| }, |
| |
| /** |
| * Helper for selection previous or next. |
| * @param {boolean} forwardp If true, select one forward (next). |
| * Else, select previous. |
| * @return {boolean} true if current selection changed. |
| */ |
| getShiftedSelection: function(offset) { |
| var newSelection = new TimelineSelection(); |
| for (var i = 0; i < this.length_; i++) { |
| var hit = this[i]; |
| hit.track.addItemNearToProvidedHitToSelection( |
| hit, offset, newSelection); |
| } |
| |
| if (newSelection.length == 0) |
| return undefined; |
| return newSelection; |
| }, |
| }; |
| |
| /** |
| * Renders a TimelineModel into a div element, making one |
| * TimelineTrack for each subrow in each thread of the model, managing |
| * overall track layout, and handling user interaction with the |
| * viewport. |
| * |
| * @constructor |
| * @extends {HTMLDivElement} |
| */ |
| var Timeline = cr.ui.define('div'); |
| |
| Timeline.prototype = { |
| __proto__: HTMLDivElement.prototype, |
| |
| model_: null, |
| |
| decorate: function() { |
| this.classList.add('timeline'); |
| |
| this.viewport_ = new TimelineViewport(this); |
| this.viewportTrack = new tracing.TimelineViewportTrack(); |
| |
| this.tracks_ = this.ownerDocument.createElement('div'); |
| this.appendChild(this.tracks_); |
| |
| this.dragBox_ = this.ownerDocument.createElement('div'); |
| this.dragBox_.className = 'timeline-drag-box'; |
| this.appendChild(this.dragBox_); |
| this.hideDragBox_(); |
| |
| this.bindEventListener_(document, 'keypress', this.onKeypress_, this); |
| this.bindEventListener_(document, 'keydown', this.onKeydown_, this); |
| this.bindEventListener_(document, 'mousedown', this.onMouseDown_, this); |
| this.bindEventListener_(document, 'mousemove', this.onMouseMove_, this); |
| this.bindEventListener_(document, 'mouseup', this.onMouseUp_, this); |
| this.bindEventListener_(document, 'dblclick', this.onDblClick_, this); |
| |
| this.lastMouseViewPos_ = {x: 0, y: 0}; |
| |
| this.selection_ = new TimelineSelection(); |
| }, |
| |
| /** |
| * Wraps the standard addEventListener but automatically binds the provided |
| * func to the provided target, tracking the resulting closure. When detach |
| * is called, these listeners will be automatically removed. |
| */ |
| bindEventListener_: function(object, event, func, target) { |
| if (!this.boundListeners_) |
| this.boundListeners_ = []; |
| var boundFunc = func.bind(target); |
| this.boundListeners_.push({object: object, |
| event: event, |
| boundFunc: boundFunc}); |
| object.addEventListener(event, boundFunc); |
| }, |
| |
| detach: function() { |
| for (var i = 0; i < this.tracks_.children.length; i++) |
| this.tracks_.children[i].detach(); |
| |
| for (var i = 0; i < this.boundListeners_.length; i++) { |
| var binding = this.boundListeners_[i]; |
| binding.object.removeEventListener(binding.event, binding.boundFunc); |
| } |
| this.boundListeners_ = undefined; |
| this.viewport_.detach(); |
| }, |
| |
| get viewport() { |
| return this.viewport_; |
| }, |
| |
| get model() { |
| return this.model_; |
| }, |
| |
| set model(model) { |
| if (!model) |
| throw Error('Model cannot be null'); |
| if (this.model) { |
| throw Error('Cannot set model twice.'); |
| } |
| this.model_ = model; |
| |
| // Figure out all the headings. |
| var allHeadings = []; |
| model.getAllThreads().forEach(function(t) { |
| allHeadings.push(t.userFriendlyName); |
| }); |
| model.getAllCounters().forEach(function(c) { |
| allHeadings.push(c.name); |
| }); |
| model.getAllCpus().forEach(function(c) { |
| allHeadings.push('CPU ' + c.cpuNumber); |
| }); |
| |
| // Figure out the maximum heading size. |
| var maxHeadingWidth = 0; |
| var measuringStick = new tracing.MeasuringStick(); |
| var headingEl = document.createElement('div'); |
| headingEl.style.position = 'fixed'; |
| headingEl.className = 'timeline-canvas-based-track-title'; |
| allHeadings.forEach(function(text) { |
| headingEl.textContent = text + ':__'; |
| var w = measuringStick.measure(headingEl).width; |
| // Limit heading width to 300px. |
| if (w > 300) |
| w = 300; |
| if (w > maxHeadingWidth) |
| maxHeadingWidth = w; |
| }); |
| maxHeadingWidth = maxHeadingWidth + 'px'; |
| |
| // Reset old tracks. |
| for (var i = 0; i < this.tracks_.children.length; i++) |
| this.tracks_.children[i].detach(); |
| this.tracks_.textContent = ''; |
| |
| // Set up the viewport track |
| this.viewportTrack.headingWidth = maxHeadingWidth; |
| this.viewportTrack.viewport = this.viewport_; |
| |
| // Get a sorted list of CPUs |
| var cpus = model.getAllCpus(); |
| cpus.sort(tracing.TimelineCpu.compare); |
| |
| // Create tracks for each CPU. |
| cpus.forEach(function(cpu) { |
| var track = new tracing.TimelineCpuTrack(); |
| track.heading = 'CPU ' + cpu.cpuNumber + ':'; |
| track.headingWidth = maxHeadingWidth; |
| track.viewport = this.viewport_; |
| track.cpu = cpu; |
| this.tracks_.appendChild(track); |
| |
| for (var counterName in cpu.counters) { |
| var counter = cpu.counters[counterName]; |
| track = new tracing.TimelineCounterTrack(); |
| track.heading = 'CPU ' + cpu.cpuNumber + ' ' + counter.name + ':'; |
| track.headingWidth = maxHeadingWidth; |
| track.viewport = this.viewport_; |
| track.counter = counter; |
| this.tracks_.appendChild(track); |
| } |
| }.bind(this)); |
| |
| // Get a sorted list of processes. |
| var processes = model.getAllProcesses(); |
| processes.sort(tracing.TimelineProcess.compare); |
| |
| // Create tracks for each process. |
| processes.forEach(function(process) { |
| // Add counter tracks for this process. |
| var counters = []; |
| for (var tid in process.counters) |
| counters.push(process.counters[tid]); |
| counters.sort(tracing.TimelineCounter.compare); |
| |
| // Create the counters for this process. |
| counters.forEach(function(counter) { |
| var track = new tracing.TimelineCounterTrack(); |
| track.heading = counter.name + ':'; |
| track.headingWidth = maxHeadingWidth; |
| track.viewport = this.viewport_; |
| track.counter = counter; |
| this.tracks_.appendChild(track); |
| }.bind(this)); |
| |
| // Get a sorted list of threads. |
| var threads = []; |
| for (var tid in process.threads) |
| threads.push(process.threads[tid]); |
| threads.sort(tracing.TimelineThread.compare); |
| |
| // Create the threads. |
| threads.forEach(function(thread) { |
| var track = new tracing.TimelineThreadTrack(); |
| track.heading = thread.userFriendlyName + ':'; |
| track.tooltip = thread.userFriendlyDetails; |
| track.headingWidth = maxHeadingWidth; |
| track.viewport = this.viewport_; |
| track.thread = thread; |
| this.tracks_.appendChild(track); |
| }.bind(this)); |
| }.bind(this)); |
| |
| // Set up a reasonable viewport. |
| this.viewport_.setWhenPossible(function() { |
| var w = this.firstCanvas.width; |
| this.viewport_.xSetWorldRange(this.model_.minTimestamp, |
| this.model_.maxTimestamp, |
| w); |
| }.bind(this)); |
| }, |
| |
| /** |
| * @param {TimelineFilter} filter The filter to use for finding matches. |
| * @param {TimelineSelection} selection The selection to add matches to. |
| * @return {Array} An array of objects that match the provided |
| * TimelineFilter. |
| */ |
| addAllObjectsMatchingFilterToSelection: function(filter, selection) { |
| for (var i = 0; i < this.tracks_.children.length; ++i) |
| this.tracks_.children[i].addAllObjectsMatchingFilterToSelection( |
| filter, selection); |
| }, |
| |
| /** |
| * @return {Element} The element whose focused state determines |
| * whether to respond to keyboard inputs. |
| * Defaults to the parent element. |
| */ |
| get focusElement() { |
| if (this.focusElement_) |
| return this.focusElement_; |
| return this.parentElement; |
| }, |
| |
| /** |
| * Sets the element whose focus state will determine whether |
| * to respond to keybaord input. |
| */ |
| set focusElement(value) { |
| this.focusElement_ = value; |
| }, |
| |
| get listenToKeys_() { |
| if (!this.viewport_.isAttachedToDocument_) |
| return false; |
| if (!this.focusElement_) |
| return true; |
| if (this.focusElement.tabIndex >= 0) |
| return document.activeElement == this.focusElement; |
| return true; |
| }, |
| |
| onKeypress_: function(e) { |
| var vp = this.viewport_; |
| if (!this.firstCanvas) |
| return; |
| if (!this.listenToKeys_) |
| return; |
| var viewWidth = this.firstCanvas.clientWidth; |
| var curMouseV, curCenterW; |
| switch (e.keyCode) { |
| case 101: // e |
| var vX = this.lastMouseViewPos_.x; |
| var wX = vp.xViewToWorld(this.lastMouseViewPos_.x); |
| var distFromCenter = vX - (viewWidth / 2); |
| var percFromCenter = distFromCenter / viewWidth; |
| var percFromCenterSq = percFromCenter * percFromCenter; |
| vp.xPanWorldPosToViewPos(wX, 'center', viewWidth); |
| break; |
| case 119: // w |
| this.zoomBy_(1.5); |
| break; |
| case 115: // s |
| this.zoomBy_(1 / 1.5); |
| break; |
| case 103: // g |
| this.onGridToggle_(true); |
| break; |
| case 71: // G |
| this.onGridToggle_(false); |
| break; |
| case 87: // W |
| this.zoomBy_(10); |
| break; |
| case 83: // S |
| this.zoomBy_(1 / 10); |
| break; |
| case 97: // a |
| vp.panX += vp.xViewVectorToWorld(viewWidth * 0.1); |
| break; |
| case 100: // d |
| vp.panX -= vp.xViewVectorToWorld(viewWidth * 0.1); |
| break; |
| case 65: // A |
| vp.panX += vp.xViewVectorToWorld(viewWidth * 0.5); |
| break; |
| case 68: // D |
| vp.panX -= vp.xViewVectorToWorld(viewWidth * 0.5); |
| break; |
| } |
| }, |
| |
| // Not all keys send a keypress. |
| onKeydown_: function(e) { |
| if (!this.listenToKeys_) |
| return; |
| var sel; |
| switch (e.keyCode) { |
| case 37: // left arrow |
| sel = this.selection.getShiftedSelection(-1); |
| if (sel) { |
| this.setSelectionAndMakeVisible(sel); |
| e.preventDefault(); |
| } |
| break; |
| case 39: // right arrow |
| sel = this.selection.getShiftedSelection(1); |
| if (sel) { |
| this.setSelectionAndMakeVisible(sel); |
| e.preventDefault(); |
| } |
| break; |
| case 9: // TAB |
| if (this.focusElement.tabIndex == -1) { |
| if (e.shiftKey) |
| this.selectPrevious_(e); |
| else |
| this.selectNext_(e); |
| e.preventDefault(); |
| } |
| break; |
| } |
| }, |
| |
| /** |
| * Zoom in or out on the timeline by the given scale factor. |
| * @param {integer} scale The scale factor to apply. If <1, zooms out. |
| */ |
| zoomBy_: function(scale) { |
| if (!this.firstCanvas) |
| return; |
| var vp = this.viewport_; |
| var viewWidth = this.firstCanvas.clientWidth; |
| var curMouseV = this.lastMouseViewPos_.x; |
| var curCenterW = vp.xViewToWorld(curMouseV); |
| vp.scaleX = vp.scaleX * scale; |
| vp.xPanWorldPosToViewPos(curCenterW, curMouseV, viewWidth); |
| }, |
| |
| get keyHelp() { |
| var help = 'Keyboard shortcuts:\n' + |
| ' w/s : Zoom in/out (with shift: go faster)\n' + |
| ' a/d : Pan left/right\n' + |
| ' e : Center on mouse\n' + |
| ' g/G : Shows grid at the start/end of the selected task\n'; |
| |
| if (this.focusElement.tabIndex) { |
| help += ' <- : Select previous event on current timeline\n' + |
| ' -> : Select next event on current timeline\n'; |
| } else { |
| help += ' <-,^TAB : Select previous event on current timeline\n' + |
| ' ->, TAB : Select next event on current timeline\n'; |
| } |
| help += |
| '\n' + |
| 'Dbl-click to zoom in; Shift dbl-click to zoom out\n'; |
| return help; |
| }, |
| |
| get selection() { |
| return this.selection_; |
| }, |
| |
| set selection(selection) { |
| if (!(selection instanceof TimelineSelection)) |
| throw 'Expected TimelineSelection'; |
| |
| // Clear old selection. |
| var i; |
| for (i = 0; i < this.selection_.length; i++) |
| this.selection_[i].selected = false; |
| |
| this.selection_ = selection; |
| |
| cr.dispatchSimpleEvent(this, 'selectionChange'); |
| for (i = 0; i < this.selection_.length; i++) |
| this.selection_[i].selected = true; |
| this.viewport_.dispatchChangeEvent(); // Triggers a redraw. |
| }, |
| |
| setSelectionAndMakeVisible: function(selection, zoomAllowed) { |
| if (!(selection instanceof TimelineSelection)) |
| throw 'Expected TimelineSelection'; |
| this.selection = selection; |
| var range = this.selection.range; |
| var size = this.viewport_.xWorldVectorToView(range.max - range.min); |
| if (zoomAllowed && size < 50) { |
| var worldCenter = range.min + (range.max - range.min) * 0.5; |
| var worldRange = (range.max - range.min) * 5; |
| this.viewport_.xSetWorldRange(worldCenter - worldRange * 0.5, |
| worldCenter + worldRange * 0.5, |
| this.firstCanvas.width); |
| return; |
| } |
| |
| this.viewport_.xPanWorldRangeIntoView(range.min, range.max, |
| this.firstCanvas.width); |
| }, |
| |
| get firstCanvas() { |
| return this.tracks_.firstChild ? |
| this.tracks_.firstChild.firstCanvas : undefined; |
| }, |
| |
| hideDragBox_: function() { |
| this.dragBox_.style.left = '-1000px'; |
| this.dragBox_.style.top = '-1000px'; |
| this.dragBox_.style.width = 0; |
| this.dragBox_.style.height = 0; |
| }, |
| |
| setDragBoxPosition_: function(eDown, eCur) { |
| var loX = Math.min(eDown.clientX, eCur.clientX); |
| var hiX = Math.max(eDown.clientX, eCur.clientX); |
| var loY = Math.min(eDown.clientY, eCur.clientY); |
| var hiY = Math.max(eDown.clientY, eCur.clientY); |
| |
| this.dragBox_.style.left = loX + 'px'; |
| this.dragBox_.style.top = loY + 'px'; |
| this.dragBox_.style.width = hiX - loX + 'px'; |
| this.dragBox_.style.height = hiY - loY + 'px'; |
| |
| var canv = this.firstCanvas; |
| var loWX = this.viewport_.xViewToWorld(loX - canv.offsetLeft); |
| var hiWX = this.viewport_.xViewToWorld(hiX - canv.offsetLeft); |
| |
| var roundedDuration = Math.round((hiWX - loWX) * 100) / 100; |
| this.dragBox_.textContent = roundedDuration + 'ms'; |
| |
| var e = new cr.Event('selectionChanging'); |
| e.loWX = loWX; |
| e.hiWX = hiWX; |
| this.dispatchEvent(e); |
| }, |
| |
| onGridToggle_: function(left) { |
| var tb; |
| if (left) |
| tb = this.selection_.range.min; |
| else |
| tb = this.selection_.range.max; |
| |
| // Shift the timebase left until its just left of minTimestamp. |
| var numInterfvalsSinceStart = Math.ceil((tb - this.model_.minTimestamp) / |
| this.viewport_.gridStep_); |
| this.viewport_.gridTimebase = tb - |
| (numInterfvalsSinceStart + 1) * this.viewport_.gridStep_; |
| this.viewport_.gridEnabled = true; |
| }, |
| |
| onMouseDown_: function(e) { |
| var canv = this.firstCanvas; |
| var rect = this.tracks_.getClientRects()[0]; |
| var inside = rect && |
| e.clientX >= rect.left && |
| e.clientX < rect.right && |
| e.clientY >= rect.top && |
| e.clientY < rect.bottom && |
| e.x >= canv.offsetLeft; |
| if (!inside) |
| return; |
| |
| var pos = { |
| x: e.clientX - canv.offsetLeft, |
| y: e.clientY - canv.offsetTop |
| }; |
| |
| var wX = this.viewport_.xViewToWorld(pos.x); |
| |
| this.dragBeginEvent_ = e; |
| e.preventDefault(); |
| if (this.focusElement.tabIndex >= 0) |
| this.focusElement.focus(); |
| }, |
| |
| onMouseMove_: function(e) { |
| if (!this.firstCanvas) |
| return; |
| var canv = this.firstCanvas; |
| var pos = { |
| x: e.clientX - canv.offsetLeft, |
| y: e.clientY - canv.offsetTop |
| }; |
| |
| // Remember position. Used during keyboard zooming. |
| this.lastMouseViewPos_ = pos; |
| |
| // Update the drag box |
| if (this.dragBeginEvent_) { |
| this.setDragBoxPosition_(this.dragBeginEvent_, e); |
| } |
| }, |
| |
| onMouseUp_: function(e) { |
| var i; |
| if (this.dragBeginEvent_) { |
| // Stop the dragging. |
| this.hideDragBox_(); |
| var eDown = this.dragBeginEvent_; |
| this.dragBeginEvent_ = null; |
| |
| // Figure out extents of the drag. |
| var loX = Math.min(eDown.clientX, e.clientX); |
| var hiX = Math.max(eDown.clientX, e.clientX); |
| var loY = Math.min(eDown.clientY, e.clientY); |
| var hiY = Math.max(eDown.clientY, e.clientY); |
| |
| // Convert to worldspace. |
| var canv = this.firstCanvas; |
| var loWX = this.viewport_.xViewToWorld(loX - canv.offsetLeft); |
| var hiWX = this.viewport_.xViewToWorld(hiX - canv.offsetLeft); |
| |
| // Figure out what has been hit. |
| var selection = new TimelineSelection(); |
| for (i = 0; i < this.tracks_.children.length; i++) { |
| var track = this.tracks_.children[i]; |
| |
| // Only check tracks that insersect the rect. |
| var trackClientRect = track.getBoundingClientRect(); |
| var a = Math.max(loY, trackClientRect.top); |
| var b = Math.min(hiY, trackClientRect.bottom); |
| if (a <= b) { |
| track.addIntersectingItemsInRangeToSelection( |
| loWX, hiWX, loY, hiY, selection); |
| } |
| } |
| // Activate the new selection. |
| this.selection = selection; |
| } |
| }, |
| |
| onDblClick_: function(e) { |
| var canv = this.firstCanvas; |
| if (e.x < canv.offsetLeft) |
| return; |
| |
| var scale = 4; |
| if (e.shiftKey) |
| scale = 1 / scale; |
| this.zoomBy_(scale); |
| e.preventDefault(); |
| } |
| }; |
| |
| /** |
| * The TimelineModel being viewed by the timeline |
| * @type {TimelineModel} |
| */ |
| cr.defineProperty(Timeline, 'model', cr.PropertyKind.JS); |
| |
| return { |
| Timeline: Timeline, |
| TimelineSelectionSliceHit: TimelineSelectionSliceHit, |
| TimelineSelectionCounterSampleHit: TimelineSelectionCounterSampleHit, |
| TimelineSelection: TimelineSelection, |
| TimelineViewport: TimelineViewport |
| }; |
| }); |