1/*
2 * Copyright (C) 2013 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 *    notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 *    notice, this list of conditions and the following disclaimer in the
11 *    documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26WebInspector.TimelineRuler = function()
27{
28    WebInspector.Object.call(this);
29
30    this._element = document.createElement("div");
31    this._element.className = WebInspector.TimelineRuler.StyleClassName;
32
33    this._headerElement = document.createElement("div");
34    this._headerElement.className = WebInspector.TimelineRuler.HeaderElementStyleClassName;
35    this._element.appendChild(this._headerElement);
36
37    this._markersElement = document.createElement("div");
38    this._markersElement.className = WebInspector.TimelineRuler.MarkersElementStyleClassName;
39    this._element.appendChild(this._markersElement);
40
41    this._zeroTime = 0;
42    this._startTime = 0;
43    this._endTime = 0;
44    this._duration = NaN;
45    this._secondsPerPixel = 0;
46    this._selectionStartTime = 0;
47    this._selectionEndTime = Infinity;
48    this._endTimePinned = false;
49    this._allowsClippedLabels = false;
50    this._allowsTimeRangeSelection = false;
51
52    this._markerElementMap = new Map;
53}
54
55WebInspector.TimelineRuler.MinimumLeftDividerSpacing = 48;
56WebInspector.TimelineRuler.MinimumDividerSpacing = 64;
57
58WebInspector.TimelineRuler.StyleClassName = "timeline-ruler";
59WebInspector.TimelineRuler.AllowsTimeRangeSelectionStyleClassName = "allows-time-range-selection";
60WebInspector.TimelineRuler.HeaderElementStyleClassName = "header";
61WebInspector.TimelineRuler.DividerElementStyleClassName = "divider";
62WebInspector.TimelineRuler.DividerLabelElementStyleClassName = "label";
63
64WebInspector.TimelineRuler.MarkersElementStyleClassName = "markers";
65WebInspector.TimelineRuler.BaseMarkerElementStyleClassName = "marker";
66WebInspector.TimelineRuler.ShadedAreaElementStyleClassName = "shaded-area";
67WebInspector.TimelineRuler.SelectionDragElementStyleClassName = "selection-drag";
68WebInspector.TimelineRuler.SelectionHandleElementStyleClassName = "selection-handle";
69WebInspector.TimelineRuler.LeftSelectionElementStyleClassName = "left";
70WebInspector.TimelineRuler.RightSelectionElementStyleClassName = "right";
71WebInspector.TimelineRuler.MinimumSelectionTimeRange = 0.01;
72
73WebInspector.TimelineRuler.Event = {
74    TimeRangeSelectionChanged: "time-ruler-time-range-selection-changed"
75};
76
77WebInspector.TimelineRuler.prototype = {
78    constructor: WebInspector.TimelineRuler,
79
80    // Public
81
82    get element()
83    {
84        return this._element;
85    },
86
87    get allowsClippedLabels()
88    {
89        return this._allowsClippedLabels
90    },
91
92    set allowsClippedLabels(x)
93    {
94        if (this._allowsClippedLabels === x)
95            return;
96
97        this._allowsClippedLabels = x || false;
98
99        this._needsLayout();
100    },
101
102    get allowsTimeRangeSelection()
103    {
104        return this._allowsTimeRangeSelection;
105    },
106
107    set allowsTimeRangeSelection(x)
108    {
109        if (this._allowsTimeRangeSelection === x)
110            return;
111
112        this._allowsTimeRangeSelection = x || false;
113
114        if (x) {
115            this._mouseDownEventListener = this._handleMouseDown.bind(this);
116            this._element.addEventListener("mousedown", this._mouseDownEventListener);
117
118            this._leftShadedAreaElement = document.createElement("div");
119            this._leftShadedAreaElement.classList.add(WebInspector.TimelineRuler.ShadedAreaElementStyleClassName);
120            this._leftShadedAreaElement.classList.add(WebInspector.TimelineRuler.LeftSelectionElementStyleClassName);
121
122            this._rightShadedAreaElement = document.createElement("div");
123            this._rightShadedAreaElement.classList.add(WebInspector.TimelineRuler.ShadedAreaElementStyleClassName);
124            this._rightShadedAreaElement.classList.add(WebInspector.TimelineRuler.RightSelectionElementStyleClassName);
125
126            this._leftSelectionHandleElement = document.createElement("div");
127            this._leftSelectionHandleElement.classList.add(WebInspector.TimelineRuler.SelectionHandleElementStyleClassName);
128            this._leftSelectionHandleElement.classList.add(WebInspector.TimelineRuler.LeftSelectionElementStyleClassName);
129            this._leftSelectionHandleElement.addEventListener("mousedown", this._handleSelectionHandleMouseDown.bind(this));
130
131            this._rightSelectionHandleElement = document.createElement("div");
132            this._rightSelectionHandleElement.classList.add(WebInspector.TimelineRuler.SelectionHandleElementStyleClassName);
133            this._rightSelectionHandleElement.classList.add(WebInspector.TimelineRuler.RightSelectionElementStyleClassName);
134            this._rightSelectionHandleElement.addEventListener("mousedown", this._handleSelectionHandleMouseDown.bind(this));
135
136            this._selectionDragElement = document.createElement("div");
137            this._selectionDragElement.classList.add(WebInspector.TimelineRuler.SelectionDragElementStyleClassName);
138
139            this._needsSelectionLayout();
140        } else {
141            this._element.removeEventListener("mousedown", this._mouseDownEventListener);
142            delete this._mouseDownEventListener;
143
144            this._leftShadedAreaElement.remove();
145            this._rightShadedAreaElement.remove();
146            this._leftSelectionHandleElement.remove();
147            this._rightSelectionHandleElement.remove();
148            this._selectionDragElement.remove();
149
150            delete this._leftShadedAreaElement;
151            delete this._rightShadedAreaElement;
152            delete this._leftSelectionHandleElement;
153            delete this._rightSelectionHandleElement;
154            delete this._selectionDragElement;
155        }
156    },
157
158    get zeroTime()
159    {
160        return this._zeroTime;
161    },
162
163    set zeroTime(x)
164    {
165        if (this._zeroTime === x)
166            return;
167
168        this._zeroTime = x || 0;
169
170        this._needsLayout();
171    },
172
173    get startTime()
174    {
175        return this._startTime;
176    },
177
178    set startTime(x)
179    {
180        if (this._startTime === x)
181            return;
182
183        this._startTime = x || 0;
184
185        if (!isNaN(this._duration))
186            this._endTime = this._startTime + this._duration;
187
188        this._needsLayout();
189    },
190
191    get duration()
192    {
193        if (!isNaN(this._duration))
194            return this._duration;
195        return this.endTime - this.startTime;
196    },
197
198    set duration(x)
199    {
200        if (this._duration === x)
201            return;
202
203        this._duration = x || NaN;
204
205        if (!isNaN(this._duration)) {
206            this._endTime = this._startTime + this._duration;
207            this._endTimePinned = true;
208        } else
209            this._endTimePinned = false;
210
211        this._needsLayout();
212    },
213
214    get endTime()
215    {
216        if (!this._endTimePinned && this._scheduledLayoutUpdateIdentifier)
217            this._recalculate();
218        return this._endTime;
219    },
220
221    set endTime(x)
222    {
223        if (this._endTime === x)
224            return;
225
226        this._endTime = x || 0;
227        this._endTimePinned = true;
228
229        this._needsLayout();
230    },
231
232    get secondsPerPixel()
233    {
234        if (this._scheduledLayoutUpdateIdentifier)
235            this._recalculate();
236        return this._secondsPerPixel;
237    },
238
239    set secondsPerPixel(x)
240    {
241        if (this._secondsPerPixel === x)
242            return;
243
244        this._secondsPerPixel = x || 0;
245        this._endTimePinned = false;
246        this._currentSliceTime = 0;
247
248        this._needsLayout();
249    },
250
251    get selectionStartTime()
252    {
253        return this._selectionStartTime;
254    },
255
256    set selectionStartTime(x)
257    {
258        if (this._selectionStartTime === x)
259            return;
260
261        this._selectionStartTime = x || 0;
262        this._timeRangeSelectionChanged = true;
263
264        this._needsSelectionLayout();
265    },
266
267    get selectionEndTime()
268    {
269        return this._selectionEndTime;
270    },
271
272    set selectionEndTime(x)
273    {
274        if (this._selectionEndTime === x)
275            return;
276
277        this._selectionEndTime = x || 0;
278        this._timeRangeSelectionChanged = true;
279
280        this._needsSelectionLayout();
281    },
282
283    addMarker: function(marker)
284    {
285        console.assert(marker instanceof WebInspector.TimelineMarker);
286
287        if (this._markerElementMap.has(marker))
288            return;
289
290        marker.addEventListener(WebInspector.TimelineMarker.Event.TimeChanged, this._timelineMarkerTimeChanged, this);
291
292        var markerElement = document.createElement("div");
293        markerElement.classList.add(WebInspector.TimelineRuler.BaseMarkerElementStyleClassName);
294        markerElement.classList.add(marker.type);
295
296        this._markerElementMap.set(marker, markerElement);
297
298        this._needsMarkerLayout();
299    },
300
301    elementForMarker: function(marker)
302    {
303        return this._markerElementMap.get(marker) || null;
304    },
305
306    updateLayout: function()
307    {
308        if (this._scheduledLayoutUpdateIdentifier) {
309            cancelAnimationFrame(this._scheduledLayoutUpdateIdentifier);
310            delete this._scheduledLayoutUpdateIdentifier;
311        }
312
313        var visibleWidth = this._recalculate();
314        if (visibleWidth <= 0)
315            return;
316
317        var duration = this.duration;
318
319        var pixelsPerSecond = visibleWidth / duration;
320
321        // Calculate a divider count based on the maximum allowed divider density.
322        var dividerCount = Math.round(visibleWidth / WebInspector.TimelineRuler.MinimumDividerSpacing);
323
324        if (this._endTimePinned || !this._currentSliceTime) {
325            // Calculate the slice time based on the rough divider count and the time span.
326            var sliceTime = duration / dividerCount;
327
328            // Snap the slice time to a nearest number (e.g. 0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50, etc.)
329            sliceTime = Math.pow(10, Math.ceil(Math.log(sliceTime) / Math.LN10));
330            if (sliceTime * pixelsPerSecond >= 5 * WebInspector.TimelineRuler.MinimumDividerSpacing)
331                sliceTime = sliceTime / 5;
332            if (sliceTime * pixelsPerSecond >= 2 * WebInspector.TimelineRuler.MinimumDividerSpacing)
333                sliceTime = sliceTime / 2;
334
335            this._currentSliceTime = sliceTime;
336        } else {
337            // Reuse the last slice time since the time duration does not scale to fit when the end time isn't pinned.
338            var sliceTime = this._currentSliceTime;
339        }
340
341        var firstDividerTime = (Math.ceil((this._startTime - this._zeroTime) / sliceTime) * sliceTime) + this._zeroTime;
342        var lastDividerTime = this._endTime;
343
344        // Calculate the divider count now based on the final slice time.
345        dividerCount = Math.ceil((lastDividerTime - firstDividerTime) / sliceTime);
346
347        // Make an extra divider in case the last one is partially visible.
348        if (!this._endTimePinned)
349            ++dividerCount;
350
351        var markerDividers = this._markersElement.querySelectorAll("." + WebInspector.TimelineRuler.DividerElementStyleClassName);
352
353        var dividerElement = this._headerElement.firstChild;
354
355        for (var i = 0; i <= dividerCount; ++i) {
356            if (!dividerElement) {
357                dividerElement = document.createElement("div");
358                dividerElement.className = WebInspector.TimelineRuler.DividerElementStyleClassName;
359                this._headerElement.appendChild(dividerElement);
360
361                var labelElement = document.createElement("div");
362                labelElement.className = WebInspector.TimelineRuler.DividerLabelElementStyleClassName;
363                dividerElement._labelElement = labelElement;
364                dividerElement.appendChild(labelElement);
365            }
366
367            var markerDividerElement = markerDividers[i];
368            if (!markerDividerElement) {
369                markerDividerElement = document.createElement("div");
370                markerDividerElement.className = WebInspector.TimelineRuler.DividerElementStyleClassName;
371                this._markersElement.appendChild(markerDividerElement);
372            }
373
374            var dividerTime = firstDividerTime + (sliceTime * i);
375
376            var newLeftPosition = (dividerTime - this._startTime) / duration;
377
378            if (!this._allowsClippedLabels) {
379                // Don't allow dividers under 0% where they will be completely hidden.
380                if (newLeftPosition < 0)
381                    continue;
382
383                // When over 100% it is time to stop making/updating dividers.
384                if (newLeftPosition > 1)
385                    break;
386
387                // Don't allow the left-most divider spacing to be so tight it clips.
388                if ((newLeftPosition * visibleWidth) < WebInspector.TimelineRuler.MinimumLeftDividerSpacing)
389                    continue;
390            }
391
392            this._updatePositionOfElement(dividerElement, newLeftPosition, visibleWidth);
393            this._updatePositionOfElement(markerDividerElement, newLeftPosition, visibleWidth);
394
395            dividerElement._labelElement.textContent = isNaN(dividerTime) ? "" : Number.secondsToString(dividerTime - this._zeroTime, true);
396            dividerElement = dividerElement.nextSibling;
397        }
398
399        // Remove extra dividers.
400        while (dividerElement) {
401            var nextDividerElement = dividerElement.nextSibling;
402            dividerElement.remove();
403            dividerElement = nextDividerElement;
404        }
405
406        for (; i < markerDividers.length; ++i)
407            markerDividers[i].remove();
408
409        this._updateMarkers(visibleWidth, duration);
410        this._updateSelection(visibleWidth, duration);
411    },
412
413    updateLayoutIfNeeded: function()
414    {
415        // If there is a main layout scheduled we can just update layout and return, since that
416        // will update markers and the selection at the same time.
417        if (this._scheduledLayoutUpdateIdentifier) {
418            this.updateLayout();
419            return;
420        }
421
422        var visibleWidth = this._element.clientWidth;
423        if (visibleWidth <= 0)
424            return;
425
426        if (this._scheduledMarkerLayoutUpdateIdentifier)
427            this._updateMarkers(visibleWidth, this.duration);
428
429        if (this._scheduledSelectionLayoutUpdateIdentifier)
430            this._updateSelection(visibleWidth, this.duration);
431    },
432
433    // Private
434
435    _needsLayout: function()
436    {
437        if (this._scheduledLayoutUpdateIdentifier)
438            return;
439
440        if (this._scheduledMarkerLayoutUpdateIdentifier) {
441            cancelAnimationFrame(this._scheduledMarkerLayoutUpdateIdentifier);
442            delete this._scheduledMarkerLayoutUpdateIdentifier;
443        }
444
445        if (this._scheduledSelectionLayoutUpdateIdentifier) {
446            cancelAnimationFrame(this._scheduledSelectionLayoutUpdateIdentifier);
447            delete this._scheduledSelectionLayoutUpdateIdentifier;
448        }
449
450        this._scheduledLayoutUpdateIdentifier = requestAnimationFrame(this.updateLayout.bind(this));
451    },
452
453    _needsMarkerLayout: function()
454    {
455        // If layout is scheduled, abort since markers will be updated when layout happens.
456        if (this._scheduledLayoutUpdateIdentifier)
457            return;
458
459        if (this._scheduledMarkerLayoutUpdateIdentifier)
460            return;
461
462        function update()
463        {
464            delete this._scheduledMarkerLayoutUpdateIdentifier;
465
466            var visibleWidth = this._element.clientWidth;
467            if (visibleWidth <= 0)
468                return;
469
470            this._updateMarkers(visibleWidth, this.duration);
471        }
472
473        this._scheduledMarkerLayoutUpdateIdentifier = requestAnimationFrame(update.bind(this));
474    },
475
476    _needsSelectionLayout: function()
477    {
478        if (!this._allowsTimeRangeSelection)
479            return;
480
481        // If layout is scheduled, abort since the selection will be updated when layout happens.
482        if (this._scheduledLayoutUpdateIdentifier)
483            return;
484
485        if (this._scheduledSelectionLayoutUpdateIdentifier)
486            return;
487
488        function update()
489        {
490            delete this._scheduledSelectionLayoutUpdateIdentifier;
491
492            var visibleWidth = this._element.clientWidth;
493            if (visibleWidth <= 0)
494                return;
495
496            this._updateSelection(visibleWidth, this.duration);
497        }
498
499        this._scheduledSelectionLayoutUpdateIdentifier = requestAnimationFrame(update.bind(this));
500    },
501
502    _recalculate: function()
503    {
504        var visibleWidth = this._element.clientWidth;
505        if (visibleWidth <= 0)
506            return 0;
507
508        if (this._endTimePinned)
509            var duration = this._endTime - this._startTime;
510        else
511            var duration = visibleWidth * this._secondsPerPixel;
512
513        this._secondsPerPixel = duration / visibleWidth;
514
515        if (!this._endTimePinned)
516            this._endTime = this._startTime + (visibleWidth * this._secondsPerPixel);
517
518        return visibleWidth;
519    },
520
521    _updatePositionOfElement: function(element, newPosition, visibleWidth, property)
522    {
523        property = property || "left";
524
525        newPosition *= this._endTimePinned ? 100 : visibleWidth;
526        newPosition = newPosition.toFixed(2);
527
528        var currentPosition = parseFloat(element.style[property]).toFixed(2);
529        if (currentPosition !== newPosition)
530            element.style[property] = newPosition + (this._endTimePinned ? "%" : "px");
531    },
532
533    _updateMarkers: function(visibleWidth, duration)
534    {
535        if (this._scheduledMarkerLayoutUpdateIdentifier) {
536            cancelAnimationFrame(this._scheduledMarkerLayoutUpdateIdentifier);
537            delete this._scheduledMarkerLayoutUpdateIdentifier;
538        }
539
540        this._markerElementMap.forEach(function(markerElement, marker) {
541            var newLeftPosition = (marker.time - this._startTime) / duration;
542
543            this._updatePositionOfElement(markerElement, newLeftPosition, visibleWidth);
544
545            if (!markerElement.parentNode)
546                this._markersElement.appendChild(markerElement);
547        }, this);
548    },
549
550    _updateSelection: function(visibleWidth, duration)
551    {
552        if (this._scheduledSelectionLayoutUpdateIdentifier) {
553            cancelAnimationFrame(this._scheduledSelectionLayoutUpdateIdentifier);
554            delete this._scheduledSelectionLayoutUpdateIdentifier;
555        }
556
557        this._element.classList.toggle(WebInspector.TimelineRuler.AllowsTimeRangeSelectionStyleClassName, this._allowsTimeRangeSelection);
558
559        if (!this._allowsTimeRangeSelection)
560            return;
561
562        var newLeftPosition = Math.max(0, (this._selectionStartTime - this._startTime) / duration);
563        this._updatePositionOfElement(this._leftShadedAreaElement, newLeftPosition, visibleWidth, "width");
564        this._updatePositionOfElement(this._leftSelectionHandleElement, newLeftPosition, visibleWidth, "left");
565        this._updatePositionOfElement(this._selectionDragElement, newLeftPosition, visibleWidth, "left");
566
567        var newRightPosition = 1 - Math.min((this._selectionEndTime - this._startTime) / duration, 1);
568        this._updatePositionOfElement(this._rightShadedAreaElement, newRightPosition, visibleWidth, "width");
569        this._updatePositionOfElement(this._rightSelectionHandleElement, newRightPosition, visibleWidth, "right");
570        this._updatePositionOfElement(this._selectionDragElement, newRightPosition, visibleWidth, "right");
571
572        if (!this._selectionDragElement.parentNode) {
573            this._element.appendChild(this._selectionDragElement);
574            this._element.appendChild(this._leftShadedAreaElement);
575            this._element.appendChild(this._leftSelectionHandleElement);
576            this._element.appendChild(this._rightShadedAreaElement);
577            this._element.appendChild(this._rightSelectionHandleElement);
578        }
579
580        if (this._timeRangeSelectionChanged)
581            this._dispatchTimeRangeSelectionChangedEvent();
582    },
583
584    _dispatchTimeRangeSelectionChangedEvent: function()
585    {
586        delete this._timeRangeSelectionChanged;
587
588        if (this._suppressTimeRangeSelectionChangedEvent)
589            return;
590
591        this.dispatchEventToListeners(WebInspector.TimelineRuler.Event.TimeRangeSelectionChanged);
592    },
593
594    _timelineMarkerTimeChanged: function()
595    {
596        this._needsMarkerLayout();
597    },
598
599    _handleMouseDown: function(event)
600    {
601        // Only handle left mouse clicks.
602        if (event.button !== 0 || event.ctrlKey)
603            return;
604
605        this._selectionIsMove = event.target === this._selectionDragElement;
606        this._suppressTimeRangeSelectionChangedEvent = !this._selectionIsMove;
607
608        if (this._selectionIsMove)
609            this._lastMousePosition = event.pageX;
610        else
611            this._mouseDownPosition = event.pageX - this._element.totalOffsetLeft;
612
613        this._mouseMoveEventListener = this._handleMouseMove.bind(this);
614        this._mouseUpEventListener = this._handleMouseUp.bind(this);
615
616        // Register these listeners on the document so we can track the mouse if it leaves the ruler.
617        document.addEventListener("mousemove", this._mouseMoveEventListener);
618        document.addEventListener("mouseup", this._mouseUpEventListener);
619
620        event.preventDefault();
621        event.stopPropagation();
622    },
623
624    _handleMouseMove: function(event)
625    {
626        console.assert(event.button === 0);
627
628        if (this._selectionIsMove) {
629            var currentMousePosition = event.pageX;
630
631            var offsetTime = (currentMousePosition - this._lastMousePosition) * this.secondsPerPixel;
632            var selectionDuration = this.selectionEndTime - this.selectionStartTime;
633
634            this.selectionStartTime = Math.max(this.startTime, Math.min(this.selectionStartTime + offsetTime, this.endTime - selectionDuration));
635            this.selectionEndTime = this.selectionStartTime + selectionDuration;
636
637            this._lastMousePosition = currentMousePosition;
638        } else {
639            var currentMousePosition = event.pageX - this._element.totalOffsetLeft;
640
641            this.selectionStartTime = Math.max(this.startTime, this.startTime + (Math.min(currentMousePosition, this._mouseDownPosition) * this.secondsPerPixel));
642            this.selectionEndTime = Math.min(this.startTime + (Math.max(currentMousePosition, this._mouseDownPosition) * this.secondsPerPixel), this.endTime);
643        }
644
645        this._updateSelection(this._element.clientWidth, this.duration);
646
647        event.preventDefault();
648        event.stopPropagation();
649    },
650
651    _handleMouseUp: function(event)
652    {
653        console.assert(event.button === 0);
654
655        if (!this._selectionIsMove && this.selectionEndTime - this.selectionStartTime < WebInspector.TimelineRuler.MinimumSelectionTimeRange) {
656            // The section is smaller than allowed, grow in the direction of the drag to meet the minumum.
657            var currentMousePosition = event.pageX - this._element.totalOffsetLeft;
658            if (currentMousePosition > this._mouseDownPosition) {
659                this.selectionEndTime = Math.min(this.selectionStartTime + WebInspector.TimelineRuler.MinimumSelectionTimeRange, this.endTime);
660                this.selectionStartTime = this.selectionEndTime - WebInspector.TimelineRuler.MinimumSelectionTimeRange;
661            } else {
662                this.selectionStartTime = Math.max(this.startTime, this.selectionEndTime - WebInspector.TimelineRuler.MinimumSelectionTimeRange);
663                this.selectionEndTime = this.selectionStartTime + WebInspector.TimelineRuler.MinimumSelectionTimeRange
664            }
665        }
666
667        delete this._suppressTimeRangeSelectionChangedEvent;
668
669        this._dispatchTimeRangeSelectionChangedEvent();
670
671        document.removeEventListener("mousemove", this._mouseMoveEventListener);
672        document.removeEventListener("mouseup", this._mouseUpEventListener);
673
674        delete this._mouseMovedEventListener;
675        delete this._mouseUpEventListener;
676        delete this._mouseDownPosition;
677        delete this._lastMousePosition;
678        delete this._selectionIsMove;
679
680        event.preventDefault();
681        event.stopPropagation();
682    },
683
684    _handleSelectionHandleMouseDown: function(event)
685    {
686        // Only handle left mouse clicks.
687        if (event.button !== 0 || event.ctrlKey)
688            return;
689
690        this._dragHandleIsStartTime = event.target === this._leftSelectionHandleElement;
691        this._mouseDownPosition = event.pageX - this._element.totalOffsetLeft;
692
693        this._selectionHandleMouseMoveEventListener = this._handleSelectionHandleMouseMove.bind(this);
694        this._selectionHandleMouseUpEventListener = this._handleSelectionHandleMouseUp.bind(this);
695
696        // Register these listeners on the document so we can track the mouse if it leaves the ruler.
697        document.addEventListener("mousemove", this._selectionHandleMouseMoveEventListener);
698        document.addEventListener("mouseup", this._selectionHandleMouseUpEventListener);
699
700        event.preventDefault();
701        event.stopPropagation();
702    },
703
704    _handleSelectionHandleMouseMove: function(event)
705    {
706        console.assert(event.button === 0);
707
708        var currentMousePosition = event.pageX - this._element.totalOffsetLeft;
709        var currentTime = this.startTime + (currentMousePosition * this.secondsPerPixel);
710
711        if (event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey) {
712            // Resize the selection on both sides when the Option keys is held down.
713            if (this._dragHandleIsStartTime) {
714                var timeDifference = currentTime - this.selectionStartTime;
715                this.selectionStartTime = Math.max(this.startTime, Math.min(currentTime, this.selectionEndTime - WebInspector.TimelineRuler.MinimumSelectionTimeRange));
716                this.selectionEndTime = Math.min(Math.max(this.selectionStartTime + WebInspector.TimelineRuler.MinimumSelectionTimeRange, this.selectionEndTime - timeDifference), this.endTime);
717            } else {
718                var timeDifference = currentTime - this.selectionEndTime;
719                this.selectionEndTime = Math.min(Math.max(this.selectionStartTime + WebInspector.TimelineRuler.MinimumSelectionTimeRange, currentTime), this.endTime);
720                this.selectionStartTime = Math.max(this.startTime, Math.min(this.selectionStartTime - timeDifference, this.selectionEndTime - WebInspector.TimelineRuler.MinimumSelectionTimeRange));
721            }
722        } else {
723            // Resize the selection on side being dragged.
724            if (this._dragHandleIsStartTime)
725                this.selectionStartTime = Math.max(this.startTime, Math.min(currentTime, this.selectionEndTime - WebInspector.TimelineRuler.MinimumSelectionTimeRange));
726            else
727                this.selectionEndTime = Math.min(Math.max(this.selectionStartTime + WebInspector.TimelineRuler.MinimumSelectionTimeRange, currentTime), this.endTime);
728        }
729
730        this._updateSelection(this._element.clientWidth, this.duration);
731
732        event.preventDefault();
733        event.stopPropagation();
734    },
735
736    _handleSelectionHandleMouseUp: function(event)
737    {
738        console.assert(event.button === 0);
739
740        document.removeEventListener("mousemove", this._selectionHandleMouseMoveEventListener);
741        document.removeEventListener("mouseup", this._selectionHandleMouseUpEventListener);
742
743        delete this._selectionHandleMouseMoveEventListener;
744        delete this._selectionHandleMouseUpEventListener;
745        delete this._dragHandleIsStartTime;
746        delete this._mouseDownPosition;
747
748        event.preventDefault();
749        event.stopPropagation();
750    }
751}
752
753WebInspector.TimelineRuler.prototype.__proto__ = WebInspector.Object.prototype;
754