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