1/*
2 * Copyright (C) 2011 Google Inc.  All rights reserved.
3 * Copyright (C) 2006, 2007, 2008 Apple Inc.  All rights reserved.
4 * Copyright (C) 2007 Matt Lilek (pewtermoose@gmail.com).
5 * Copyright (C) 2009 Joseph Pecoraro
6 *
7 * Redistribution and use in source and binary forms, with or without
8 * modification, are permitted provided that the following conditions
9 * are met:
10 *
11 * 1.  Redistributions of source code must retain the above copyright
12 *     notice, this list of conditions and the following disclaimer.
13 * 2.  Redistributions in binary form must reproduce the above copyright
14 *     notice, this list of conditions and the following disclaimer in the
15 *     documentation and/or other materials provided with the distribution.
16 * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
17 *     its contributors may be used to endorse or promote products derived
18 *     from this software without specific prior written permission.
19 *
20 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
21 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
24 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
25 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
26 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
27 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
29 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 */
31
32/**
33 * @param {!Element} element
34 * @param {?function(!MouseEvent): boolean} elementDragStart
35 * @param {function(!MouseEvent)} elementDrag
36 * @param {?function(!MouseEvent)} elementDragEnd
37 * @param {string} cursor
38 * @param {?string=} hoverCursor
39 */
40WebInspector.installDragHandle = function(element, elementDragStart, elementDrag, elementDragEnd, cursor, hoverCursor)
41{
42    element.addEventListener("mousedown", WebInspector.elementDragStart.bind(WebInspector, elementDragStart, elementDrag, elementDragEnd, cursor), false);
43    if (hoverCursor !== null)
44        element.style.cursor = hoverCursor || cursor;
45}
46
47/**
48 * @param {?function(!MouseEvent):boolean} elementDragStart
49 * @param {function(!MouseEvent)} elementDrag
50 * @param {?function(!MouseEvent)} elementDragEnd
51 * @param {string} cursor
52 * @param {!Event} event
53 */
54WebInspector.elementDragStart = function(elementDragStart, elementDrag, elementDragEnd, cursor, event)
55{
56    // Only drag upon left button. Right will likely cause a context menu. So will ctrl-click on mac.
57    if (event.button || (WebInspector.isMac() && event.ctrlKey))
58        return;
59
60    if (WebInspector._elementDraggingEventListener)
61        return;
62
63    if (elementDragStart && !elementDragStart(/** @type {!MouseEvent} */ (event)))
64        return;
65
66    if (WebInspector._elementDraggingGlassPane) {
67        WebInspector._elementDraggingGlassPane.dispose();
68        delete WebInspector._elementDraggingGlassPane;
69    }
70
71    var targetDocument = event.target.ownerDocument;
72
73    WebInspector._elementDraggingEventListener = elementDrag;
74    WebInspector._elementEndDraggingEventListener = elementDragEnd;
75    WebInspector._mouseOutWhileDraggingTargetDocument = targetDocument;
76
77    targetDocument.addEventListener("mousemove", WebInspector._elementDragMove, true);
78    targetDocument.addEventListener("mouseup", WebInspector._elementDragEnd, true);
79    targetDocument.addEventListener("mouseout", WebInspector._mouseOutWhileDragging, true);
80
81    targetDocument.body.style.cursor = cursor;
82
83    event.preventDefault();
84}
85
86WebInspector._mouseOutWhileDragging = function()
87{
88    WebInspector._unregisterMouseOutWhileDragging();
89    WebInspector._elementDraggingGlassPane = new WebInspector.GlassPane();
90}
91
92WebInspector._unregisterMouseOutWhileDragging = function()
93{
94    if (!WebInspector._mouseOutWhileDraggingTargetDocument)
95        return;
96    WebInspector._mouseOutWhileDraggingTargetDocument.removeEventListener("mouseout", WebInspector._mouseOutWhileDragging, true);
97    delete WebInspector._mouseOutWhileDraggingTargetDocument;
98}
99
100/**
101 * @param {!Event} event
102 */
103WebInspector._elementDragMove = function(event)
104{
105    if (WebInspector._elementDraggingEventListener(/** @type {!MouseEvent} */ (event)))
106        WebInspector._cancelDragEvents(event);
107}
108
109/**
110 * @param {!Event} event
111 */
112WebInspector._cancelDragEvents = function(event)
113{
114    var targetDocument = event.target.ownerDocument;
115    targetDocument.removeEventListener("mousemove", WebInspector._elementDragMove, true);
116    targetDocument.removeEventListener("mouseup", WebInspector._elementDragEnd, true);
117    WebInspector._unregisterMouseOutWhileDragging();
118
119    targetDocument.body.style.removeProperty("cursor");
120
121    if (WebInspector._elementDraggingGlassPane)
122        WebInspector._elementDraggingGlassPane.dispose();
123
124    delete WebInspector._elementDraggingGlassPane;
125    delete WebInspector._elementDraggingEventListener;
126    delete WebInspector._elementEndDraggingEventListener;
127}
128
129/**
130 * @param {!Event} event
131 */
132WebInspector._elementDragEnd = function(event)
133{
134    var elementDragEnd = WebInspector._elementEndDraggingEventListener;
135
136    WebInspector._cancelDragEvents(/** @type {!MouseEvent} */ (event));
137
138    event.preventDefault();
139    if (elementDragEnd)
140        elementDragEnd(/** @type {!MouseEvent} */ (event));
141}
142
143/**
144 * @constructor
145 */
146WebInspector.GlassPane = function()
147{
148    this.element = document.createElement("div");
149    this.element.style.cssText = "position:absolute;top:0;bottom:0;left:0;right:0;background-color:transparent;z-index:1000;";
150    this.element.id = "glass-pane";
151    document.body.appendChild(this.element);
152    WebInspector._glassPane = this;
153}
154
155WebInspector.GlassPane.prototype = {
156    dispose: function()
157    {
158        delete WebInspector._glassPane;
159        if (WebInspector.GlassPane.DefaultFocusedViewStack.length)
160            WebInspector.GlassPane.DefaultFocusedViewStack.peekLast().focus();
161        this.element.remove();
162    }
163}
164
165/**
166 * @type {!Array.<!WebInspector.View|!WebInspector.Dialog>}
167 */
168WebInspector.GlassPane.DefaultFocusedViewStack = [];
169
170/**
171 * @param {?Node=} node
172 * @return {boolean}
173 */
174WebInspector.isBeingEdited = function(node)
175{
176    if (!node || node.nodeType !== Node.ELEMENT_NODE)
177        return false;
178    var element = /** {!Element} */ (node);
179    if (element.classList.contains("text-prompt") || element.nodeName === "INPUT" || element.nodeName === "TEXTAREA")
180        return true;
181
182    if (!WebInspector.__editingCount)
183        return false;
184
185    while (element) {
186        if (element.__editing)
187            return true;
188        element = element.parentElement;
189    }
190    return false;
191}
192
193/**
194 * @param {!Element} element
195 * @param {boolean} value
196 * @return {boolean}
197 */
198WebInspector.markBeingEdited = function(element, value)
199{
200    if (value) {
201        if (element.__editing)
202            return false;
203        element.classList.add("being-edited");
204        element.__editing = true;
205        WebInspector.__editingCount = (WebInspector.__editingCount || 0) + 1;
206    } else {
207        if (!element.__editing)
208            return false;
209        element.classList.remove("being-edited");
210        delete element.__editing;
211        --WebInspector.__editingCount;
212    }
213    return true;
214}
215
216WebInspector.CSSNumberRegex = /^(-?(?:\d+(?:\.\d+)?|\.\d+))$/;
217
218WebInspector.StyleValueDelimiters = " \xA0\t\n\"':;,/()";
219
220
221/**
222  * @param {!Event} event
223  * @return {?string}
224  */
225WebInspector._valueModificationDirection = function(event)
226{
227    var direction = null;
228    if (event.type === "mousewheel") {
229        if (event.wheelDeltaY > 0)
230            direction = "Up";
231        else if (event.wheelDeltaY < 0)
232            direction = "Down";
233    } else {
234        if (event.keyIdentifier === "Up" || event.keyIdentifier === "PageUp")
235            direction = "Up";
236        else if (event.keyIdentifier === "Down" || event.keyIdentifier === "PageDown")
237            direction = "Down";
238    }
239    return direction;
240}
241
242/**
243 * @param {string} hexString
244 * @param {!Event} event
245 */
246WebInspector._modifiedHexValue = function(hexString, event)
247{
248    var direction = WebInspector._valueModificationDirection(event);
249    if (!direction)
250        return hexString;
251
252    var number = parseInt(hexString, 16);
253    if (isNaN(number) || !isFinite(number))
254        return hexString;
255
256    var maxValue = Math.pow(16, hexString.length) - 1;
257    var arrowKeyOrMouseWheelEvent = (event.keyIdentifier === "Up" || event.keyIdentifier === "Down" || event.type === "mousewheel");
258    var delta;
259
260    if (arrowKeyOrMouseWheelEvent)
261        delta = (direction === "Up") ? 1 : -1;
262    else
263        delta = (event.keyIdentifier === "PageUp") ? 16 : -16;
264
265    if (event.shiftKey)
266        delta *= 16;
267
268    var result = number + delta;
269    if (result < 0)
270        result = 0; // Color hex values are never negative, so clamp to 0.
271    else if (result > maxValue)
272        return hexString;
273
274    // Ensure the result length is the same as the original hex value.
275    var resultString = result.toString(16).toUpperCase();
276    for (var i = 0, lengthDelta = hexString.length - resultString.length; i < lengthDelta; ++i)
277        resultString = "0" + resultString;
278    return resultString;
279}
280
281/**
282 * @param {number} number
283 * @param {!Event} event
284 */
285WebInspector._modifiedFloatNumber = function(number, event)
286{
287    var direction = WebInspector._valueModificationDirection(event);
288    if (!direction)
289        return number;
290
291    var arrowKeyOrMouseWheelEvent = (event.keyIdentifier === "Up" || event.keyIdentifier === "Down" || event.type === "mousewheel");
292
293    // Jump by 10 when shift is down or jump by 0.1 when Alt/Option is down.
294    // Also jump by 10 for page up and down, or by 100 if shift is held with a page key.
295    var changeAmount = 1;
296    if (event.shiftKey && !arrowKeyOrMouseWheelEvent)
297        changeAmount = 100;
298    else if (event.shiftKey || !arrowKeyOrMouseWheelEvent)
299        changeAmount = 10;
300    else if (event.altKey)
301        changeAmount = 0.1;
302
303    if (direction === "Down")
304        changeAmount *= -1;
305
306    // Make the new number and constrain it to a precision of 6, this matches numbers the engine returns.
307    // Use the Number constructor to forget the fixed precision, so 1.100000 will print as 1.1.
308    var result = Number((number + changeAmount).toFixed(6));
309    if (!String(result).match(WebInspector.CSSNumberRegex))
310        return null;
311
312    return result;
313}
314
315/**
316  * @param {!Event} event
317  * @param {!Element} element
318  * @param {function(string,string)=} finishHandler
319  * @param {function(string)=} suggestionHandler
320  * @param {function(string, number, string):string=} customNumberHandler
321  * @return {boolean}
322 */
323WebInspector.handleElementValueModifications = function(event, element, finishHandler, suggestionHandler, customNumberHandler)
324{
325    var arrowKeyOrMouseWheelEvent = (event.keyIdentifier === "Up" || event.keyIdentifier === "Down" || event.type === "mousewheel");
326    var pageKeyPressed = (event.keyIdentifier === "PageUp" || event.keyIdentifier === "PageDown");
327    if (!arrowKeyOrMouseWheelEvent && !pageKeyPressed)
328        return false;
329
330    var selection = window.getSelection();
331    if (!selection.rangeCount)
332        return false;
333
334    var selectionRange = selection.getRangeAt(0);
335    if (!selectionRange.commonAncestorContainer.isSelfOrDescendant(element))
336        return false;
337
338    var originalValue = element.textContent;
339    var wordRange = selectionRange.startContainer.rangeOfWord(selectionRange.startOffset, WebInspector.StyleValueDelimiters, element);
340    var wordString = wordRange.toString();
341
342    if (suggestionHandler && suggestionHandler(wordString))
343        return false;
344
345    var replacementString;
346    var prefix, suffix, number;
347
348    var matches;
349    matches = /(.*#)([\da-fA-F]+)(.*)/.exec(wordString);
350    if (matches && matches.length) {
351        prefix = matches[1];
352        suffix = matches[3];
353        number = WebInspector._modifiedHexValue(matches[2], event);
354
355        replacementString = customNumberHandler ? customNumberHandler(prefix, number, suffix) : prefix + number + suffix;
356    } else {
357        matches = /(.*?)(-?(?:\d+(?:\.\d+)?|\.\d+))(.*)/.exec(wordString);
358        if (matches && matches.length) {
359            prefix = matches[1];
360            suffix = matches[3];
361            number = WebInspector._modifiedFloatNumber(parseFloat(matches[2]), event);
362
363            // Need to check for null explicitly.
364            if (number === null)
365                return false;
366
367            replacementString = customNumberHandler ? customNumberHandler(prefix, number, suffix) : prefix + number + suffix;
368        }
369    }
370
371    if (replacementString) {
372        var replacementTextNode = document.createTextNode(replacementString);
373
374        wordRange.deleteContents();
375        wordRange.insertNode(replacementTextNode);
376
377        var finalSelectionRange = document.createRange();
378        finalSelectionRange.setStart(replacementTextNode, 0);
379        finalSelectionRange.setEnd(replacementTextNode, replacementString.length);
380
381        selection.removeAllRanges();
382        selection.addRange(finalSelectionRange);
383
384        event.handled = true;
385        event.preventDefault();
386
387        if (finishHandler)
388            finishHandler(originalValue, replacementString);
389
390        return true;
391    }
392    return false;
393}
394
395/**
396 * @param {number} ms
397 * @param {number=} precision
398 * @return {string}
399 */
400Number.preciseMillisToString = function(ms, precision)
401{
402  precision = precision || 0;
403  var format = "%." + precision + "f\u2009ms";
404  return WebInspector.UIString(format, ms);
405}
406
407/** @type {!WebInspector.UIStringFormat} */
408WebInspector._subMillisFormat = new WebInspector.UIStringFormat("%.3f\u2009ms");
409
410/** @type {!WebInspector.UIStringFormat} */
411WebInspector._millisFormat = new WebInspector.UIStringFormat("%.0f\u2009ms");
412
413/** @type {!WebInspector.UIStringFormat} */
414WebInspector._secondsFormat = new WebInspector.UIStringFormat("%.2f\u2009s");
415
416/** @type {!WebInspector.UIStringFormat} */
417WebInspector._minutesFormat = new WebInspector.UIStringFormat("%.1f\u2009min");
418
419/** @type {!WebInspector.UIStringFormat} */
420WebInspector._hoursFormat = new WebInspector.UIStringFormat("%.1f\u2009hrs");
421
422/** @type {!WebInspector.UIStringFormat} */
423WebInspector._daysFormat = new WebInspector.UIStringFormat("%.1f\u2009days");
424
425/**
426 * @param {number} ms
427 * @param {boolean=} higherResolution
428 * @return {string}
429 */
430Number.millisToString = function(ms, higherResolution)
431{
432    if (!isFinite(ms))
433        return "-";
434
435    if (ms === 0)
436        return "0";
437
438    if (higherResolution && ms < 1000)
439        return WebInspector._subMillisFormat.format(ms);
440    else if (ms < 1000)
441        return WebInspector._millisFormat.format(ms);
442
443    var seconds = ms / 1000;
444    if (seconds < 60)
445        return WebInspector._secondsFormat.format(seconds);
446
447    var minutes = seconds / 60;
448    if (minutes < 60)
449        return WebInspector._minutesFormat.format(minutes);
450
451    var hours = minutes / 60;
452    if (hours < 24)
453        return WebInspector._hoursFormat.format(hours);
454
455    var days = hours / 24;
456    return WebInspector._daysFormat.format(days);
457}
458
459/**
460 * @param {number} seconds
461 * @param {boolean=} higherResolution
462 * @return {string}
463 */
464Number.secondsToString = function(seconds, higherResolution)
465{
466    if (!isFinite(seconds))
467        return "-";
468    return Number.millisToString(seconds * 1000, higherResolution);
469}
470
471/**
472 * @param {number} bytes
473 * @return {string}
474 */
475Number.bytesToString = function(bytes)
476{
477    if (bytes < 1024)
478        return WebInspector.UIString("%.0f\u2009B", bytes);
479
480    var kilobytes = bytes / 1024;
481    if (kilobytes < 100)
482        return WebInspector.UIString("%.1f\u2009KB", kilobytes);
483    if (kilobytes < 1024)
484        return WebInspector.UIString("%.0f\u2009KB", kilobytes);
485
486    var megabytes = kilobytes / 1024;
487    if (megabytes < 100)
488        return WebInspector.UIString("%.1f\u2009MB", megabytes);
489    else
490        return WebInspector.UIString("%.0f\u2009MB", megabytes);
491}
492
493/**
494 * @param {number} num
495 * @return {string}
496 */
497Number.withThousandsSeparator = function(num)
498{
499    var str = num + "";
500    var re = /(\d+)(\d{3})/;
501    while (str.match(re))
502        str = str.replace(re, "$1\u2009$2"); // \u2009 is a thin space.
503    return str;
504}
505
506/**
507 * @return {boolean}
508 */
509WebInspector.useLowerCaseMenuTitles = function()
510{
511    return WebInspector.platform() === "windows";
512}
513
514/**
515 * @param {string} format
516 * @param {?ArrayLike} substitutions
517 * @param {!Object.<string, function(string, ...):*>} formatters
518 * @param {string} initialValue
519 * @param {function(string, string): ?} append
520 * @return {!{formattedResult: string, unusedSubstitutions: ?ArrayLike}};
521 */
522WebInspector.formatLocalized = function(format, substitutions, formatters, initialValue, append)
523{
524    return String.format(WebInspector.UIString(format), substitutions, formatters, initialValue, append);
525}
526
527/**
528 * @return {string}
529 */
530WebInspector.openLinkExternallyLabel = function()
531{
532    return WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Open link in new tab" : "Open Link in New Tab");
533}
534
535/**
536 * @return {string}
537 */
538WebInspector.copyLinkAddressLabel = function()
539{
540    return WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Copy link address" : "Copy Link Address");
541}
542
543/**
544 * @return {string}
545 */
546WebInspector.anotherProfilerActiveLabel = function()
547{
548    return WebInspector.UIString("Another profiler is already active");
549}
550
551/**
552 * @param {string|undefined} description
553 * @return {string}
554 */
555WebInspector.asyncStackTraceLabel = function(description)
556{
557    if (description)
558        return description + " " + WebInspector.UIString("(async)");
559    return WebInspector.UIString("Async Call");
560}
561
562/**
563 * @return {string}
564 */
565WebInspector.manageBlackboxingButtonLabel = function()
566{
567    return WebInspector.UIString("Manage framework blackboxing...");
568}
569
570WebInspector.installPortStyles = function()
571{
572    var platform = WebInspector.platform();
573    document.body.classList.add("platform-" + platform);
574    var flavor = WebInspector.platformFlavor();
575    if (flavor)
576        document.body.classList.add("platform-" + flavor);
577    var port = WebInspector.port();
578    document.body.classList.add("port-" + port);
579}
580
581WebInspector._windowFocused = function(event)
582{
583    if (event.target.document.nodeType === Node.DOCUMENT_NODE)
584        document.body.classList.remove("inactive");
585}
586
587WebInspector._windowBlurred = function(event)
588{
589    if (event.target.document.nodeType === Node.DOCUMENT_NODE)
590        document.body.classList.add("inactive");
591}
592
593/**
594 * @return {!Element}
595 */
596WebInspector.previousFocusElement = function()
597{
598    return WebInspector._previousFocusElement;
599}
600
601/**
602 * @return {!Element}
603 */
604WebInspector.currentFocusElement = function()
605{
606    return WebInspector._currentFocusElement;
607}
608
609WebInspector._focusChanged = function(event)
610{
611    WebInspector.setCurrentFocusElement(event.target);
612}
613
614WebInspector._documentBlurred = function(event)
615{
616    // We want to know when currentFocusElement loses focus to nowhere.
617    // This is the case when event.relatedTarget is null (no element is being focused)
618    // and document.activeElement is reset to default (this is not a window blur).
619    if (!event.relatedTarget && document.activeElement === document.body)
620      WebInspector.setCurrentFocusElement(null);
621}
622
623WebInspector._textInputTypes = ["text", "search", "tel", "url", "email", "password"].keySet();
624WebInspector._isTextEditingElement = function(element)
625{
626    if (element instanceof HTMLInputElement)
627        return element.type in WebInspector._textInputTypes;
628
629    if (element instanceof HTMLTextAreaElement)
630        return true;
631
632    return false;
633}
634
635WebInspector.setCurrentFocusElement = function(x)
636{
637    if (WebInspector._glassPane && x && !WebInspector._glassPane.element.isAncestor(x))
638        return;
639    if (WebInspector._currentFocusElement !== x)
640        WebInspector._previousFocusElement = WebInspector._currentFocusElement;
641    WebInspector._currentFocusElement = x;
642
643    if (WebInspector._currentFocusElement) {
644        WebInspector._currentFocusElement.focus();
645
646        // Make a caret selection inside the new element if there isn't a range selection and there isn't already a caret selection inside.
647        // This is needed (at least) to remove caret from console when focus is moved to some element in the panel.
648        // The code below should not be applied to text fields and text areas, hence _isTextEditingElement check.
649        var selection = window.getSelection();
650        if (!WebInspector._isTextEditingElement(WebInspector._currentFocusElement) && selection.isCollapsed && !WebInspector._currentFocusElement.isInsertionCaretInside()) {
651            var selectionRange = WebInspector._currentFocusElement.ownerDocument.createRange();
652            selectionRange.setStart(WebInspector._currentFocusElement, 0);
653            selectionRange.setEnd(WebInspector._currentFocusElement, 0);
654
655            selection.removeAllRanges();
656            selection.addRange(selectionRange);
657        }
658    } else if (WebInspector._previousFocusElement)
659        WebInspector._previousFocusElement.blur();
660}
661
662WebInspector.restoreFocusFromElement = function(element)
663{
664    if (element && element.isSelfOrAncestor(WebInspector.currentFocusElement()))
665        WebInspector.setCurrentFocusElement(WebInspector.previousFocusElement());
666}
667
668WebInspector.setToolbarColors = function(backgroundColor, color)
669{
670    if (!WebInspector._themeStyleElement) {
671        WebInspector._themeStyleElement = document.createElement("style");
672        document.head.appendChild(WebInspector._themeStyleElement);
673    }
674    var parsedColor = WebInspector.Color.parse(color);
675    var shadowColor = parsedColor ? parsedColor.invert().setAlpha(0.33).toString(WebInspector.Color.Format.RGBA) : "white";
676    var prefix = WebInspector.isMac() ? "body:not(.undocked)" : "";
677    WebInspector._themeStyleElement.textContent =
678        String.sprintf(
679            "%s .toolbar-colors {\
680                 background-image: none !important;\
681                 background-color: %s !important;\
682                 color: %s !important;\
683             }", prefix, backgroundColor, color) +
684        String.sprintf(
685             "%s .toolbar-colors button.status-bar-item .glyph, %s .toolbar-colors button.status-bar-item .long-click-glyph {\
686                 background-color: %s;\
687             }", prefix, prefix, color) +
688        String.sprintf(
689             "%s .toolbar-colors button.status-bar-item .glyph.shadow, %s .toolbar-colors button.status-bar-item .long-click-glyph.shadow {\
690                 background-color: %s;\
691             }", prefix, prefix, shadowColor);
692}
693
694WebInspector.resetToolbarColors = function()
695{
696    if (WebInspector._themeStyleElement)
697        WebInspector._themeStyleElement.textContent = "";
698}
699
700/**
701 * @param {!Element} element
702 * @param {number} offset
703 * @param {number} length
704 * @param {!Array.<!Object>=} domChanges
705 * @return {?Element}
706 */
707WebInspector.highlightSearchResult = function(element, offset, length, domChanges)
708{
709    var result = WebInspector.highlightSearchResults(element, [new WebInspector.SourceRange(offset, length)], domChanges);
710    return result.length ? result[0] : null;
711}
712
713/**
714 * @param {!Element} element
715 * @param {!Array.<!WebInspector.SourceRange>} resultRanges
716 * @param {!Array.<!Object>=} changes
717 * @return {!Array.<!Element>}
718 */
719WebInspector.highlightSearchResults = function(element, resultRanges, changes)
720{
721    return WebInspector.highlightRangesWithStyleClass(element, resultRanges, "highlighted-search-result", changes);
722}
723
724/**
725 * @param {!Element} element
726 * @param {string} className
727 */
728WebInspector.runCSSAnimationOnce = function(element, className)
729{
730    function animationEndCallback()
731    {
732        element.classList.remove(className);
733        element.removeEventListener("animationend", animationEndCallback, false);
734    }
735
736    if (element.classList.contains(className))
737        element.classList.remove(className);
738
739    element.addEventListener("animationend", animationEndCallback, false);
740    element.classList.add(className);
741}
742
743/**
744 * @param {!Element} element
745 * @param {!Array.<!WebInspector.SourceRange>} resultRanges
746 * @param {string} styleClass
747 * @param {!Array.<!Object>=} changes
748 * @return {!Array.<!Element>}
749 */
750WebInspector.highlightRangesWithStyleClass = function(element, resultRanges, styleClass, changes)
751{
752    changes = changes || [];
753    var highlightNodes = [];
754    var lineText = element.textContent;
755    var ownerDocument = element.ownerDocument;
756    var textNodeSnapshot = ownerDocument.evaluate(".//text()", element, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
757
758    var snapshotLength = textNodeSnapshot.snapshotLength;
759    if (snapshotLength === 0)
760        return highlightNodes;
761
762    var nodeRanges = [];
763    var rangeEndOffset = 0;
764    for (var i = 0; i < snapshotLength; ++i) {
765        var range = {};
766        range.offset = rangeEndOffset;
767        range.length = textNodeSnapshot.snapshotItem(i).textContent.length;
768        rangeEndOffset = range.offset + range.length;
769        nodeRanges.push(range);
770    }
771
772    var startIndex = 0;
773    for (var i = 0; i < resultRanges.length; ++i) {
774        var startOffset = resultRanges[i].offset;
775        var endOffset = startOffset + resultRanges[i].length;
776
777        while (startIndex < snapshotLength && nodeRanges[startIndex].offset + nodeRanges[startIndex].length <= startOffset)
778            startIndex++;
779        var endIndex = startIndex;
780        while (endIndex < snapshotLength && nodeRanges[endIndex].offset + nodeRanges[endIndex].length < endOffset)
781            endIndex++;
782        if (endIndex === snapshotLength)
783            break;
784
785        var highlightNode = ownerDocument.createElement("span");
786        highlightNode.className = styleClass;
787        highlightNode.textContent = lineText.substring(startOffset, endOffset);
788
789        var lastTextNode = textNodeSnapshot.snapshotItem(endIndex);
790        var lastText = lastTextNode.textContent;
791        lastTextNode.textContent = lastText.substring(endOffset - nodeRanges[endIndex].offset);
792        changes.push({ node: lastTextNode, type: "changed", oldText: lastText, newText: lastTextNode.textContent });
793
794        if (startIndex === endIndex) {
795            lastTextNode.parentElement.insertBefore(highlightNode, lastTextNode);
796            changes.push({ node: highlightNode, type: "added", nextSibling: lastTextNode, parent: lastTextNode.parentElement });
797            highlightNodes.push(highlightNode);
798
799            var prefixNode = ownerDocument.createTextNode(lastText.substring(0, startOffset - nodeRanges[startIndex].offset));
800            lastTextNode.parentElement.insertBefore(prefixNode, highlightNode);
801            changes.push({ node: prefixNode, type: "added", nextSibling: highlightNode, parent: lastTextNode.parentElement });
802        } else {
803            var firstTextNode = textNodeSnapshot.snapshotItem(startIndex);
804            var firstText = firstTextNode.textContent;
805            var anchorElement = firstTextNode.nextSibling;
806
807            firstTextNode.parentElement.insertBefore(highlightNode, anchorElement);
808            changes.push({ node: highlightNode, type: "added", nextSibling: anchorElement, parent: firstTextNode.parentElement });
809            highlightNodes.push(highlightNode);
810
811            firstTextNode.textContent = firstText.substring(0, startOffset - nodeRanges[startIndex].offset);
812            changes.push({ node: firstTextNode, type: "changed", oldText: firstText, newText: firstTextNode.textContent });
813
814            for (var j = startIndex + 1; j < endIndex; j++) {
815                var textNode = textNodeSnapshot.snapshotItem(j);
816                var text = textNode.textContent;
817                textNode.textContent = "";
818                changes.push({ node: textNode, type: "changed", oldText: text, newText: textNode.textContent });
819            }
820        }
821        startIndex = endIndex;
822        nodeRanges[startIndex].offset = endOffset;
823        nodeRanges[startIndex].length = lastTextNode.textContent.length;
824
825    }
826    return highlightNodes;
827}
828
829WebInspector.applyDomChanges = function(domChanges)
830{
831    for (var i = 0, size = domChanges.length; i < size; ++i) {
832        var entry = domChanges[i];
833        switch (entry.type) {
834        case "added":
835            entry.parent.insertBefore(entry.node, entry.nextSibling);
836            break;
837        case "changed":
838            entry.node.textContent = entry.newText;
839            break;
840        }
841    }
842}
843
844WebInspector.revertDomChanges = function(domChanges)
845{
846    for (var i = domChanges.length - 1; i >= 0; --i) {
847        var entry = domChanges[i];
848        switch (entry.type) {
849        case "added":
850            entry.node.remove();
851            break;
852        case "changed":
853            entry.node.textContent = entry.oldText;
854            break;
855        }
856    }
857}
858
859/**
860 * @constructor
861 * @param {boolean} autoInvoke
862 */
863WebInspector.InvokeOnceHandlers = function(autoInvoke)
864{
865    this._handlers = null;
866    this._autoInvoke = autoInvoke;
867}
868
869WebInspector.InvokeOnceHandlers.prototype = {
870    /**
871     * @param {!Object} object
872     * @param {function()} method
873     */
874    add: function(object, method)
875    {
876        if (!this._handlers) {
877            this._handlers = new Map();
878            if (this._autoInvoke)
879                this.scheduleInvoke();
880        }
881        var methods = this._handlers.get(object);
882        if (!methods) {
883            methods = new Set();
884            this._handlers.set(object, methods);
885        }
886        methods.add(method);
887    },
888
889    scheduleInvoke: function()
890    {
891        if (this._handlers)
892            requestAnimationFrame(this._invoke.bind(this));
893    },
894
895    _invoke: function()
896    {
897        var handlers = this._handlers;
898        this._handlers = null;
899        var keys = handlers.keys();
900        for (var i = 0; i < keys.length; ++i) {
901            var object = keys[i];
902            var methods = handlers.get(object).values();
903            for (var j = 0; j < methods.length; ++j)
904                methods[j].call(object);
905        }
906    }
907}
908
909WebInspector._coalescingLevel = 0;
910WebInspector._postUpdateHandlers = null;
911
912WebInspector.startBatchUpdate = function()
913{
914    if (!WebInspector._coalescingLevel++)
915        WebInspector._postUpdateHandlers = new WebInspector.InvokeOnceHandlers(false);
916}
917
918WebInspector.endBatchUpdate = function()
919{
920    if (--WebInspector._coalescingLevel)
921        return;
922    WebInspector._postUpdateHandlers.scheduleInvoke();
923    WebInspector._postUpdateHandlers = null;
924}
925
926/**
927 * @param {!Object} object
928 * @param {function()} method
929 */
930WebInspector.invokeOnceAfterBatchUpdate = function(object, method)
931{
932    if (!WebInspector._postUpdateHandlers)
933        WebInspector._postUpdateHandlers = new WebInspector.InvokeOnceHandlers(true);
934    WebInspector._postUpdateHandlers.add(object, method);
935}
936
937/**
938 * @param {!Function} func
939 * @param {!Array.<{from:number, to:number}>} params
940 * @param {number} frames
941 * @param {function()=} animationComplete
942 * @return {function()}
943 */
944WebInspector.animateFunction = function(func, params, frames, animationComplete)
945{
946    var values = new Array(params.length);
947    var deltas = new Array(params.length);
948    for (var i = 0; i < params.length; ++i) {
949        values[i] = params[i].from;
950        deltas[i] = (params[i].to - params[i].from) / frames;
951    }
952
953    var raf = requestAnimationFrame(animationStep);
954
955    var framesLeft = frames;
956
957    function animationStep()
958    {
959        if (--framesLeft < 0) {
960            if (animationComplete)
961                animationComplete();
962            return;
963        }
964        for (var i = 0; i < params.length; ++i) {
965            if (params[i].to > params[i].from)
966                values[i] = Number.constrain(values[i] + deltas[i], params[i].from, params[i].to);
967            else
968                values[i] = Number.constrain(values[i] + deltas[i], params[i].to, params[i].from);
969        }
970        func.apply(null, values);
971        raf = window.requestAnimationFrame(animationStep);
972    }
973
974    function cancelAnimation()
975    {
976        window.cancelAnimationFrame(raf);
977    }
978
979    return cancelAnimation;
980}
981
982/**
983 * @constructor
984 * @extends {WebInspector.Object}
985 * @param {!Element} element
986 */
987WebInspector.LongClickController = function(element)
988{
989    this._element = element;
990}
991
992/**
993 * @enum {string}
994 */
995WebInspector.LongClickController.Events = {
996    LongClick: "LongClick",
997    LongPress: "LongPress"
998};
999
1000WebInspector.LongClickController.prototype = {
1001    reset: function()
1002    {
1003        if (this._longClickInterval) {
1004            clearInterval(this._longClickInterval);
1005            delete this._longClickInterval;
1006        }
1007    },
1008
1009    enable: function()
1010    {
1011        if (this._longClickData)
1012            return;
1013        var boundMouseDown = mouseDown.bind(this);
1014        var boundMouseUp = mouseUp.bind(this);
1015        var boundReset = this.reset.bind(this);
1016
1017        this._element.addEventListener("mousedown", boundMouseDown, false);
1018        this._element.addEventListener("mouseout", boundReset, false);
1019        this._element.addEventListener("mouseup", boundMouseUp, false);
1020        this._element.addEventListener("click", boundReset, true);
1021
1022        var longClicks = 0;
1023
1024        this._longClickData = { mouseUp: boundMouseUp, mouseDown: boundMouseDown, reset: boundReset };
1025
1026        /**
1027         * @param {!Event} e
1028         * @this {WebInspector.LongClickController}
1029         */
1030        function mouseDown(e)
1031        {
1032            if (e.which !== 1)
1033                return;
1034            longClicks = 0;
1035            this._longClickInterval = setInterval(longClicked.bind(this, e), 200);
1036        }
1037
1038        /**
1039         * @param {!Event} e
1040         * @this {WebInspector.LongClickController}
1041         */
1042        function mouseUp(e)
1043        {
1044            if (e.which !== 1)
1045                return;
1046            this.reset();
1047        }
1048
1049        /**
1050         * @param {!Event} e
1051         * @this {WebInspector.LongClickController}
1052         */
1053        function longClicked(e)
1054        {
1055            ++longClicks;
1056            this.dispatchEventToListeners(longClicks === 1 ? WebInspector.LongClickController.Events.LongClick : WebInspector.LongClickController.Events.LongPress, e);
1057        }
1058    },
1059
1060    disable: function()
1061    {
1062        if (!this._longClickData)
1063            return;
1064        this._element.removeEventListener("mousedown", this._longClickData.mouseDown, false);
1065        this._element.removeEventListener("mouseout", this._longClickData.reset, false);
1066        this._element.removeEventListener("mouseup", this._longClickData.mouseUp, false);
1067        this._element.addEventListener("click", this._longClickData.reset, true);
1068        delete this._longClickData;
1069    },
1070
1071    __proto__: WebInspector.Object.prototype
1072}
1073
1074;(function() {
1075
1076function windowLoaded()
1077{
1078    window.addEventListener("focus", WebInspector._windowFocused, false);
1079    window.addEventListener("blur", WebInspector._windowBlurred, false);
1080    document.addEventListener("focus", WebInspector._focusChanged, true);
1081    document.addEventListener("blur", WebInspector._documentBlurred, true);
1082    window.removeEventListener("DOMContentLoaded", windowLoaded, false);
1083}
1084
1085window.addEventListener("DOMContentLoaded", windowLoaded, false);
1086
1087})();
1088