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>