1<!DOCTYPE html>
2<!--
3Copyright (c) 2012 The Chromium Authors. All rights reserved.
4Use of this source code is governed by a BSD-style license that can be
5found in the LICENSE file.
6-->
7
8<link rel="import" href="/tracing/base/event.html">
9<link rel="import" href="/tracing/model/event_set.html">
10<link rel="import" href="/tracing/ui/base/animation.html">
11<link rel="import" href="/tracing/ui/base/animation_controller.html">
12<link rel="import" href="/tracing/ui/base/dom_helpers.html">
13<link rel="import" href="/tracing/ui/base/draw_helpers.html">
14<link rel="import" href="/tracing/ui/timeline_interest_range.html">
15<link rel="import" href="/tracing/ui/timeline_display_transform.html">
16<link rel="import" href="/tracing/ui/tracks/container_to_track_map.html">
17<link rel="import" href="/tracing/ui/tracks/event_to_track_map.html">
18
19<script>
20'use strict';
21
22/**
23 * @fileoverview Code for the viewport.
24 */
25tr.exportTo('tr.ui', function() {
26  var TimelineDisplayTransform = tr.ui.TimelineDisplayTransform;
27  var TimelineInterestRange = tr.ui.TimelineInterestRange;
28
29  /**
30   * The TimelineViewport manages the transform used for navigating
31   * within the timeline. It is a simple transform:
32   *   x' = (x+pan) * scale
33   *
34   * The timeline code tries to avoid directly accessing this transform,
35   * instead using this class to do conversion between world and viewspace,
36   * as well as the math for centering the viewport in various interesting
37   * ways.
38   *
39   * @constructor
40   * @extends {tr.b.EventTarget}
41   */
42  function TimelineViewport(parentEl) {
43    this.parentEl_ = parentEl;
44    this.modelTrackContainer_ = undefined;
45    this.currentDisplayTransform_ = new TimelineDisplayTransform();
46    this.initAnimationController_();
47
48    // Flow events
49    this.showFlowEvents_ = false;
50
51    // Highlights.
52    this.highlightVSync_ = false;
53
54    // High details.
55    this.highDetails_ = false;
56
57    // Grid system.
58    this.gridTimebase_ = 0;
59    this.gridStep_ = 1000 / 60;
60    this.gridEnabled_ = false;
61
62    // Init logic.
63    this.hasCalledSetupFunction_ = false;
64
65    this.onResize_ = this.onResize_.bind(this);
66    this.onModelTrackControllerScroll_ =
67        this.onModelTrackControllerScroll_.bind(this);
68
69    // The following code uses an interval to detect when the parent element
70    // is attached to the document. That is a trigger to run the setup function
71    // and install a resize listener.
72    this.checkForAttachInterval_ = setInterval(
73        this.checkForAttach_.bind(this), 250);
74
75    this.majorMarkPositions = [];
76    this.interestRange_ = new TimelineInterestRange(this);
77
78    this.eventToTrackMap_ = new tr.ui.tracks.EventToTrackMap();
79    this.containerToTrackMap = new tr.ui.tracks.ContainerToTrackMap();
80  }
81
82  TimelineViewport.prototype = {
83    __proto__: tr.b.EventTarget.prototype,
84
85    /**
86     * Allows initialization of the viewport when the viewport's parent element
87     * has been attached to the document and given a size.
88     * @param {Function} fn Function to call when the viewport can be safely
89     * initialized.
90     */
91    setWhenPossible: function(fn) {
92      this.pendingSetFunction_ = fn;
93    },
94
95    /**
96     * @return {boolean} Whether the current timeline is attached to the
97     * document.
98     */
99    get isAttachedToDocumentOrInTestMode() {
100      // Allow not providing a parent element, used by tests.
101      if (this.parentEl_ === undefined)
102        return;
103      return tr.ui.b.isElementAttachedToDocument(this.parentEl_);
104    },
105
106    onResize_: function() {
107      this.dispatchChangeEvent();
108    },
109
110    /**
111     * Checks whether the parentNode is attached to the document.
112     * When it is, it installs the iframe-based resize detection hook
113     * and then runs the pendingSetFunction_, if present.
114     */
115    checkForAttach_: function() {
116      if (!this.isAttachedToDocumentOrInTestMode || this.clientWidth == 0)
117        return;
118
119      if (!this.iframe_) {
120        this.iframe_ = document.createElement('iframe');
121        this.iframe_.style.cssText =
122            'position:absolute;width:100%;height:0;border:0;visibility:hidden;';
123        this.parentEl_.appendChild(this.iframe_);
124
125        this.iframe_.contentWindow.addEventListener('resize', this.onResize_);
126      }
127
128      var curSize = this.parentEl_.clientWidth + 'x' +
129          this.parentEl_.clientHeight;
130      if (this.pendingSetFunction_) {
131        this.lastSize_ = curSize;
132        try {
133          this.pendingSetFunction_();
134        } catch (ex) {
135          console.log('While running setWhenPossible:',
136              ex.message ? ex.message + '\n' + ex.stack : ex.stack);
137        }
138        this.pendingSetFunction_ = undefined;
139      }
140
141      window.clearInterval(this.checkForAttachInterval_);
142      this.checkForAttachInterval_ = undefined;
143    },
144
145    /**
146     * Fires the change event on this viewport. Used to notify listeners
147     * to redraw when the underlying model has been mutated.
148     */
149    dispatchChangeEvent: function() {
150      tr.b.dispatchSimpleEvent(this, 'change');
151    },
152
153    detach: function() {
154      if (this.checkForAttachInterval_) {
155        window.clearInterval(this.checkForAttachInterval_);
156        this.checkForAttachInterval_ = undefined;
157      }
158      if (this.iframe_) {
159        this.iframe_.removeEventListener('resize', this.onResize_);
160        this.parentEl_.removeChild(this.iframe_);
161      }
162    },
163
164    initAnimationController_: function() {
165      this.dtAnimationController_ = new tr.ui.b.AnimationController();
166      this.dtAnimationController_.addEventListener(
167          'didtick', function(e) {
168            this.onCurentDisplayTransformChange_(e.oldTargetState);
169          }.bind(this));
170
171      var that = this;
172      this.dtAnimationController_.target = {
173        get panX() {
174          return that.currentDisplayTransform_.panX;
175        },
176
177        set panX(panX) {
178          that.currentDisplayTransform_.panX = panX;
179        },
180
181        get panY() {
182          return that.currentDisplayTransform_.panY;
183        },
184
185        set panY(panY) {
186          that.currentDisplayTransform_.panY = panY;
187        },
188
189        get scaleX() {
190          return that.currentDisplayTransform_.scaleX;
191        },
192
193        set scaleX(scaleX) {
194          that.currentDisplayTransform_.scaleX = scaleX;
195        },
196
197        cloneAnimationState: function() {
198          return that.currentDisplayTransform_.clone();
199        },
200
201        xPanWorldPosToViewPos: function(xWorld, xView) {
202          that.currentDisplayTransform_.xPanWorldPosToViewPos(
203              xWorld, xView, that.modelTrackContainer_.canvas.clientWidth);
204        }
205      };
206    },
207
208    get currentDisplayTransform() {
209      return this.currentDisplayTransform_;
210    },
211
212    setDisplayTransformImmediately: function(displayTransform) {
213      this.dtAnimationController_.cancelActiveAnimation();
214
215      var oldDisplayTransform =
216          this.dtAnimationController_.target.cloneAnimationState();
217      this.currentDisplayTransform_.set(displayTransform);
218      this.onCurentDisplayTransformChange_(oldDisplayTransform);
219    },
220
221    queueDisplayTransformAnimation: function(animation) {
222      if (!(animation instanceof tr.ui.b.Animation))
223        throw new Error('animation must be instanceof tr.ui.b.Animation');
224      this.dtAnimationController_.queueAnimation(animation);
225    },
226
227    onCurentDisplayTransformChange_: function(oldDisplayTransform) {
228      // Ensure panY stays clamped in the track container's scroll range.
229      if (this.modelTrackContainer_) {
230        this.currentDisplayTransform.panY = tr.b.clamp(
231            this.currentDisplayTransform.panY,
232            0,
233            this.modelTrackContainer_.scrollHeight -
234                this.modelTrackContainer_.clientHeight);
235      }
236
237      var changed = !this.currentDisplayTransform.equals(oldDisplayTransform);
238      var yChanged = this.currentDisplayTransform.panY !==
239          oldDisplayTransform.panY;
240      if (yChanged)
241        this.modelTrackContainer_.scrollTop = this.currentDisplayTransform.panY;
242      if (changed)
243        this.dispatchChangeEvent();
244    },
245
246    onModelTrackControllerScroll_: function(e) {
247      if (this.dtAnimationController_.activeAnimation &&
248          this.dtAnimationController_.activeAnimation.affectsPanY)
249        this.dtAnimationController_.cancelActiveAnimation();
250      var panY = this.modelTrackContainer_.scrollTop;
251      this.currentDisplayTransform_.panY = panY;
252    },
253
254    get modelTrackContainer() {
255      return this.modelTrackContainer_;
256    },
257
258    set modelTrackContainer(m) {
259      if (this.modelTrackContainer_)
260        this.modelTrackContainer_.removeEventListener('scroll',
261            this.onModelTrackControllerScroll_);
262
263      this.modelTrackContainer_ = m;
264      this.modelTrackContainer_.addEventListener('scroll',
265          this.onModelTrackControllerScroll_);
266    },
267
268    get showFlowEvents() {
269      return this.showFlowEvents_;
270    },
271
272    set showFlowEvents(showFlowEvents) {
273      this.showFlowEvents_ = showFlowEvents;
274      this.dispatchChangeEvent();
275    },
276
277    get highlightVSync() {
278      return this.highlightVSync_;
279    },
280
281    set highlightVSync(highlightVSync) {
282      this.highlightVSync_ = highlightVSync;
283      this.dispatchChangeEvent();
284    },
285
286    get highDetails() {
287      return this.highDetails_;
288    },
289
290    set highDetails(highDetails) {
291      this.highDetails_ = highDetails;
292      this.dispatchChangeEvent();
293    },
294
295    get gridEnabled() {
296      return this.gridEnabled_;
297    },
298
299    set gridEnabled(enabled) {
300      if (this.gridEnabled_ == enabled)
301        return;
302
303      this.gridEnabled_ = enabled && true;
304      this.dispatchChangeEvent();
305    },
306
307    get gridTimebase() {
308      return this.gridTimebase_;
309    },
310
311    set gridTimebase(timebase) {
312      if (this.gridTimebase_ == timebase)
313        return;
314      this.gridTimebase_ = timebase;
315      this.dispatchChangeEvent();
316    },
317
318    get gridStep() {
319      return this.gridStep_;
320    },
321
322    get interestRange() {
323      return this.interestRange_;
324    },
325
326    drawMajorMarkLines: function(ctx) {
327      // Apply subpixel translate to get crisp lines.
328      // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/
329      ctx.save();
330      ctx.translate((Math.round(ctx.lineWidth) % 2) / 2, 0);
331
332      ctx.beginPath();
333      for (var idx in this.majorMarkPositions) {
334        var x = Math.floor(this.majorMarkPositions[idx]);
335        tr.ui.b.drawLine(ctx, x, 0, x, ctx.canvas.height);
336      }
337      ctx.strokeStyle = '#ddd';
338      ctx.stroke();
339
340      ctx.restore();
341    },
342
343    drawGridLines: function(ctx, viewLWorld, viewRWorld) {
344      if (!this.gridEnabled)
345        return;
346
347      var dt = this.currentDisplayTransform;
348      var x = this.gridTimebase;
349
350      // Apply subpixel translate to get crisp lines.
351      // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/
352      ctx.save();
353      ctx.translate((Math.round(ctx.lineWidth) % 2) / 2, 0);
354
355      ctx.beginPath();
356      while (x < viewRWorld) {
357        if (x >= viewLWorld) {
358          // Do conversion to viewspace here rather than on
359          // x to avoid precision issues.
360          var vx = Math.floor(dt.xWorldToView(x));
361          tr.ui.b.drawLine(ctx, vx, 0, vx, ctx.canvas.height);
362        }
363
364        x += this.gridStep;
365      }
366      ctx.strokeStyle = 'rgba(255, 0, 0, 0.25)';
367      ctx.stroke();
368
369      ctx.restore();
370    },
371
372    /**
373     * Helper for selection previous or next.
374     * @param {boolean} offset If positive, select one forward (next).
375     *   Else, select previous.
376     *
377     * @return {boolean} true if current selection changed.
378     */
379    getShiftedSelection: function(selection, offset) {
380      var newSelection = new tr.model.EventSet();
381      for (var i = 0; i < selection.length; i++) {
382        var event = selection[i];
383
384        // If this is a flow event, then move to its slice based on the
385        // offset direction.
386        if (event instanceof tr.model.FlowEvent) {
387          if (offset > 0) {
388            newSelection.push(event.endSlice);
389          } else if (offset < 0) {
390            newSelection.push(event.startSlice);
391          } else {
392            /* Do nothing. Zero offsets don't do anything. */
393          }
394          continue;
395        }
396
397        var track = this.trackForEvent(event);
398        track.addEventNearToProvidedEventToSelection(
399            event, offset, newSelection);
400      }
401
402      if (newSelection.length == 0)
403        return undefined;
404      return newSelection;
405    },
406
407    rebuildEventToTrackMap: function() {
408      // TODO(charliea): Make the event to track map have a similar interface
409      // to the container to track map so that we can just clear() here.
410      this.eventToTrackMap_ = new tr.ui.tracks.EventToTrackMap();
411      this.modelTrackContainer_.addEventsToTrackMap(this.eventToTrackMap_);
412    },
413
414    rebuildContainerToTrackMap: function() {
415      this.containerToTrackMap.clear();
416      this.modelTrackContainer_.addContainersToTrackMap(
417          this.containerToTrackMap);
418    },
419
420    trackForEvent: function(event) {
421      return this.eventToTrackMap_[event.guid];
422    }
423  };
424
425  return {
426    TimelineViewport: TimelineViewport
427  };
428});
429</script>
430