1<!-- Copyright (C) 2020 The Android Open Source Project
2
3     Licensed under the Apache License, Version 2.0 (the "License");
4     you may not use this file except in compliance with the License.
5     You may obtain a copy of the License at
6
7          http://www.apache.org/licenses/LICENSE-2.0
8
9     Unless required by applicable law or agreed to in writing, software
10     distributed under the License is distributed on an "AS IS" BASIS,
11     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12     See the License for the specific language governing permissions and
13     limitations under the License.
14-->
15<template>
16  <div class="overlay" v-if="hasTimeline || video">
17    <div class="overlay-content" ref="overlayContent">
18      <draggable-div
19        ref="videoOverlay"
20        class="video-overlay"
21        v-show="minimized && showVideoOverlay"
22        position="bottomLeft"
23        :asyncLoad="true"
24        :resizeable="true"
25        v-on:requestExtraWidth="updateVideoOverlayWidth"
26        :style="videoOverlayStyle"
27        v-if="video"
28      >
29        <template slot="header">
30          <div class="close-video-overlay" @click="closeVideoOverlay">
31            <md-icon>
32              close
33              <md-tooltip md-direction="right">Close video overlay</md-tooltip>
34            </md-icon>
35          </div>
36        </template>
37        <template slot="main">
38          <div ref="overlayVideoContainer">
39            <videoview
40              ref="video"
41              :file="video"
42              :height="videoHeight"
43              @loaded="videoLoaded" />
44          </div>
45        </template>
46      </draggable-div>
47    </div>
48    <md-bottom-bar
49      class="bottom-nav"
50      v-if="hasTimeline || (video && !showVideoOverlay)"
51      ref="bottomNav"
52    >
53      <div class="nav-content">
54        <div class="">
55          <md-toolbar
56            md-elevation="0"
57            class="md-transparent">
58
59            <div class="toolbar" :class="{ expanded: expanded }">
60              <div class="resize-bar" v-show="expanded">
61                <div v-if="video" @mousedown="resizeBottomNav">
62                  <md-icon class="drag-handle">
63                    drag_handle
64                    <md-tooltip md-direction="top">resize</md-tooltip>
65                  </md-icon>
66                </div>
67              </div>
68
69              <div class="active-timeline" v-show="minimized">
70                <div
71                  class="active-timeline-icon"
72                  @click="$refs.navigationTypeSelection.$el
73                          .querySelector('input').click()"
74                >
75                  <md-icon class="collapsed-timeline-icon">
76                    {{ collapsedTimelineIcon }}
77                    <md-tooltip>
78                      {{ collapsedTimelineIconTooltip }}
79                    </md-tooltip>
80                  </md-icon>
81                </div>
82
83                <md-field
84                  ref="navigationTypeSelection"
85                  class="nagivation-style-selection-field"
86                >
87
88                  <label>Navigation</label>
89                  <md-select
90                    v-model="navigationStyle"
91                    name="navigationStyle"
92                    md-dense
93                  >
94                    <md-icon-option :value="NAVIGATION_STYLE.GLOBAL"
95                      icon="public"
96                      desc="Consider all timelines for navigation"
97                    />
98                    <md-icon-option
99                      :value="NAVIGATION_STYLE.FOCUSED"
100                      :icon="TRACE_ICONS[focusedFile.type]"
101                      :desc="`Automatically switch what timeline is considered
102                        for navigation based on what is visible on screen.
103                        Currently ${focusedFile.type}.`"
104                    />
105                    <!-- TODO: Add edit button for custom settings that opens
106                               popup dialog menu -->
107                    <md-icon-option
108                      :value="NAVIGATION_STYLE.CUSTOM"
109                      icon="dashboard_customize"
110                      desc="Considers only the enabled timelines for
111                            navigation. Expand the bottom bar to toggle
112                            timelines."
113                    />
114                    <md-optgroup label="Targeted">
115                      <md-icon-option
116                        v-for="file in timelineFiles"
117                        v-bind:key="file.type"
118                        :value="`${NAVIGATION_STYLE.TARGETED}-` +
119                                `${file.type}`"
120                        :displayValue="file.type"
121                        :shortValue="NAVIGATION_STYLE.TARGETED"
122                        :icon="TRACE_ICONS[file.type]"
123                        :desc="`Only consider ${file.type} ` +
124                               'for timeline navigation.'"
125                      />
126                    </md-optgroup>
127                  </md-select>
128                </md-field>
129              </div>
130
131              <div
132                class="minimized-timeline-content"
133                v-show="minimized"
134                v-if="hasTimeline"
135              >
136                <label>
137                  {{ seekTime }}
138                </label>
139                <timeline
140                  :timeline="Object.freeze(minimizedTimeline.timeline)"
141                  :selected-index="minimizedTimeline.selectedIndex"
142                  :scale="scale"
143                  :crop="crop"
144                  class="minimized-timeline"
145                />
146              </div>
147
148              <md-button
149                class="md-icon-button show-video-overlay-btn"
150                :class="{active: minimized && showVideoOverlay}"
151                @click="toggleVideoOverlay"
152                v-show="minimized"
153                style="margin-bottom: 10px;"
154              >
155                <i class="md-icon md-icon-font">
156                  featured_video
157                </i>
158                <md-tooltip md-direction="top">
159                  <span v-if="showVideoOverlay">Hide video overlay</span>
160                  <span v-else>Show video overlay</span>
161                </md-tooltip>
162              </md-button>
163
164              <md-button
165                class="md-icon-button toggle-btn"
166                @click="toggle"
167                style="margin-bottom: 10px;"
168              >
169                <md-icon v-if="minimized">
170                  expand_less
171                  <md-tooltip md-direction="top">Expand timeline</md-tooltip>
172                </md-icon>
173                <md-icon v-else>
174                  expand_more
175                  <md-tooltip md-direction="top">Collapse timeline</md-tooltip>
176                </md-icon>
177              </md-button>
178            </div>
179          </md-toolbar>
180
181          <div class="expanded-content" v-show="expanded">
182            <div :v-if="video">
183              <div
184                class="expanded-content-video"
185                ref="expandedContentVideoContainer"
186              >
187                <!-- Video moved here on expansion -->
188              </div>
189            </div>
190            <div class="flex-fill">
191              <div
192                ref="expandedTimeline"
193                :style="`padding-top: ${resizeOffset}px;`"
194              >
195                <div class="seek-time" v-if="seekTime">
196                  <b>Seek time</b>: {{ seekTime }}
197                </div>
198
199                <timelines
200                  :timelineFiles="timelineFiles"
201                  :scale="scale"
202                  :crop="crop"
203                  :cropIntent="cropIntent"
204                  v-on:crop="onTimelineCrop"
205                />
206
207                <div class="timeline-selection">
208                  <div class="timeline-selection-header">
209                    <label>Timeline Area Selection</label>
210                    <span class="material-icons help-icon">
211                      help_outline
212                      <md-tooltip md-direction="right">
213                        Select the area of the timeline to focus on.
214                        Click and drag to select.
215                      </md-tooltip>
216                    </span>
217                    <md-button
218                      class="md-primary"
219                      v-if="isCropped"
220                      @click.native="clearSelection"
221                    >
222                      Clear selection
223                    </md-button>
224                  </div>
225                  <timeline-selection
226                    :timeline="mergedTimeline.timeline"
227                    :start-timestamp="0"
228                    :end-timestamp="0"
229                    :scale="scale"
230                    :cropArea="crop"
231                    v-on:crop="onTimelineCrop"
232                    v-on:cropIntent="onTimelineCropIntent"
233                    v-on:showVideoAt="changeVideoTimestamp"
234                    v-on:resetVideoTimestamp="resetVideoTimestamp"
235                  />
236                </div>
237
238                <div class="help" v-if="!minimized">
239                  <div class="help-icon-wrapper">
240                    <span class="material-icons help-icon">
241                      help_outline
242                      <md-tooltip md-direction="left">
243                        Click on icons to disable timelines
244                      </md-tooltip>
245                    </span>
246                  </div>
247                </div>
248              </div>
249            </div>
250          </div>
251        </div>
252      </div>
253    </md-bottom-bar>
254  </div>
255</template>
256<script>
257import Timeline from './Timeline.vue';
258import Timelines from './Timelines.vue';
259import TimelineSelection from './TimelineSelection.vue';
260import DraggableDiv from './DraggableDiv.vue';
261import VideoView from './VideoView.vue';
262import MdIconOption from './components/IconSelection/IconSelectOption.vue';
263import FileType from './mixins/FileType.js';
264import {NAVIGATION_STYLE} from './utils/consts';
265import {TRACE_ICONS} from '@/decode.js';
266
267// eslint-disable-next-line camelcase
268import {nanos_to_string} from './transform.js';
269
270export default {
271  name: 'overlay',
272  props: ['store'],
273  mixins: [FileType],
274  data() {
275    return {
276      minimized: true,
277      // height of video in expanded timeline,
278      // made to match expandedTimeline dynamically
279      videoHeight: 'auto',
280      dragState: {
281        clientY: null,
282        lastDragEndPosition: null,
283      },
284      resizeOffset: 0,
285      showVideoOverlay: true,
286      mergedTimeline: null,
287      NAVIGATION_STYLE,
288      navigationStyle: this.store.navigationStyle,
289      videoOverlayExtraWidth: 0,
290      crop: null,
291      cropIntent: null,
292      TRACE_ICONS,
293    };
294  },
295  created() {
296    this.mergedTimeline = this.computeMergedTimeline();
297    this.$store.commit('setMergedTimeline', this.mergedTimeline);
298    this.updateNavigationFileFilter();
299  },
300  mounted() {
301    this.emitBottomHeightUpdate();
302  },
303  destroyed() {
304    this.$store.commit('removeMergedTimeline', this.mergedTimeline);
305  },
306  watch: {
307    navigationStyle(style) {
308      // Only store navigation type in local store if it's a type that will
309      // work regardless of what data is loaded.
310      if (style === NAVIGATION_STYLE.GLOBAL ||
311        style === NAVIGATION_STYLE.FOCUSED) {
312        this.store.navigationStyle = style;
313      }
314      this.updateNavigationFileFilter();
315    },
316    minimized() {
317      // Minimized toggled
318      this.updateNavigationFileFilter();
319
320      this.$nextTick(this.emitBottomHeightUpdate);
321    },
322  },
323  computed: {
324    video() {
325      return this.$store.getters.video;
326    },
327    videoOverlayStyle() {
328      return {
329        width: 150 + this.videoOverlayExtraWidth + 'px',
330      };
331    },
332    timelineFiles() {
333      return this.$store.getters.timelineFiles;
334    },
335    focusedFile() {
336      return this.$store.state.focusedFile;
337    },
338    expanded() {
339      return !this.minimized;
340    },
341    seekTime() {
342      return nanos_to_string(this.currentTimestamp);
343    },
344    scale() {
345      const mx = Math.max(...(this.timelineFiles.map((f) =>
346        Math.max(...f.timeline))));
347      const mi = Math.min(...(this.timelineFiles.map((f) =>
348        Math.min(...f.timeline))));
349      return [mi, mx];
350    },
351    currentTimestamp() {
352      return this.$store.state.currentTimestamp;
353    },
354    hasTimeline() {
355      // Returns true if a meaningful timeline exists (i.e. not only dumps)
356      for (const file of this.timelineFiles) {
357        if (file.timeline.length > 0 &&
358            (file.timeline[0] !== undefined || file.timeline.length > 1)) {
359          return true;
360        }
361      }
362
363      return false;
364    },
365    collapsedTimelineIconTooltip() {
366      switch (this.navigationStyle) {
367        case NAVIGATION_STYLE.GLOBAL:
368          return 'All timelines';
369
370        case NAVIGATION_STYLE.FOCUSED:
371          return `Focused: ${this.focusedFile.type}`;
372
373        case NAVIGATION_STYLE.CUSTOM:
374          return 'Enabled timelines';
375
376        default:
377          const split = this.navigationStyle.split('-');
378          if (split[0] !== NAVIGATION_STYLE.TARGETED) {
379            throw new Error('Unexpected nagivation type');
380          }
381
382          const fileType = split[1];
383
384          return fileType;
385      }
386    },
387    collapsedTimelineIcon() {
388      switch (this.navigationStyle) {
389        case NAVIGATION_STYLE.GLOBAL:
390          return 'public';
391
392        case NAVIGATION_STYLE.FOCUSED:
393          return TRACE_ICONS[this.focusedFile.type];
394
395        case NAVIGATION_STYLE.CUSTOM:
396          return 'dashboard_customize';
397
398        default:
399          const split = this.navigationStyle.split('-');
400          if (split[0] !== NAVIGATION_STYLE.TARGETED) {
401            throw new Error('Unexpected nagivation type');
402          }
403
404          const fileType = split[1];
405
406          return TRACE_ICONS[fileType];
407      }
408    },
409    minimizedTimeline() {
410      if (this.navigationStyle === NAVIGATION_STYLE.GLOBAL) {
411        return this.mergedTimeline;
412      }
413
414      if (this.navigationStyle === NAVIGATION_STYLE.FOCUSED) {
415        return this.focusedFile;
416      }
417
418      if (this.navigationStyle === NAVIGATION_STYLE.CUSTOM) {
419        // TODO: Return custom timeline
420        return this.mergedTimeline;
421      }
422
423      if (this.navigationStyle.split('-')[0] === NAVIGATION_STYLE.TARGETED) {
424        return this.$store.state
425            .traces[this.navigationStyle.split('-')[1]];
426      }
427
428      throw new Error('Unexpected Nagivation Style');
429    },
430    isCropped() {
431      return this.crop != null &&
432        (this.crop.left !== 0 || this.crop.right !== 1);
433    },
434  },
435  updated() {
436    this.$nextTick(() => {
437      if (this.$refs.expandedTimeline && this.expanded) {
438        this.videoHeight = this.$refs.expandedTimeline.clientHeight;
439      } else {
440        this.videoHeight = 'auto';
441      }
442    });
443  },
444  methods: {
445    emitBottomHeightUpdate() {
446      if (this.$refs.bottomNav) {
447        const newHeight = this.$refs.bottomNav.$el.clientHeight;
448        this.$emit('bottom-nav-height-change', newHeight);
449      }
450    },
451    computeMergedTimeline() {
452      const mergedTimeline = {
453        timeline: [], // Array of integers timestamps
454        selectedIndex: 0,
455      };
456
457      const timelineIndexes = [];
458      const timelines = [];
459      for (const file of this.timelineFiles) {
460        timelineIndexes.push(0);
461        timelines.push(file.timeline);
462      }
463
464      while (true) {
465        let minTime = Infinity;
466        let timelineToAdvance;
467
468        for (let i = 0; i < timelines.length; i++) {
469          const timeline = timelines[i];
470          const index = timelineIndexes[i];
471
472          if (index >= timeline.length) {
473            continue;
474          }
475
476          const time = timeline[index];
477
478          if (time < minTime) {
479            minTime = time;
480            timelineToAdvance = i;
481          }
482        }
483
484        if (timelineToAdvance === undefined) {
485          // No more elements left
486          break;
487        }
488
489        timelineIndexes[timelineToAdvance]++;
490        mergedTimeline.timeline.push(minTime);
491      }
492
493      // Object is frozen for performance reasons
494      // It will prevent Vue from making it a reactive object which will be very
495      // slow as the timeline gets larger.
496      Object.freeze(mergedTimeline.timeline);
497
498      return mergedTimeline;
499    },
500    toggle() {
501      this.minimized ? this.expand() : this.minimize();
502
503      this.minimized = !this.minimized;
504    },
505    expand() {
506      if (this.video) {
507        this.$refs.expandedContentVideoContainer
508            .appendChild(this.$refs.video.$el);
509      }
510    },
511    minimize() {
512      if (this.video) {
513        this.$refs.overlayVideoContainer.appendChild(this.$refs.video.$el);
514      }
515    },
516    fileIsVisible(f) {
517      return this.visibleDataViews.includes(f.filename);
518    },
519    resizeBottomNav(e) {
520      this.initResizeAction(e);
521    },
522    initResizeAction(e) {
523      document.onmousemove = this.startResize;
524      document.onmouseup = this.endResize;
525    },
526    startResize(e) {
527      if (this.dragState.clientY === null) {
528        this.dragState.clientY = e.clientY;
529      }
530
531      const movement = this.dragState.clientY - e.clientY;
532
533      const resizeOffset = this.resizeOffset + movement;
534      if (resizeOffset < 0) {
535        this.resizeOffset = 0;
536        this.dragState.clientY = null;
537      } else if (movement > this.getBottomNavDistanceToTop()) {
538        this.dragState.clientY += this.getBottomNavDistanceToTop();
539        this.resizeOffset += this.getBottomNavDistanceToTop();
540      } else {
541        this.resizeOffset = resizeOffset;
542        this.dragState.clientY = e.clientY;
543      }
544    },
545    endResize() {
546      this.dragState.lastDragEndPosition = this.dragState.clientY;
547      this.dragState.clientY = null;
548      document.onmouseup = null;
549      document.onmousemove = null;
550    },
551    getBottomNavDistanceToTop() {
552      return this.$refs.bottomNav.$el.getBoundingClientRect().top;
553    },
554    closeVideoOverlay() {
555      this.showVideoOverlay = false;
556    },
557    openVideoOverlay() {
558      this.showVideoOverlay = true;
559    },
560    toggleVideoOverlay() {
561      this.showVideoOverlay = !this.showVideoOverlay;
562    },
563    videoLoaded() {
564      this.$refs.videoOverlay.contentLoaded();
565    },
566    updateNavigationFileFilter() {
567      if (!this.minimized) {
568        // Always use custom mode navigation when timeline is expanded
569        this.$store.commit('setNavigationFilesFilter',
570            (f) => !f.timelineDisabled);
571        return;
572      }
573
574      let navigationStyleFilter;
575      switch (this.navigationStyle) {
576        case NAVIGATION_STYLE.GLOBAL:
577          navigationStyleFilter = (f) => true;
578          break;
579
580        case NAVIGATION_STYLE.FOCUSED:
581          navigationStyleFilter =
582            (f) => f.type === this.focusedFile.type;
583          break;
584
585        case NAVIGATION_STYLE.CUSTOM:
586          navigationStyleFilter = (f) => !f.timelineDisabled;
587          break;
588
589        default:
590          const split = this.navigationStyle.split('-');
591          if (split[0] !== NAVIGATION_STYLE.TARGETED) {
592            throw new Error('Unexpected nagivation type');
593          }
594
595          const fileType = split[1];
596          navigationStyleFilter =
597            (f) => f.type === fileType;
598      }
599
600      this.$store.commit('setNavigationFilesFilter', navigationStyleFilter);
601    },
602    updateVideoOverlayWidth(width) {
603      this.videoOverlayExtraWidth = width;
604    },
605    onTimelineCrop(cropDetails) {
606      this.crop = cropDetails;
607    },
608    onTimelineCropIntent(cropIntent) {
609      this.cropIntent = cropIntent;
610    },
611    changeVideoTimestamp(ts) {
612      if (!this.$refs.video) {
613        return;
614      }
615      this.$refs.video.selectFrameAtTime(ts);
616    },
617    resetVideoTimestamp() {
618      if (!this.$refs.video) {
619        return;
620      }
621      this.$refs.video.jumpToSelectedIndex();
622    },
623    clearSelection() {
624      this.crop = null;
625    },
626  },
627  components: {
628    'timeline': Timeline,
629    'timelines': Timelines,
630    'timeline-selection': TimelineSelection,
631    'videoview': VideoView,
632    'draggable-div': DraggableDiv,
633    'md-icon-option': MdIconOption,
634  },
635};
636</script>
637<style scoped>
638.overlay {
639  position: fixed;
640  top: 0;
641  left: 0;
642  bottom: 0;
643  right: 0;
644  width: 100vw;
645  height: 100vh;
646  z-index: 10;
647  margin: 0;
648  display: flex;
649  flex-direction: column;
650  pointer-events: none;
651}
652
653.overlay-content {
654  flex-grow: 1;
655}
656
657.bottom-nav {
658  background: white;
659  margin: 0;
660  max-height: 100vh;
661  bottom: 0;
662  left: 0;
663  pointer-events: all;
664}
665
666.nav-content {
667  width: 100%;
668}
669
670.toolbar, .active-timeline, .options {
671  display: flex;
672  flex-direction: row;
673  flex: 1;
674  align-items: center;
675}
676
677.toolbar.expanded {
678  align-items: baseline;
679}
680
681.minimized-timeline-content {
682 flex-grow: 1;
683}
684
685.minimized-timeline-content .seek-time {
686  padding: 3px 0;
687}
688
689.options, .expanded-content .seek-time {
690  padding: 0 20px 15px 20px;
691}
692
693.options label {
694  font-weight: 600;
695}
696
697.options .datafilter {
698  height: 50px;
699  display: flex;
700  align-items: center;
701}
702
703.expanded-content {
704  display: flex;
705}
706
707.flex-fill {
708  flex-grow: 1;
709}
710
711.video {
712  flex-grow: 0;
713}
714
715.resize-bar {
716  flex-grow: 1;
717}
718
719.drag-handle {
720  cursor: grab;
721}
722
723.md-icon-button {
724  margin: 0;
725}
726
727.toggle-btn {
728  margin-left: 8px;
729  align-self: flex-end;
730}
731
732.video-overlay {
733  display: inline-block;
734  margin-bottom: 15px;
735  min-width: 50px;
736  max-width: 50vw;
737  height: auto;
738  resize: horizontal;
739  pointer-events: all;
740}
741
742.close-video-overlay {
743  float: right;
744  cursor: pointer;
745}
746
747.show-video-overlay-btn {
748  margin-left: 12px;
749  margin-right: -8px;
750  align-self: flex-end;
751}
752
753.show-video-overlay-btn .md-icon {
754  color: #9E9E9E!important;
755}
756
757.collapsed-timeline-icon {
758  cursor: pointer;
759}
760
761.show-video-overlay-btn.active .md-icon {
762  color: #212121!important;
763}
764
765.help {
766  display: flex;
767  align-content: flex-end;
768  align-items: flex-end;
769  flex-direction: column;
770}
771
772.help-icon-wrapper {
773  margin-right: 20px;
774  margin-bottom: 10px;
775}
776
777.help-icon-wrapper .help-icon {
778  cursor: help;
779}
780
781.trace-icon {
782  cursor: pointer;
783  user-select: none;
784}
785
786.trace-icon.disabled {
787  color: gray;
788}
789
790.active-timeline {
791  flex: 0 0 auto;
792}
793
794.active-timeline .icon {
795  margin-right: 20px;
796}
797
798.active-timeline .active-timeline-icon {
799  margin-right: 10px;
800  align-self: flex-end;
801  margin-bottom: 18px;
802}
803
804.minimized-timeline-content {
805  align-self: flex-start;
806  padding-top: 1px;
807}
808
809.minimized-timeline-content label {
810  color: rgba(0,0,0,0.54);
811  font-size: 12px;
812  font-family: inherit;
813}
814
815.minimized-timeline-content .minimized-timeline {
816  margin-top: 4px;
817}
818
819.nagivation-style-selection-field {
820  width: 90px;
821  margin-right: 10px;
822  margin-bottom: 0;
823}
824
825.timeline-selection-header {
826  display: flex;
827  align-items: center;
828  padding-left: 15px;
829  height: 48px;
830}
831
832.help-icon {
833  font-size: 15px;
834  margin-bottom: 15px;
835  cursor: help;
836}
837</style>
838