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 16<template> 17 <div class="timelines-container"> 18 19 <div class="timeline-icons" @mousedown="mousedownHandler"> 20 <div 21 v-for="file in timelineFiles" 22 :key="file.filename" 23 class="trace-icon" 24 :class="{disabled: file.timelineDisabled}" 25 @click="toggleTimeline(file)" 26 style="cursor: pointer;" 27 > 28 <i class="material-icons"> 29 {{ TRACE_ICONS[file.type] }} 30 <md-tooltip md-direction="bottom">{{ file.type }}</md-tooltip> 31 </i> 32 </div> 33 </div> 34 35 <div class="timelines-wrapper" ref="timelinesWrapper"> 36 <md-list class="timelines" @mousedown="mousedownHandler" ref="timelines"> 37 <md-list-item 38 v-for="file in timelineFiles" 39 :key="file.filename" 40 > 41 <timeline 42 :timeline="Object.freeze(file.timeline)" 43 :selected-index="file.selectedIndex" 44 :scale="scale" 45 :crop="crop" 46 :disabled="file.timelineDisabled" 47 class="timeline" 48 /> 49 </md-list-item> 50 </md-list> 51 52 <div 53 class="selection" 54 :style="selectionStyle" 55 /> 56 57 <div 58 v-show="this.cropIntent" 59 class="selection-intent" 60 :style="selectionIntentStyle" 61 /> 62 </div> 63 </div> 64</template> 65<script> 66import Timeline from './Timeline.vue'; 67import {TRACE_ICONS} from '@/decode.js'; 68 69export default { 70 name: 'Timelines', 71 props: ['timelineFiles', 'scale', 'crop', 'cropIntent'], 72 data() { 73 return { 74 // Distances of sides from top left corner of wrapping div in pixels 75 selectionPosition: { 76 top: 0, 77 left: 0, 78 bottom: 0, 79 right: 0, 80 }, 81 TRACE_ICONS, 82 }; 83 }, 84 computed: { 85 /** 86 * Used to check whether or not a selection box should be displayed. 87 * @return {bool} true if any of the positions are non nullish values 88 */ 89 isEmptySelection() { 90 return this.selectionPosition.top || 91 this.selectionPosition.left || 92 this.selectionPosition.bottom || 93 this.selectionPosition.right; 94 }, 95 /** 96 * Generates the style of the selection box. 97 * @return {object} an object containing the style of the selection box. 98 */ 99 selectionStyle() { 100 return { 101 top: `${this.selectionPosition.top}px`, 102 left: `${this.selectionPosition.left}px`, 103 height: 104 `${this.selectionPosition.bottom - this.selectionPosition.top}px`, 105 width: 106 `${this.selectionPosition.right - this.selectionPosition.left}px`, 107 }; 108 }, 109 /** 110 * Generates the dynamic style of the selection intent box. 111 * @return {object} an object containing the style of the selection intent 112 * box. 113 */ 114 selectionIntentStyle() { 115 if (!(this.cropIntent && this.$refs.timelinesWrapper)) { 116 return { 117 left: 0, 118 width: 0, 119 }; 120 } 121 122 const activeCropLeft = this.crop?.left ?? 0; 123 const activeCropRight = this.crop?.right ?? 1; 124 const timelineWidth = 125 this.$refs.timelinesWrapper.getBoundingClientRect().width; 126 127 const r = timelineWidth / (activeCropRight - activeCropLeft); 128 129 let left = 0; 130 let boderLeft = 'none'; 131 if (this.cropIntent.left > activeCropLeft) { 132 left = (this.cropIntent.left - activeCropLeft) * r; 133 boderLeft = null; 134 } 135 136 let right = timelineWidth; 137 let borderRight = 'none'; 138 if (this.cropIntent.right < activeCropRight) { 139 right = timelineWidth - (activeCropRight - this.cropIntent.right) * r; 140 borderRight = null; 141 } 142 143 return { 144 'left': `${left}px`, 145 'width': `${right - left}px`, 146 'border-left': boderLeft, 147 'border-right': borderRight, 148 }; 149 }, 150 }, 151 methods: { 152 /** 153 * Adds an overlay to make sure element selection can't happen and the 154 * crosshair cursor style is maintained wherever the curso is on the screen 155 * while a selection is taking place. 156 */ 157 addOverlay() { 158 if (this.overlay) { 159 return; 160 } 161 162 this.overlay = document.createElement('div'); 163 Object.assign(this.overlay.style, { 164 'position': 'fixed', 165 'top': 0, 166 'left': 0, 167 'height': '100vh', 168 'width': '100vw', 169 'z-index': 100, 170 'cursor': 'crosshair', 171 }); 172 173 document.body.appendChild(this.overlay); 174 }, 175 176 /** 177 * Removes the overlay that is added by a call to addOverlay. 178 */ 179 removeOverlay() { 180 if (!this.overlay) { 181 return; 182 } 183 184 document.body.removeChild(this.overlay); 185 delete this.overlay; 186 }, 187 188 /** 189 * Generates an object that can is used to update the position and style of 190 * the selection box when a selection is being made. The object contains 191 * three functions which all take a DOM event as a parameter. 192 * 193 * - init: setup the initial drag position of the selection base on the 194 * mousedown event 195 * - update: updates the selection box's coordinates based on the mousemouve 196 * event 197 * - reset: clears the selection box, shold be called when the mouseup event 198 * occurs or when we want to no longer display the selection box. 199 * @return {null} 200 */ 201 selectionPositionsUpdater() { 202 let startClientX; let startClientY; let x; let y; 203 204 return { 205 init: (e) => { 206 startClientX = e.clientX; 207 startClientY = e.clientY; 208 x = startClientX - 209 this.$refs.timelines.$el.getBoundingClientRect().left; 210 y = startClientY - 211 this.$refs.timelines.$el.getBoundingClientRect().top; 212 }, 213 update: (e) => { 214 let left; let right; let top; let bottom; 215 216 const xDiff = e.clientX - startClientX; 217 if (xDiff > 0) { 218 left = x; 219 right = x + xDiff; 220 } else { 221 left = x + xDiff; 222 right = x; 223 } 224 225 const yDiff = e.clientY - startClientY; 226 if (yDiff > 0) { 227 top = y; 228 bottom = y + yDiff; 229 } else { 230 top = y + yDiff; 231 bottom = y; 232 } 233 234 if (left < 0) { 235 left = 0; 236 } 237 if (top < 0) { 238 top = 0; 239 } 240 if (right > this.$refs.timelines.$el.getBoundingClientRect().width) { 241 right = this.$refs.timelines.$el.getBoundingClientRect().width; 242 } 243 244 if (bottom > 245 this.$refs.timelines.$el.getBoundingClientRect().height) { 246 bottom = this.$refs.timelines.$el.getBoundingClientRect().height; 247 } 248 249 this.$set(this.selectionPosition, 'left', left); 250 this.$set(this.selectionPosition, 'right', right); 251 this.$set(this.selectionPosition, 'top', top); 252 this.$set(this.selectionPosition, 'bottom', bottom); 253 }, 254 reset: (e) => { 255 this.$set(this.selectionPosition, 'left', 0); 256 this.$set(this.selectionPosition, 'right', 0); 257 this.$set(this.selectionPosition, 'top', 0); 258 this.$set(this.selectionPosition, 'bottom', 0); 259 }, 260 }; 261 }, 262 263 /** 264 * Handles the mousedown event indicating the start of a selection. 265 * Adds listeners to handles mousemove and mouseup event to detect the 266 * selection and update the selection box's coordinates. 267 * @param {event} e 268 */ 269 mousedownHandler(e) { 270 const selectionPositionsUpdater = this.selectionPositionsUpdater(); 271 selectionPositionsUpdater.init(e); 272 273 let dragged = false; 274 275 const mousemoveHandler = (e) => { 276 if (!dragged) { 277 dragged = true; 278 this.addOverlay(); 279 } 280 281 selectionPositionsUpdater.update(e); 282 }; 283 document.addEventListener('mousemove', mousemoveHandler); 284 285 const mouseupHandler = (e) => { 286 document.removeEventListener('mousemove', mousemoveHandler); 287 document.removeEventListener('mouseup', mouseupHandler); 288 289 if (dragged) { 290 this.removeOverlay(); 291 selectionPositionsUpdater.update(e); 292 this.zoomToSelection(); 293 } 294 selectionPositionsUpdater.reset(); 295 }; 296 document.addEventListener('mouseup', mouseupHandler); 297 }, 298 299 /** 300 * Update the crop values to zoom into the timeline based on the currently 301 * set selection box coordinates. 302 */ 303 zoomToSelection() { 304 const left = this.crop?.left ?? 0; 305 const right = this.crop?.right ?? 1; 306 307 const ratio = 308 (this.selectionPosition.right - this.selectionPosition.left) / 309 this.$refs.timelines.$el.getBoundingClientRect().width; 310 311 const newCropWidth = ratio * (right - left); 312 const newLeft = left + (this.selectionPosition.left / 313 this.$refs.timelines.$el.getBoundingClientRect().width) * 314 (right - left); 315 316 if (this.crop) { 317 this.$set(this.crop, 'left', newLeft); 318 this.$set(this.crop, 'right', newLeft + newCropWidth); 319 } else { 320 this.$emit('crop', { 321 left: newLeft, 322 right: newLeft + newCropWidth, 323 }); 324 } 325 }, 326 327 toggleTimeline(file) { 328 this.$set(file, 'timelineDisabled', !file.timelineDisabled); 329 }, 330 }, 331 components: { 332 Timeline, 333 }, 334}; 335</script> 336<style scoped> 337.timelines-container { 338 display: flex; 339} 340 341.timelines-container .timelines-wrapper { 342 flex-grow: 1; 343 cursor: crosshair; 344 position: relative; 345} 346 347.timelines-wrapper { 348 overflow: hidden; 349} 350 351.selection, .selection-intent { 352 position: absolute; 353 z-index: 100; 354 background: rgba(255, 36, 36, 0.5); 355 pointer-events: none; 356} 357 358.selection-intent { 359 top: 0; 360 height: 100%; 361 margin-left: -3px; 362 border-left: 3px #1261A0 solid; 363 border-right: 3px #1261A0 solid; 364} 365 366.timeline-icons { 367 display: flex; 368 flex-direction: column; 369 justify-content: space-evenly; 370 margin-left: 15px; 371} 372 373.trace-icon.disabled { 374 color: gray; 375} 376</style> 377