Add option to crop timeline
Test: Try the feature out in the UI
Change-Id: Ic814cc01bf1f14d899259901319db2bf4646b62e
diff --git a/tools/winscope/src/Overlay.vue b/tools/winscope/src/Overlay.vue
index c23cd86..5b9f805 100644
--- a/tools/winscope/src/Overlay.vue
+++ b/tools/winscope/src/Overlay.vue
@@ -121,6 +121,7 @@
:timeline="minimizedTimeline.timeline"
:selected-index="minimizedTimeline.selectedIndex"
:scale="scale"
+ :crop="crop"
class="minimized-timeline"
/>
</div>
@@ -184,11 +185,27 @@
:timeline="file.timeline"
:selected-index="file.selectedIndex"
:scale="scale"
+ :crop="crop"
:disabled="file.timelineDisabled"
class="timeline"
/>
</md-list-item>
</md-list>
+ <div class="timeline-selection">
+ <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>
+ <br />
+ <timeline-selection
+ :timeline="mergedTimeline.timeline"
+ :start-timestamp="0"
+ :end-timestamp="0"
+ :scale="scale"
+ v-on:crop="onTimelineCrop"
+ />
+ </div>
<div class="help" v-if="!minimized">
<div class="help-icon-wrapper">
@@ -207,18 +224,19 @@
</div>
</template>
<script>
-import Timeline from './Timeline.vue'
-import DraggableDiv from './DraggableDiv.vue'
-import VideoView from './VideoView.vue'
-import MdIconOption from './components/IconSelection/IconSelectOption.vue'
-import FileType from './mixins/FileType.js'
-import {NAVIGATION_STYLE} from './utils/consts'
+import Timeline from './Timeline.vue';
+import TimelineSelection from './TimelineSelection.vue';
+import DraggableDiv from './DraggableDiv.vue';
+import VideoView from './VideoView.vue';
+import MdIconOption from './components/IconSelection/IconSelectOption.vue';
+import FileType from './mixins/FileType.js';
+import {NAVIGATION_STYLE} from './utils/consts';
-import { nanos_to_string } from './transform.js'
+import {nanos_to_string} from './transform.js';
export default {
name: 'overlay',
- props: [ 'store' ],
+ props: ['store'],
mixins: [FileType],
data() {
return {
@@ -236,7 +254,8 @@
NAVIGATION_STYLE,
navigationStyle: this.store.navigationStyle,
videoOverlayExtraWidth: 0,
- }
+ crop: null,
+ };
},
created() {
this.mergedTimeline = this.computeMergedTimeline();
@@ -263,7 +282,7 @@
this.updateNavigationFileFilter();
this.$nextTick(this.emitBottomHeightUpdate);
- }
+ },
},
computed: {
video() {
@@ -272,7 +291,7 @@
videoOverlayStyle() {
return {
width: 150 + this.videoOverlayExtraWidth + 'px',
- }
+ };
},
timelineFiles() {
return this.$store.getters.timelineFiles;
@@ -287,8 +306,8 @@
return nanos_to_string(this.currentTimestamp);
},
scale() {
- var mx = Math.max(...(this.timelineFiles.map(f => Math.max(...f.timeline))));
- var mi = Math.min(...(this.timelineFiles.map(f => Math.min(...f.timeline))));
+ 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() {
@@ -309,18 +328,18 @@
collapsedTimelineIconTooltip() {
switch (this.navigationStyle) {
case NAVIGATION_STYLE.GLOBAL:
- return "All timelines";
+ return 'All timelines';
case NAVIGATION_STYLE.FOCUSED:
return `Focused: ${this.focusedFile.type.name}`;
case NAVIGATION_STYLE.CUSTOM:
- return "Enabled timelines";
+ return 'Enabled timelines';
default:
const split = this.navigationStyle.split('-');
if (split[0] !== NAVIGATION_STYLE.TARGETED) {
- throw new Error("Unexpected nagivation type");
+ throw new Error('Unexpected nagivation type');
}
const fileType = split[1];
@@ -330,18 +349,18 @@
collapsedTimelineIcon() {
switch (this.navigationStyle) {
case NAVIGATION_STYLE.GLOBAL:
- return "public";
+ return 'public';
case NAVIGATION_STYLE.FOCUSED:
return this.focusedFile.type.icon;
case NAVIGATION_STYLE.CUSTOM:
- return "dashboard_customize";
+ return 'dashboard_customize';
default:
const split = this.navigationStyle.split('-');
if (split[0] !== NAVIGATION_STYLE.TARGETED) {
- throw new Error("Unexpected nagivation type");
+ throw new Error('Unexpected nagivation type');
}
const fileType = split[1];
@@ -362,22 +381,22 @@
return this.mergedTimeline;
}
- if (this.navigationStyle.split("-")[0] === NAVIGATION_STYLE.TARGETED) {
+ if (this.navigationStyle.split('-')[0] === NAVIGATION_STYLE.TARGETED) {
return this.$store.state
- .filesByType[this.navigationStyle.split("-")[1]];
+ .filesByType[this.navigationStyle.split('-')[1]];
}
- throw new Error("Unexpected Nagivation Style");
+ throw new Error('Unexpected Nagivation Style');
},
},
- updated () {
- this.$nextTick(function () {
+ updated() {
+ this.$nextTick(function() {
if (this.$refs.expandedTimeline && this.expanded) {
this.videoHeight = this.$refs.expandedTimeline.clientHeight;
} else {
this.videoHeight = 'auto';
}
- })
+ });
},
methods: {
emitBottomHeightUpdate() {
@@ -399,7 +418,7 @@
timelines.push(file.timeline);
}
- while(true) {
+ while (true) {
let minTime = Infinity;
let timelineToAdvance;
@@ -496,37 +515,37 @@
this.$refs.videoOverlay.contentLoaded();
},
toggleTimeline(file) {
- this.$set(file, "timelineDisabled", !file.timelineDisabled);
+ this.$set(file, 'timelineDisabled', !file.timelineDisabled);
},
updateNavigationFileFilter() {
if (!this.minimized) {
// Always use custom mode navigation when timeline is expanded
- this.$store.commit('setNavigationFilesFilter', f => !f.timelineDisabled);
+ this.$store.commit('setNavigationFilesFilter', (f) => !f.timelineDisabled);
return;
}
let navigationStyleFilter;
switch (this.navigationStyle) {
case NAVIGATION_STYLE.GLOBAL:
- navigationStyleFilter = f => true;
+ navigationStyleFilter = (f) => true;
break;
case NAVIGATION_STYLE.FOCUSED:
- navigationStyleFilter = f => f.type.name === this.focusedFile.type.name;
+ navigationStyleFilter = (f) => f.type.name === this.focusedFile.type.name;
break;
case NAVIGATION_STYLE.CUSTOM:
- navigationStyleFilter = f => !f.timelineDisabled;
+ navigationStyleFilter = (f) => !f.timelineDisabled;
break;
default:
const split = this.navigationStyle.split('-');
if (split[0] !== NAVIGATION_STYLE.TARGETED) {
- throw new Error("Unexpected nagivation type");
+ throw new Error('Unexpected nagivation type');
}
const fileType = split[1];
- navigationStyleFilter = f => f.type.name === this.getDataTypeByName(fileType).name;
+ navigationStyleFilter = (f) => f.type.name === this.getDataTypeByName(fileType).name;
}
this.$store.commit('setNavigationFilesFilter', navigationStyleFilter);
@@ -534,14 +553,18 @@
updateVideoOverlayWidth(width) {
this.videoOverlayExtraWidth = width;
},
+ onTimelineCrop(cropDetails) {
+ this.crop = cropDetails;
+ },
},
components: {
'timeline': Timeline,
+ 'timeline-selection': TimelineSelection,
'videoview': VideoView,
'draggable-div': DraggableDiv,
'md-icon-option': MdIconOption,
},
-}
+};
</script>
<style scoped>
.overlay {
@@ -731,4 +754,4 @@
margin-bottom: 0;
}
-</style>
\ No newline at end of file
+</style>
diff --git a/tools/winscope/src/Timeline.vue b/tools/winscope/src/Timeline.vue
index 7160326..0313a34 100644
--- a/tools/winscope/src/Timeline.vue
+++ b/tools/winscope/src/Timeline.vue
@@ -26,7 +26,6 @@
class="point"
/>
<rect
- v-if="timeline.length"
:x="position(selected)"
y="0"
:width="pointWidth"
@@ -39,7 +38,8 @@
<script>
export default {
name: "timeline",
- props: ["timeline", "selectedIndex", "scale", "disabled"],
+ // TODO: Add indication of trim, at least for collasped timeline
+ props: ["timeline", "selectedIndex", "scale", "crop", "disabled"],
data() {
return {
pointWidth: "1%",
@@ -49,14 +49,21 @@
},
methods: {
position(item) {
- return this.translate(item);
+ let pos = this.translate(item);
+
+ if (this.crop) {
+ pos = (pos - this.crop.left) / (this.crop.right - this.crop.left);
+ }
+
+ return pos * 100 - (1 /*pointWidth*/) + "%";
},
translate(cx) {
- var scale = [...this.scale];
+ const scale = [...this.scale];
if (scale[0] >= scale[1]) {
return cx;
}
- return (((cx - scale[0]) / (scale[1] - scale[0])) * 100) + "%";
+
+ return (cx - scale[0]) / (scale[1] - scale[0]);
},
onItemClick(index) {
if (this.disabled) {
diff --git a/tools/winscope/src/TimelineSelection.vue b/tools/winscope/src/TimelineSelection.vue
new file mode 100644
index 0000000..8595ed9
--- /dev/null
+++ b/tools/winscope/src/TimelineSelection.vue
@@ -0,0 +1,359 @@
+<!-- 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="wrapper">
+ <svg
+ width="100%"
+ height="20"
+ class="timeline-svg"
+ :class="{disabled: disabled}"
+ ref="timelineSvg"
+ >
+ <rect
+ :x="position(item)"
+ y="0"
+ :width="pointWidth"
+ :height="pointHeight"
+ :rx="corner"
+ v-for="item in timeline"
+ :key="item"
+ class="point"
+ />
+ <rect
+ v-if="selectedWidth >= 0"
+ :x="selectionStartPosition"
+ y="0"
+ :width="selectedWidth"
+ :height="pointHeight"
+ :rx="corner"
+ class="point selection"
+ ref="selectedSection"
+ />
+ <rect
+ v-else
+ :x="selectionStartPosition + selectedWidth"
+ y="0"
+ :width="-selectedWidth"
+ :height="pointHeight"
+ :rx="corner"
+ class="point selection"
+ ref="selectedSection"
+ />
+
+ <rect
+ :x="selectionStartPosition - 2"
+ y="0"
+ :width="4"
+ :height="pointHeight"
+ :rx="corner"
+ class="point selection-edge"
+ ref="leftResizeDragger"
+ />
+
+ <rect
+ :x="selectionStartPosition + selectedWidth - 2"
+ y="0"
+ :width="4"
+ :height="pointHeight"
+ :rx="corner"
+ class="point selection-edge"
+ ref="rightResizeDragger"
+ />
+ </svg>
+ </div>
+</template>
+<script>
+export default {
+ name: "timelineSelection",
+ props: ["timeline", "startTimestamp", "endTimestamp", "scale", "disabled"],
+ data() {
+ return {
+ pointWidth: "1%",
+ pointHeight: 15,
+ corner: 2,
+ selectionStartPosition: 0,
+ selectionEndPosition: 0,
+ };
+ },
+ watch: {
+ selectionStartPosition() {
+ this.emitCropDetails();
+ },
+ selectionEndPosition() {
+ this.emitCropDetails();
+ }
+ },
+ methods: {
+ position(item) {
+ return this.translate(item);
+ },
+ translate(cx) {
+ var scale = [...this.scale];
+ if (scale[0] >= scale[1]) {
+ return cx;
+ }
+
+ return (((cx - scale[0]) / (scale[1] - scale[0])) * 100 - (1 /*pointWidth*/)) + "%";
+ },
+ emitCropDetails() {
+ const width = this.$refs.timelineSvg.clientWidth;
+ this.$emit('crop', {
+ left: this.selectionStartPosition / width,
+ right: this.selectionEndPosition / width
+ });
+ },
+
+ setupCreateSelectionListeners() {
+ this.timelineSvgMouseDownEventListener = e => {
+ e.stopPropagation();
+ this.selecting = true;
+ this.dragged = false;
+ this.mouseDownX = e.offsetX;
+ this.mouseDownClientX = e.clientX;
+
+ document.body.style.cursor = "crosshair";
+ };
+
+ this.createSelectionMouseMoveEventListener = e => {
+ if (this.selecting) {
+ if (!this.dragged) {
+ this.selectionStartX = this.mouseDownX;
+ }
+
+ this.dragged = true;
+ const draggedAmount = e.clientX - this.mouseDownClientX;
+
+ if (draggedAmount >= 0) {
+ this.selectionStartPosition = this.selectionStartX;
+
+ const endX = this.selectionStartX + draggedAmount;
+ if (endX <= this.$refs.timelineSvg.clientWidth) {
+ this.selectionEndPosition = endX;
+ } else {
+ this.selectionEndPosition = this.$refs.timelineSvg.clientWidth;
+ }
+ } else {
+ this.selectionEndPosition = this.selectionStartX;
+
+ const startX = this.selectionStartX + draggedAmount;
+ if (startX >= 0) {
+ this.selectionStartPosition = startX;
+ } else {
+ this.selectionStartPosition = 0;
+ }
+ }
+ }
+ }
+
+ this.createSelectionMouseUpEventListener = e => {
+ this.selecting = false;
+ document.body.style.cursor = null;
+ };
+
+ this.$refs.timelineSvg
+ .addEventListener('mousedown', this.timelineSvgMouseDownEventListener);
+ document
+ .addEventListener('mousemove', this.createSelectionMouseMoveEventListener);
+ document
+ .addEventListener('mouseup', this.createSelectionMouseUpEventListener);
+ },
+
+ teardownCreateSelectionListeners() {
+ this.$refs.timelineSvg
+ .removeEventListener('mousedown', this.timelineSvgMouseDownEventListener);
+ document
+ .removeEventListener('mousemove', this.createSelectionMouseMoveEventListener);
+ document
+ .removeEventListener('mouseup', this.createSelectionMouseUpEventListener);
+ },
+
+ setupDragSelectionListeners() {
+ this.selectedSectionMouseDownListener = e => {
+ e.stopPropagation();
+ this.draggingSelection = true;
+ this.draggingSelectionStartX = e.clientX;
+ this.draggingSelectionStartPos = this.selectionStartPosition;
+ this.draggingSelectionEndPos = this.selectionEndPosition;
+
+ document.body.style.cursor = "move";
+ };
+
+ this.dragSelectionMouseMoveEventListener = e => {
+ if (this.draggingSelection) {
+ const dragAmount = e.clientX - this.draggingSelectionStartX;
+
+ const newStartPos = this.draggingSelectionStartPos + dragAmount;
+ const newEndPos = this.draggingSelectionEndPos + dragAmount;
+ if (newStartPos >= 0 && newEndPos <= this.$refs.timelineSvg.clientWidth) {
+ this.selectionStartPosition = newStartPos;
+ this.selectionEndPosition = newEndPos;
+ } else {
+ if (newStartPos < 0) {
+ this.selectionStartPosition = 0;
+ this.selectionEndPosition = newEndPos - (newStartPos /*negative overflown amount*/);
+ } else {
+ const overflownAmount = newEndPos - this.$refs.timelineSvg.clientWidth;
+ this.selectionEndPosition = this.$refs.timelineSvg.clientWidth;
+ this.selectionStartPosition = newStartPos - overflownAmount;
+ }
+ }
+ }
+ }
+
+ this.dragSelectionMouseUpEventListener = e => {
+ this.draggingSelection = false;
+ document.body.style.cursor = null;
+ };
+
+ this.$refs.selectedSection
+ .addEventListener('mousedown', this.selectedSectionMouseDownListener);
+ document
+ .addEventListener('mousemove', this.dragSelectionMouseMoveEventListener);
+ document
+ .addEventListener('mouseup', this.dragSelectionMouseUpEventListener);
+ },
+
+ teardownDragSelectionListeners() {
+ this.$refs.selectedSection
+ .removeEventListener('mousedown', this.selectedSectionMouseDownListener);
+ document
+ .removeEventListener('mousemove', this.dragSelectionMouseMoveEventListener);
+ document
+ .removeEventListener('mouseup', this.dragSelectionMouseUpEventListener);
+ },
+
+ setupResizeSelectionListeners() {
+ this.leftResizeDraggerMouseDownEventListener = e => {
+ e.stopPropagation();
+ this.resizeingLeft = true;
+ this.resizeStartX = e.clientX;
+ this.resizeStartPos = this.selectionStartPosition;
+
+ document.body.style.cursor = "ew-resize";
+ };
+
+ this.rightResizeDraggerMouseDownEventListener = e => {
+ e.stopPropagation();
+ this.resizeingRight = true;
+ this.resizeStartX = e.clientX;
+ this.resizeEndPos = this.selectionEndPosition;
+
+ document.body.style.cursor = "ew-resize";
+ };
+
+ this.resizeMouseMoveEventListener = e => {
+ if (this.resizeingLeft) {
+ const moveAmount = e.clientX - this.resizeStartX;
+ let newStartPos = this.resizeStartPos + moveAmount;
+ if (newStartPos >= this.selectionEndPosition) {
+ newStartPos = this.selectionEndPosition;
+ }
+ if (newStartPos < 0) {
+ newStartPos = 0;
+ }
+
+ this.selectionStartPosition = newStartPos;
+ }
+
+ if (this.resizeingRight) {
+ const moveAmount = e.clientX - this.resizeStartX;
+ let newEndPos = this.resizeEndPos + moveAmount;
+ if (newEndPos <= this.selectionStartPosition) {
+ newEndPos = this.selectionStartPosition;
+ }
+ if (newEndPos > this.$refs.timelineSvg.clientWidth) {
+ newEndPos = this.$refs.timelineSvg.clientWidth;
+ }
+
+ this.selectionEndPosition = newEndPos;
+ }
+ };
+
+ this.resizeSelectionMouseUpEventListener = e => {
+ this.resizeingLeft = false;
+ this.resizeingRight = false;
+ document.body.style.cursor = null;
+ }
+
+ document.body.style.cursor = null;
+
+ this.$refs.leftResizeDragger
+ .addEventListener('mousedown', this.leftResizeDraggerMouseDownEventListener);
+ this.$refs.rightResizeDragger
+ .addEventListener('mousedown', this.rightResizeDraggerMouseDownEventListener);
+ document
+ .addEventListener('mousemove', this.resizeMouseMoveEventListener);
+ document
+ .addEventListener('mouseup', this.resizeSelectionMouseUpEventListener);
+ },
+
+ teardownResizeSelectionListeners() {
+ this.$refs.leftResizeDragger
+ .removeEventListener('mousedown', this.leftResizeDraggerMouseDownEventListener);
+ this.$refs.rightResizeDragger
+ .removeEventListener('mousedown', this.rightResizeDraggerMouseDownEventListener);
+ document
+ .removeEventListener('mousemove', this.resizeMouseMoveEventListener);
+ document
+ .removeEventListener('mouseup', this.resizeSelectionMouseUpEventListener);
+ },
+ },
+ computed: {
+ timestamps() {
+ if (this.timeline.length == 1) {
+ return [0];
+ }
+ return this.timeline;
+ },
+ selected() {
+ return this.timeline[this.selectedIndex];
+ },
+ selectedWidth() {
+ return this.selectionEndPosition - this.selectionStartPosition;
+ }
+ },
+ mounted() {
+ this.setupCreateSelectionListeners();
+ this.setupDragSelectionListeners();
+ this.setupResizeSelectionListeners();
+ },
+ beforeDestroy() {
+ this.teardownCreateSelectionListeners();
+ this.teardownDragSelectionListeners();
+ this.teardownResizeSelectionListeners();
+ },
+};
+</script>
+<style scoped>
+.wrapper {
+ padding: 0 15px;
+}
+
+.timeline-svg {
+ cursor: crosshair;
+}
+.timeline-svg .point {
+ fill: #BDBDBD;
+}
+.timeline-svg .point.selection {
+ fill: rgba(240, 59, 59, 0.596);
+ cursor: move;
+}
+
+.timeline-svg .point.selection-edge {
+ fill: rgba(27, 123, 212, 0.596);
+ cursor: ew-resize;
+}
+</style>