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