1<!DOCTYPE html> 2<!-- 3Copyright (c) 2013 The Chromium Authors. All rights reserved. 4Use of this source code is governed by a BSD-style license that can be 5found in the LICENSE file. 6--> 7 8<link rel="import" href="/tracing/base/range.html"> 9<link rel="import" href="/tracing/model/event_set.html"> 10<link rel="import" href="/tracing/model/slice.html"> 11<link rel="import" href="/tracing/ui/base/ui.html"> 12 13<script> 14'use strict'; 15 16/** 17 * @fileoverview Provides the TimingTool class. 18 */ 19tr.exportTo('tr.ui.b', function() { 20 21 /** 22 * Tool for taking time measurements in the TimelineTrackView using 23 * Viewportmarkers. 24 * @constructor 25 */ 26 function TimingTool(viewport, targetElement) { 27 this.viewport_ = viewport; 28 29 // Prepare the event handlers to be added and removed repeatedly. 30 this.onMouseMove_ = this.onMouseMove_.bind(this); 31 this.onDblClick_ = this.onDblClick_.bind(this); 32 this.targetElement_ = targetElement; 33 34 // Valid only during mousedown. 35 this.isMovingLeftEdge_ = false; 36 }; 37 38 TimingTool.prototype = { 39 40 onEnterTiming: function(e) { 41 this.targetElement_.addEventListener('mousemove', this.onMouseMove_); 42 this.targetElement_.addEventListener('dblclick', this.onDblClick_); 43 }, 44 45 onBeginTiming: function(e) { 46 if (!this.isTouchPointInsideTrackBounds_(e.clientX, e.clientY)) 47 return; 48 49 var pt = this.getSnappedToEventPosition_(e); 50 this.mouseDownAt_(pt.x, pt.y); 51 52 this.updateSnapIndicators_(pt); 53 }, 54 55 updateSnapIndicators_: function(pt) { 56 if (!pt.snapped) 57 return; 58 var ir = this.viewport_.interestRange; 59 if (ir.min === pt.x) 60 ir.leftSnapIndicator = new tr.ui.SnapIndicator(pt.y, pt.height); 61 if (ir.max === pt.x) 62 ir.rightSnapIndicator = new tr.ui.SnapIndicator(pt.y, pt.height); 63 }, 64 65 onUpdateTiming: function(e) { 66 var pt = this.getSnappedToEventPosition_(e); 67 this.mouseMoveAt_(pt.x, pt.y, true); 68 this.updateSnapIndicators_(pt); 69 }, 70 71 onEndTiming: function(e) { 72 this.mouseUp_(); 73 }, 74 75 onExitTiming: function(e) { 76 this.targetElement_.removeEventListener('mousemove', this.onMouseMove_); 77 this.targetElement_.removeEventListener('dblclick', this.onDblClick_); 78 }, 79 80 onMouseMove_: function(e) { 81 if (e.button) 82 return; 83 var worldX = this.getWorldXFromEvent_(e); 84 this.mouseMoveAt_(worldX, e.clientY, false); 85 }, 86 87 onDblClick_: function(e) { 88 // TODO(nduca): Implement dobuleclicking. 89 console.error('not implemented'); 90 }, 91 92 //////////////////////////////////////////////////////////////////////////// 93 94 isTouchPointInsideTrackBounds_: function(clientX, clientY) { 95 if (!this.viewport_ || 96 !this.viewport_.modelTrackContainer || 97 !this.viewport_.modelTrackContainer.canvas) 98 return false; 99 100 var canvas = this.viewport_.modelTrackContainer.canvas; 101 var canvasRect = canvas.getBoundingClientRect(); 102 if (clientX >= canvasRect.left && clientX <= canvasRect.right && 103 clientY >= canvasRect.top && clientY <= canvasRect.bottom) 104 return true; 105 106 return false; 107 }, 108 109 mouseDownAt_: function(worldX, y) { 110 var ir = this.viewport_.interestRange; 111 var dt = this.viewport_.currentDisplayTransform; 112 113 var pixelRatio = window.devicePixelRatio || 1; 114 var nearnessThresholdWorld = dt.xViewVectorToWorld(6 * pixelRatio); 115 116 if (ir.isEmpty) { 117 ir.setMinAndMax(worldX, worldX); 118 ir.rightSelected = true; 119 this.isMovingLeftEdge_ = false; 120 return; 121 } 122 123 124 // Left edge test. 125 if (Math.abs(worldX - ir.min) < nearnessThresholdWorld) { 126 ir.leftSelected = true; 127 ir.min = worldX; 128 this.isMovingLeftEdge_ = true; 129 return; 130 } 131 132 // Right edge test. 133 if (Math.abs(worldX - ir.max) < nearnessThresholdWorld) { 134 ir.rightSelected = true; 135 ir.max = worldX; 136 this.isMovingLeftEdge_ = false; 137 return; 138 } 139 140 ir.setMinAndMax(worldX, worldX); 141 ir.rightSelected = true; 142 this.isMovingLeftEdge_ = false; 143 }, 144 145 mouseMoveAt_: function(worldX, y, mouseDown) { 146 var ir = this.viewport_.interestRange; 147 148 if (mouseDown) { 149 this.updateMovingEdge_(worldX); 150 return; 151 } 152 153 var ir = this.viewport_.interestRange; 154 var dt = this.viewport_.currentDisplayTransform; 155 156 var pixelRatio = window.devicePixelRatio || 1; 157 var nearnessThresholdWorld = dt.xViewVectorToWorld(6 * pixelRatio); 158 159 // Left edge test. 160 if (Math.abs(worldX - ir.min) < nearnessThresholdWorld) { 161 ir.leftSelected = true; 162 ir.rightSelected = false; 163 return; 164 } 165 166 // Right edge test. 167 if (Math.abs(worldX - ir.max) < nearnessThresholdWorld) { 168 ir.leftSelected = false; 169 ir.rightSelected = true; 170 return; 171 } 172 173 ir.leftSelected = false; 174 ir.rightSelected = false; 175 return; 176 }, 177 178 updateMovingEdge_: function(newWorldX) { 179 var ir = this.viewport_.interestRange; 180 var a = ir.min; 181 var b = ir.max; 182 if (this.isMovingLeftEdge_) 183 a = newWorldX; 184 else 185 b = newWorldX; 186 187 if (a <= b) 188 ir.setMinAndMax(a, b); 189 else 190 ir.setMinAndMax(b, a); 191 192 if (ir.min == newWorldX) { 193 this.isMovingLeftEdge_ = true; 194 ir.leftSelected = true; 195 ir.rightSelected = false; 196 } else { 197 this.isMovingLeftEdge_ = false; 198 ir.leftSelected = false; 199 ir.rightSelected = true; 200 } 201 }, 202 203 mouseUp_: function() { 204 var dt = this.viewport_.currentDisplayTransform; 205 var ir = this.viewport_.interestRange; 206 207 ir.leftSelected = false; 208 ir.rightSelected = false; 209 210 var pixelRatio = window.devicePixelRatio || 1; 211 var minWidthValue = dt.xViewVectorToWorld(2 * pixelRatio); 212 if (ir.range < minWidthValue) 213 ir.reset(); 214 }, 215 216 getWorldXFromEvent_: function(e) { 217 var pixelRatio = window.devicePixelRatio || 1; 218 var canvas = this.viewport_.modelTrackContainer.canvas; 219 var worldOffset = canvas.getBoundingClientRect().left; 220 var viewX = (e.clientX - worldOffset) * pixelRatio; 221 return this.viewport_.currentDisplayTransform.xViewToWorld(viewX); 222 }, 223 224 225 /** 226 * Get the closest position of an event within a vertical range of the mouse 227 * position if possible, otherwise use the position of the mouse pointer. 228 * @param {MouseEvent} e Mouse event with the current mouse coordinates. 229 * @return { 230 * {Number} x, The x coordinate in world space. 231 * {Number} y, The y coordinate in world space. 232 * {Number} height, The height of the event. 233 * {boolean} snapped Whether the coordinates are from a snapped event or 234 * the mouse position. 235 * } 236 */ 237 getSnappedToEventPosition_: function(e) { 238 var pixelRatio = window.devicePixelRatio || 1; 239 var EVENT_SNAP_RANGE = 16 * pixelRatio; 240 241 var modelTrackContainer = this.viewport_.modelTrackContainer; 242 var modelTrackContainerRect = modelTrackContainer.getBoundingClientRect(); 243 244 var viewport = this.viewport_; 245 var dt = viewport.currentDisplayTransform; 246 var worldMaxDist = dt.xViewVectorToWorld(EVENT_SNAP_RANGE); 247 248 var worldX = this.getWorldXFromEvent_(e); 249 var mouseY = e.clientY; 250 251 var selection = new tr.model.EventSet(); 252 253 // Look at the track under mouse position first for better performance. 254 modelTrackContainer.addClosestEventToSelection( 255 worldX, worldMaxDist, mouseY, mouseY, selection); 256 257 // Look at all tracks visible on screen. 258 if (!selection.length) { 259 modelTrackContainer.addClosestEventToSelection( 260 worldX, worldMaxDist, 261 modelTrackContainerRect.top, modelTrackContainerRect.bottom, 262 selection); 263 } 264 265 var minDistX = worldMaxDist; 266 var minDistY = Infinity; 267 var pixWidth = dt.xViewVectorToWorld(1); 268 269 // Create result object with the mouse coordinates. 270 var result = { 271 x: worldX, 272 y: mouseY - modelTrackContainerRect.top, 273 height: 0, 274 snapped: false 275 }; 276 277 var eventBounds = new tr.b.Range(); 278 for (var i = 0; i < selection.length; i++) { 279 var event = selection[i]; 280 var track = viewport.trackForEvent(event); 281 var trackRect = track.getBoundingClientRect(); 282 283 eventBounds.reset(); 284 event.addBoundsToRange(eventBounds); 285 var eventX; 286 if (Math.abs(eventBounds.min - worldX) < 287 Math.abs(eventBounds.max - worldX)) { 288 eventX = eventBounds.min; 289 } else { 290 eventX = eventBounds.max; 291 } 292 293 var distX = eventX - worldX; 294 295 var eventY = trackRect.top; 296 var eventHeight = trackRect.height; 297 var distY = Math.abs(eventY + eventHeight / 2 - mouseY); 298 299 // Prefer events with a closer y position if their x difference is below 300 // the width of a pixel. 301 if ((distX <= minDistX || Math.abs(distX - minDistX) < pixWidth) && 302 distY < minDistY) { 303 minDistX = distX; 304 minDistY = distY; 305 306 // Retrieve the event position from the hit. 307 result.x = eventX; 308 result.y = eventY + 309 modelTrackContainer.scrollTop - modelTrackContainerRect.top; 310 result.height = eventHeight; 311 result.snapped = true; 312 } 313 } 314 315 return result; 316 } 317 }; 318 319 return { 320 TimingTool: TimingTool 321 }; 322}); 323</script> 324