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