blob: a75f4ec3b0e141a63fe635df159aa6a835303bc3 [file] [log] [blame]
<!-- Copyright (C) 2020 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<template>
<div class="overlay" v-if="hasTimeline || video">
<div class="overlay-content" ref="overlayContent">
<draggable-div
ref="videoOverlay"
class="video-overlay"
v-show="minimized && showVideoOverlay"
position="bottomLeft"
:asyncLoad="true"
:resizeable="true"
v-on:requestExtraWidth="updateVideoOverlayWidth"
:style="videoOverlayStyle"
v-if="video"
>
<template slot="header">
<div class="close-video-overlay" @click="closeVideoOverlay">
<md-icon>
close
<md-tooltip md-direction="right">Close video overlay</md-tooltip>
</md-icon>
</div>
</template>
<template slot="main">
<div ref="overlayVideoContainer">
<videoview
ref="video"
:file="video"
:height="videoHeight"
@loaded="videoLoaded" />
</div>
</template>
</draggable-div>
</div>
<md-bottom-bar
class="bottom-nav"
v-if="hasTimeline || (video && !showVideoOverlay)"
ref="bottomNav"
>
<div class="nav-content">
<div class="">
<searchbar
class="search-bar"
v-if="search"
:searchTypes="searchTypes"
:store="store"
:presentTags="Object.freeze(presentTags)"
:presentErrors="Object.freeze(presentErrors)"
:timeline="mergedTimeline.timeline"
/>
<md-toolbar
md-elevation="0"
class="md-transparent">
<md-button
@click="toggleSearch()"
class="drop-search"
>
Toggle search bar
</md-button>
<div class="toolbar" :class="{ expanded: expanded }">
<div class="resize-bar" v-show="expanded">
<div v-if="video" @mousedown="resizeBottomNav">
<md-icon class="drag-handle">
drag_handle
<md-tooltip md-direction="top">resize</md-tooltip>
</md-icon>
</div>
</div>
<div class="active-timeline" v-show="minimized">
<div
class="active-timeline-icon"
@click="$refs.navigationTypeSelection.$el
.querySelector('input').click()"
>
<md-icon class="collapsed-timeline-icon">
{{ collapsedTimelineIcon }}
<md-tooltip>
{{ collapsedTimelineIconTooltip }}
</md-tooltip>
</md-icon>
</div>
<md-field
v-if="multipleTraces"
ref="navigationTypeSelection"
class="navigation-style-selection-field"
>
<label>Navigation</label>
<md-select
v-model="navigationStyle"
name="navigationStyle"
md-dense
>
<md-icon-option
:value="NAVIGATION_STYLE.GLOBAL"
icon="public"
desc="Consider all timelines for navigation"
/>
<md-icon-option
:value="NAVIGATION_STYLE.FOCUSED"
:icon="TRACE_ICONS[focusedFile.type]"
:desc="`Automatically switch what timeline is considered
for navigation based on what is visible on screen.
Currently ${focusedFile.type}.`"
/>
<!-- TODO: Add edit button for custom settings that opens
popup dialog menu -->
<md-icon-option
:value="NAVIGATION_STYLE.CUSTOM"
icon="dashboard_customize"
desc="Considers only the enabled timelines for
navigation. Expand the bottom bar to toggle
timelines."
/>
<md-optgroup label="Targeted">
<md-icon-option
v-for="file in timelineFiles"
v-bind:key="file.type"
:value="`${NAVIGATION_STYLE.TARGETED}-` +
`${file.type}`"
:displayValue="file.type"
:shortValue="NAVIGATION_STYLE.TARGETED"
:icon="TRACE_ICONS[file.type]"
:desc="`Only consider ${file.type} ` +
'for timeline navigation.'"
/>
</md-optgroup>
</md-select>
</md-field>
</div>
<div
class="minimized-timeline-content"
v-show="minimized"
v-if="hasTimeline"
>
<input
class="timestamp-search-input"
v-model="searchInput"
spellcheck="false"
:placeholder="seekTime"
@focus="updateInputMode(true)"
@blur="updateInputMode(false)"
@keyup.enter="updateSearchForTimestamp"
/>
<timeline
:store="store"
:flickerMode="flickerMode"
:tags="Object.freeze(presentTags)"
:errors="Object.freeze(presentErrors)"
:timeline="Object.freeze(minimizedTimeline.timeline)"
:selected-index="minimizedTimeline.selectedIndex"
:scale="scale"
:crop="crop"
class="minimized-timeline"
/>
</div>
<md-button
class="md-icon-button show-video-overlay-btn"
:class="{active: minimized && showVideoOverlay}"
@click="toggleVideoOverlay"
v-show="minimized"
style="margin-bottom: 10px;"
>
<i class="md-icon md-icon-font">
featured_video
</i>
<md-tooltip md-direction="top">
<span v-if="showVideoOverlay">Hide video overlay</span>
<span v-else>Show video overlay</span>
</md-tooltip>
</md-button>
<md-button
class="md-icon-button toggle-btn"
@click="toggle"
style="margin-bottom: 10px;"
>
<md-icon v-if="minimized">
expand_less
<md-tooltip md-direction="top" @click="buttonClicked(`Expand Timeline`)">Expand timeline</md-tooltip>
</md-icon>
<md-icon v-else>
expand_more
<md-tooltip md-direction="top" @click="buttonClicked(`Collapse Timeline`)">Collapse timeline</md-tooltip>
</md-icon>
</md-button>
</div>
</md-toolbar>
<div class="expanded-content" v-show="expanded">
<div :v-if="video">
<div
class="expanded-content-video"
ref="expandedContentVideoContainer"
>
<!-- Video moved here on expansion -->
</div>
</div>
<div class="flex-fill">
<div
ref="expandedTimeline"
:style="`padding-top: ${resizeOffset}px;`"
>
<div class="seek-time" v-if="seekTime">
<b>Seek time: </b>
<input
class="timestamp-search-input"
:class="{ expanded: expanded }"
v-model="searchInput"
spellcheck="false"
:placeholder="seekTime"
@focus="updateInputMode(true)"
@blur="updateInputMode(false)"
@keyup.enter="updateSearchForTimestamp"
/>
</div>
<timelines
:timelineFiles="timelineFiles"
:scale="scale"
:crop="crop"
:cropIntent="cropIntent"
v-on:crop="onTimelineCrop"
/>
<div class="timeline-selection">
<div class="timeline-selection-header">
<label>Timeline Area Selection</label>
<span class="material-icons help-icon">
help_outline
<md-tooltip md-direction="right">
Select the area of the timeline to focus on.
Click and drag to select.
</md-tooltip>
</span>
<md-button
class="md-primary"
v-if="isCropped"
@click.native="clearSelection"
>
Clear selection
</md-button>
</div>
<timeline-selection
:timeline="mergedTimeline.timeline"
:start-timestamp="0"
:end-timestamp="0"
:scale="scale"
:cropArea="crop"
v-on:crop="onTimelineCrop"
v-on:cropIntent="onTimelineCropIntent"
v-on:showVideoAt="changeVideoTimestamp"
v-on:resetVideoTimestamp="resetVideoTimestamp"
/>
</div>
<div class="help" v-if="!minimized">
<div class="help-icon-wrapper">
<span class="material-icons help-icon">
help_outline
<md-tooltip md-direction="left">
Click on icons to disable timelines
</md-tooltip>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</md-bottom-bar>
</div>
</template>
<script>
import Timeline from './Timeline.vue';
import Timelines from './Timelines.vue';
import TimelineSelection from './TimelineSelection.vue';
import DraggableDiv from './DraggableDiv.vue';
import VideoView from './VideoView.vue';
import MdIconOption from './components/IconSelection/IconSelectOption.vue';
import Searchbar from './Searchbar.vue';
import FileType from './mixins/FileType.js';
import {NAVIGATION_STYLE} from './utils/consts';
import {TRACE_ICONS} from '@/decode.js';
// eslint-disable-next-line camelcase
import {nanos_to_string, getClosestTimestamp} from './transform.js';
export default {
name: 'overlay',
props: ['store', 'presentTags', 'presentErrors', 'searchTypes'],
mixins: [FileType],
data() {
return {
minimized: true,
// height of video in expanded timeline,
// made to match expandedTimeline dynamically
videoHeight: 'auto',
dragState: {
clientY: null,
lastDragEndPosition: null,
},
resizeOffset: 0,
showVideoOverlay: true,
mergedTimeline: null,
NAVIGATION_STYLE,
navigationStyle: this.store.navigationStyle,
videoOverlayExtraWidth: 0,
crop: null,
cropIntent: null,
TRACE_ICONS,
search: false,
searchInput: "",
isSeekTimeInputMode: false,
};
},
created() {
this.mergedTimeline = this.computeMergedTimeline();
this.$store.commit('setMergedTimeline', this.mergedTimeline);
this.updateNavigationFileFilter();
},
mounted() {
this.emitBottomHeightUpdate();
},
destroyed() {
this.$store.commit('removeMergedTimeline', this.mergedTimeline);
this.updateInputMode(false);
},
watch: {
navigationStyle(style) {
// Only store navigation type in local store if it's a type that will
// work regardless of what data is loaded.
if (style === NAVIGATION_STYLE.GLOBAL ||
style === NAVIGATION_STYLE.FOCUSED) {
this.store.navigationStyle = style;
}
this.updateNavigationFileFilter();
},
minimized() {
// Minimized toggled
this.updateNavigationFileFilter();
this.$nextTick(this.emitBottomHeightUpdate);
},
},
computed: {
video() {
return this.$store.getters.video;
},
videoOverlayStyle() {
return {
width: 150 + this.videoOverlayExtraWidth + 'px',
};
},
timelineFiles() {
return this.$store.getters.timelineFiles;
},
focusedFile() {
return this.$store.state.focusedFile;
},
expanded() {
return !this.minimized;
},
seekTime() {
return nanos_to_string(this.currentTimestamp);
},
scale() {
const mx = Math.max(...(this.timelineFiles.map((f) =>
Math.max(...f.timeline))));
const mi = Math.min(...(this.timelineFiles.map((f) =>
Math.min(...f.timeline))));
return [mi, mx];
},
currentTimestamp() {
return this.$store.state.currentTimestamp;
},
hasTimeline() {
// Returns true if a meaningful timeline exists (i.e. not only dumps)
for (const file of this.timelineFiles) {
if (file.timeline.length > 0 &&
(file.timeline[0] !== undefined || file.timeline.length > 1)) {
return true;
}
}
return false;
},
collapsedTimelineIconTooltip() {
switch (this.navigationStyle) {
case NAVIGATION_STYLE.GLOBAL:
return 'All timelines';
case NAVIGATION_STYLE.FOCUSED:
return `Focused: ${this.focusedFile.type}`;
case NAVIGATION_STYLE.CUSTOM:
return 'Enabled timelines';
default:
const split = this.navigationStyle.split('-');
if (split[0] !== NAVIGATION_STYLE.TARGETED) {
console.warn('Unexpected navigation type; fallback to global');
return 'All timelines';
}
const fileType = split[1];
return fileType;
}
},
collapsedTimelineIcon() {
switch (this.navigationStyle) {
case NAVIGATION_STYLE.GLOBAL:
return 'public';
case NAVIGATION_STYLE.FOCUSED:
return TRACE_ICONS[this.focusedFile.type];
case NAVIGATION_STYLE.CUSTOM:
return 'dashboard_customize';
default:
const split = this.navigationStyle.split('-');
if (split[0] !== NAVIGATION_STYLE.TARGETED) {
console.warn('Unexpected navigation type; fallback to global');
return 'public';
}
const fileType = split[1];
return TRACE_ICONS[fileType];
}
},
minimizedTimeline() {
if (this.navigationStyle === NAVIGATION_STYLE.GLOBAL) {
return this.mergedTimeline;
}
if (this.navigationStyle === NAVIGATION_STYLE.FOCUSED) {
//dumps do not have a timeline, so if scrolling over a dump, show merged timeline
if (this.focusedFile.timeline) {
return this.focusedFile;
}
return this.mergedTimeline;
}
if (this.navigationStyle === NAVIGATION_STYLE.CUSTOM) {
// TODO: Return custom timeline
return this.mergedTimeline;
}
if (
this.navigationStyle.split('-').length >= 2
&& this.navigationStyle.split('-')[0] === NAVIGATION_STYLE.TARGETED
) {
return this.$store.state
.traces[this.navigationStyle.split('-')[1]];
}
console.warn('Unexpected navigation type; fallback to global');
return this.mergedTimeline;
},
isCropped() {
return this.crop != null &&
(this.crop.left !== 0 || this.crop.right !== 1);
},
multipleTraces() {
return this.timelineFiles.length > 1;
},
flickerMode() {
return this.presentTags.length>0 || this.presentErrors.length>0;
},
},
updated() {
this.$nextTick(() => {
if (this.$refs.expandedTimeline && this.expanded) {
this.videoHeight = this.$refs.expandedTimeline.clientHeight;
} else {
this.videoHeight = 'auto';
}
});
},
methods: {
toggleSearch() {
this.search = !(this.search);
this.buttonClicked("Toggle Search Bar");
},
/**
* determines whether left/right arrow keys should move cursor in input field
* and upon click of input field, fills with current timestamp
*/
updateInputMode(isInputMode) {
this.isSeekTimeInputMode = isInputMode;
this.store.isInputMode = isInputMode;
if (!isInputMode) {
this.searchInput = "";
} else {
this.searchInput = this.seekTime;
}
},
/** Navigates to closest timestamp in timeline to search input*/
updateSearchForTimestamp() {
const closestTimestamp = getClosestTimestamp(this.searchInput, this.mergedTimeline.timeline);
this.$store.dispatch("updateTimelineTime", closestTimestamp);
this.updateInputMode(false);
this.newEventOccurred("Searching for timestamp")
},
emitBottomHeightUpdate() {
if (this.$refs.bottomNav) {
const newHeight = this.$refs.bottomNav.$el.clientHeight;
this.$emit('bottom-nav-height-change', newHeight);
}
},
computeMergedTimeline() {
const mergedTimeline = {
timeline: [], // Array of integers timestamps
selectedIndex: 0,
};
const timelineIndexes = [];
const timelines = [];
for (const file of this.timelineFiles) {
timelineIndexes.push(0);
timelines.push(file.timeline);
}
var timelineToAdvance = 0;
while (timelineToAdvance !== undefined) {
timelineToAdvance = undefined;
let minTime = Infinity;
for (let i = 0; i < timelines.length; i++) {
const timeline = timelines[i];
const index = timelineIndexes[i];
if (index >= timeline.length) {
continue;
}
const time = timeline[index];
if (time < minTime) {
minTime = time;
timelineToAdvance = i;
}
}
if (timelineToAdvance === undefined) {
// No more elements left
break;
}
timelineIndexes[timelineToAdvance]++;
mergedTimeline.timeline.push(minTime);
}
// Object is frozen for performance reasons
// It will prevent Vue from making it a reactive object which will be very
// slow as the timeline gets larger.
Object.freeze(mergedTimeline.timeline);
return mergedTimeline;
},
toggle() {
this.minimized ? this.expand() : this.minimize();
this.minimized = !this.minimized;
},
expand() {
if (this.video) {
this.$refs.expandedContentVideoContainer
.appendChild(this.$refs.video.$el);
}
},
minimize() {
if (this.video) {
this.$refs.overlayVideoContainer.appendChild(this.$refs.video.$el);
}
},
fileIsVisible(f) {
return this.visibleDataViews.includes(f.filename);
},
resizeBottomNav(e) {
this.initResizeAction(e);
},
initResizeAction(e) {
document.onmousemove = this.startResize;
document.onmouseup = this.endResize;
},
startResize(e) {
if (this.dragState.clientY === null) {
this.dragState.clientY = e.clientY;
}
const movement = this.dragState.clientY - e.clientY;
const resizeOffset = this.resizeOffset + movement;
if (resizeOffset < 0) {
this.resizeOffset = 0;
this.dragState.clientY = null;
} else if (movement > this.getBottomNavDistanceToTop()) {
this.dragState.clientY += this.getBottomNavDistanceToTop();
this.resizeOffset += this.getBottomNavDistanceToTop();
} else {
this.resizeOffset = resizeOffset;
this.dragState.clientY = e.clientY;
}
},
endResize() {
this.dragState.lastDragEndPosition = this.dragState.clientY;
this.dragState.clientY = null;
document.onmouseup = null;
document.onmousemove = null;
},
getBottomNavDistanceToTop() {
return this.$refs.bottomNav.$el.getBoundingClientRect().top;
},
closeVideoOverlay() {
this.showVideoOverlay = false;
this.buttonClicked("Close Video Overlay")
},
openVideoOverlay() {
this.showVideoOverlay = true;
this.buttonClicked("Open Video Overlay")
},
toggleVideoOverlay() {
this.showVideoOverlay = !this.showVideoOverlay;
this.buttonClicked("Toggle Video Overlay")
},
videoLoaded() {
this.$refs.videoOverlay.contentLoaded();
},
updateNavigationFileFilter() {
if (!this.minimized) {
// Always use custom mode navigation when timeline is expanded
this.$store.commit('setNavigationFilesFilter',
(f) => !f.timelineDisabled);
return;
}
let navigationStyleFilter;
switch (this.navigationStyle) {
case NAVIGATION_STYLE.GLOBAL:
navigationStyleFilter = (f) => true;
break;
case NAVIGATION_STYLE.FOCUSED:
navigationStyleFilter =
(f) => f.type === this.focusedFile.type;
break;
case NAVIGATION_STYLE.CUSTOM:
navigationStyleFilter = (f) => !f.timelineDisabled;
break;
default:
const split = this.navigationStyle.split('-');
if (split[0] !== NAVIGATION_STYLE.TARGETED) {
console.warn('Unexpected navigation type; fallback to global');
navigationStyleFilter = (f) => true;
break;
}
const fileType = split[1];
navigationStyleFilter =
(f) => f.type === fileType;
}
this.$store.commit('setNavigationFilesFilter', navigationStyleFilter);
},
updateVideoOverlayWidth(width) {
this.videoOverlayExtraWidth = width;
},
onTimelineCrop(cropDetails) {
this.crop = cropDetails;
},
onTimelineCropIntent(cropIntent) {
this.cropIntent = cropIntent;
},
changeVideoTimestamp(ts) {
if (!this.$refs.video) {
return;
}
this.$refs.video.selectFrameAtTime(ts);
},
resetVideoTimestamp() {
if (!this.$refs.video) {
return;
}
this.$refs.video.jumpToSelectedIndex();
},
clearSelection() {
this.crop = null;
},
},
components: {
'timeline': Timeline,
'timelines': Timelines,
'timeline-selection': TimelineSelection,
'videoview': VideoView,
'draggable-div': DraggableDiv,
'md-icon-option': MdIconOption,
'searchbar': Searchbar,
},
};
</script>
<style scoped>
.overlay {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
width: 100vw;
height: 100vh;
z-index: 10;
margin: 0;
display: flex;
flex-direction: column;
pointer-events: none;
}
.overlay-content {
flex-grow: 1;
}
.bottom-nav {
background: white;
margin: 0;
max-height: 100vh;
bottom: 0;
left: 0;
pointer-events: all;
}
.nav-content {
width: 100%;
}
.toolbar, .active-timeline, .options {
display: flex;
flex-direction: row;
flex: 1;
align-items: center;
}
.toolbar.expanded {
align-items: baseline;
}
.minimized-timeline-content {
flex-grow: 1;
}
.minimized-timeline-content .seek-time {
padding: 3px 0;
}
.options, .expanded-content .seek-time {
padding: 0 20px 15px 20px;
}
.options label {
font-weight: 600;
}
.options .datafilter {
height: 50px;
display: flex;
align-items: center;
}
.expanded-content {
display: flex;
}
.flex-fill {
flex-grow: 1;
}
.video {
flex-grow: 0;
}
.resize-bar {
flex-grow: 1;
}
.drag-handle {
cursor: grab;
}
.md-icon-button {
margin: 0;
}
.toggle-btn {
margin-left: 8px;
align-self: flex-end;
}
.video-overlay {
display: inline-block;
margin-bottom: 15px;
min-width: 50px;
max-width: 50vw;
height: auto;
resize: horizontal;
pointer-events: all;
}
.close-video-overlay {
float: right;
cursor: pointer;
}
.show-video-overlay-btn {
margin-left: 12px;
margin-right: -8px;
align-self: flex-end;
}
.show-video-overlay-btn .md-icon {
color: #9E9E9E!important;
}
.collapsed-timeline-icon {
cursor: pointer;
}
.show-video-overlay-btn.active .md-icon {
color: #212121!important;
}
.help {
display: flex;
align-content: flex-end;
align-items: flex-end;
flex-direction: column;
}
.help-icon-wrapper {
margin-right: 20px;
margin-bottom: 10px;
}
.help-icon-wrapper .help-icon {
cursor: help;
}
.trace-icon {
cursor: pointer;
user-select: none;
}
.trace-icon.disabled {
color: gray;
}
.active-timeline {
flex: 0 0 auto;
}
.active-timeline .icon {
margin-right: 20px;
}
.active-timeline .active-timeline-icon {
margin-right: 10px;
align-self: flex-end;
margin-bottom: 18px;
}
.minimized-timeline-content {
align-self: flex-start;
padding-top: 1px;
}
.minimized-timeline-content label {
color: rgba(0,0,0,0.54);
font-size: 12px;
font-family: inherit;
cursor: text;
}
.minimized-timeline-content .minimized-timeline {
margin-top: 4px;
}
.navigation-style-selection-field {
width: 90px;
margin-right: 10px;
margin-bottom: 0;
}
.timeline-selection-header {
display: flex;
align-items: center;
padding-left: 15px;
height: 48px;
}
.help-icon {
font-size: 15px;
margin-bottom: 15px;
cursor: help;
}
.timestamp-search-input {
outline: none;
border-width: 0 0 1px;
border-color: gray;
font-family: inherit;
color: #448aff;
font-size: 12px;
padding: 0;
letter-spacing: inherit;
width: 125px;
}
.timestamp-search-input:focus {
border-color: #448aff;
}
.timestamp-search-input.expanded {
font-size: 14px;
width: 150px;
}
.drop-search:hover {
background-color: #9af39f;
}
</style>