Home | History | Annotate | Download | only in ui
      1 <!DOCTYPE html>
      2 <!--
      3 Copyright (c) 2012 The Chromium Authors. All rights reserved.
      4 Use of this source code is governed by a BSD-style license that can be
      5 found 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  */
     25 tr.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 
    430