blob: 8b9c225d5cbb88ddac5505c76ca251be3b0ad672 [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="timelines-container">
<div class="timeline-icons" @mousedown="mousedownHandler">
<div
v-for="file in timelineFiles"
:key="file.filename"
class="trace-icon"
:class="{disabled: file.timelineDisabled}"
@click="toggleTimeline(file)"
style="cursor: pointer;"
>
<i class="material-icons">
{{ TRACE_ICONS[file.type] }}
<md-tooltip md-direction="bottom">{{ file.type }}</md-tooltip>
</i>
</div>
</div>
<div class="timelines-wrapper" ref="timelinesWrapper">
<md-list class="timelines" @mousedown="mousedownHandler" ref="timelines">
<md-list-item
v-for="file in timelineFiles"
:key="file.filename"
>
<timeline
:timeline="Object.freeze(file.timeline)"
:selected-index="file.selectedIndex"
:scale="scale"
:crop="crop"
:disabled="file.timelineDisabled"
class="timeline"
/>
</md-list-item>
</md-list>
<div
class="selection"
:style="selectionStyle"
/>
<div
v-show="this.cropIntent"
class="selection-intent"
:style="selectionIntentStyle"
/>
</div>
</div>
</template>
<script>
import Timeline from './Timeline.vue';
import {TRACE_ICONS} from '@/decode.js';
export default {
name: 'Timelines',
props: ['timelineFiles', 'scale', 'crop', 'cropIntent'],
data() {
return {
// Distances of sides from top left corner of wrapping div in pixels
selectionPosition: {
top: 0,
left: 0,
bottom: 0,
right: 0,
},
TRACE_ICONS,
};
},
computed: {
/**
* Used to check whether or not a selection box should be displayed.
* @return {bool} true if any of the positions are non nullish values
*/
isEmptySelection() {
return this.selectionPosition.top ||
this.selectionPosition.left ||
this.selectionPosition.bottom ||
this.selectionPosition.right;
},
/**
* Generates the style of the selection box.
* @return {object} an object containing the style of the selection box.
*/
selectionStyle() {
return {
top: `${this.selectionPosition.top}px`,
left: `${this.selectionPosition.left}px`,
height:
`${this.selectionPosition.bottom - this.selectionPosition.top}px`,
width:
`${this.selectionPosition.right - this.selectionPosition.left}px`,
};
},
/**
* Generates the dynamic style of the selection intent box.
* @return {object} an object containing the style of the selection intent
* box.
*/
selectionIntentStyle() {
if (!(this.cropIntent && this.$refs.timelinesWrapper)) {
return {
left: 0,
width: 0,
};
}
const activeCropLeft = this.crop?.left ?? 0;
const activeCropRight = this.crop?.right ?? 1;
const timelineWidth =
this.$refs.timelinesWrapper.getBoundingClientRect().width;
const r = timelineWidth / (activeCropRight - activeCropLeft);
let left = 0;
let boderLeft = 'none';
if (this.cropIntent.left > activeCropLeft) {
left = (this.cropIntent.left - activeCropLeft) * r;
boderLeft = null;
}
let right = timelineWidth;
let borderRight = 'none';
if (this.cropIntent.right < activeCropRight) {
right = timelineWidth - (activeCropRight - this.cropIntent.right) * r;
borderRight = null;
}
return {
'left': `${left}px`,
'width': `${right - left}px`,
'border-left': boderLeft,
'border-right': borderRight,
};
},
},
methods: {
/**
* Adds an overlay to make sure element selection can't happen and the
* crosshair cursor style is maintained wherever the curso is on the screen
* while a selection is taking place.
*/
addOverlay() {
if (this.overlay) {
return;
}
this.overlay = document.createElement('div');
Object.assign(this.overlay.style, {
'position': 'fixed',
'top': 0,
'left': 0,
'height': '100vh',
'width': '100vw',
'z-index': 10,
'cursor': 'crosshair',
});
document.body.appendChild(this.overlay);
},
/**
* Removes the overlay that is added by a call to addOverlay.
*/
removeOverlay() {
if (!this.overlay) {
return;
}
document.body.removeChild(this.overlay);
delete this.overlay;
},
/**
* Generates an object that can is used to update the position and style of
* the selection box when a selection is being made. The object contains
* three functions which all take a DOM event as a parameter.
*
* - init: setup the initial drag position of the selection base on the
* mousedown event
* - update: updates the selection box's coordinates based on the mousemouve
* event
* - reset: clears the selection box, shold be called when the mouseup event
* occurs or when we want to no longer display the selection box.
* @return {null}
*/
selectionPositionsUpdater() {
let startClientX; let startClientY; let x; let y;
return {
init: (e) => {
startClientX = e.clientX;
startClientY = e.clientY;
x = startClientX -
this.$refs.timelines.$el.getBoundingClientRect().left;
y = startClientY -
this.$refs.timelines.$el.getBoundingClientRect().top;
},
update: (e) => {
let left; let right; let top; let bottom;
const xDiff = e.clientX - startClientX;
if (xDiff > 0) {
left = x;
right = x + xDiff;
} else {
left = x + xDiff;
right = x;
}
const yDiff = e.clientY - startClientY;
if (yDiff > 0) {
top = y;
bottom = y + yDiff;
} else {
top = y + yDiff;
bottom = y;
}
if (left < 0) {
left = 0;
}
if (top < 0) {
top = 0;
}
if (right > this.$refs.timelines.$el.getBoundingClientRect().width) {
right = this.$refs.timelines.$el.getBoundingClientRect().width;
}
if (bottom >
this.$refs.timelines.$el.getBoundingClientRect().height) {
bottom = this.$refs.timelines.$el.getBoundingClientRect().height;
}
this.$set(this.selectionPosition, 'left', left);
this.$set(this.selectionPosition, 'right', right);
this.$set(this.selectionPosition, 'top', top);
this.$set(this.selectionPosition, 'bottom', bottom);
},
reset: (e) => {
this.$set(this.selectionPosition, 'left', 0);
this.$set(this.selectionPosition, 'right', 0);
this.$set(this.selectionPosition, 'top', 0);
this.$set(this.selectionPosition, 'bottom', 0);
},
};
},
/**
* Handles the mousedown event indicating the start of a selection.
* Adds listeners to handles mousemove and mouseup event to detect the
* selection and update the selection box's coordinates.
* @param {event} e
*/
mousedownHandler(e) {
const selectionPositionsUpdater = this.selectionPositionsUpdater();
const startClientX = e.clientX;
const startClientY = e.clientY;
selectionPositionsUpdater.init(e);
let dragged = false;
const mousemoveHandler = (e) => {
if (!dragged) {
const xDiff = e.clientX - startClientX;
const yDiff = e.clientY - startClientY;
// Only start the cropping procedures and show the cropping overlay
// once we have dragged more than 5 pixels to mark a deliberate drag
// rather than an accidental subtle movement when clicking on an entry
if (xDiff > 10 || yDiff > 10) {
dragged = true;
this.addOverlay();
}
}
selectionPositionsUpdater.update(e);
};
document.addEventListener('mousemove', mousemoveHandler);
const mouseupHandler = (e) => {
document.removeEventListener('mousemove', mousemoveHandler);
document.removeEventListener('mouseup', mouseupHandler);
if (dragged) {
this.removeOverlay();
selectionPositionsUpdater.update(e);
this.zoomToSelection();
}
selectionPositionsUpdater.reset();
};
document.addEventListener('mouseup', mouseupHandler);
},
/**
* Update the crop values to zoom into the timeline based on the currently
* set selection box coordinates.
*/
zoomToSelection() {
const left = this.crop?.left ?? 0;
const right = this.crop?.right ?? 1;
const ratio =
(this.selectionPosition.right - this.selectionPosition.left) /
this.$refs.timelines.$el.getBoundingClientRect().width;
const newCropWidth = ratio * (right - left);
const newLeft = left + (this.selectionPosition.left /
this.$refs.timelines.$el.getBoundingClientRect().width) *
(right - left);
if (this.crop) {
this.$set(this.crop, 'left', newLeft);
this.$set(this.crop, 'right', newLeft + newCropWidth);
} else {
this.$emit('crop', {
left: newLeft,
right: newLeft + newCropWidth,
});
}
},
toggleTimeline(file) {
this.$set(file, 'timelineDisabled', !file.timelineDisabled);
},
},
components: {
Timeline,
},
};
</script>
<style scoped>
.timelines-container {
display: flex;
}
.timelines-container .timelines-wrapper {
flex-grow: 1;
cursor: crosshair;
position: relative;
}
.timelines-wrapper {
overflow: hidden;
}
.selection, .selection-intent {
position: absolute;
z-index: 10;
background: rgba(255, 36, 36, 0.5);
pointer-events: none;
}
.selection-intent {
top: 0;
height: 100%;
margin-left: -3px;
border-left: 3px #1261A0 solid;
border-right: 3px #1261A0 solid;
}
.timeline-icons {
display: flex;
flex-direction: column;
justify-content: space-evenly;
margin-left: 15px;
}
.trace-icon.disabled {
color: gray;
}
</style>