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.HelpScreen.isVisible())
160            WebInspector.HelpScreen.focus();
161        else
162            WebInspector.inspectorView.focus();
163        this.element.remove();
164    }
165}
166
167WebInspector.animateStyle = function(animations, duration, callback)
168{
169    var startTime = new Date().getTime();
170    var hasCompleted = false;
171
172    const animationsLength = animations.length;
173    const propertyUnit = {opacity: ""};
174    const defaultUnit = "px";
175
176    // Pre-process animations.
177    for (var i = 0; i < animationsLength; ++i) {
178        var animation = animations[i];
179        var element = null, start = null, end = null, key = null;
180        for (key in animation) {
181            if (key === "element")
182                element = animation[key];
183            else if (key === "start")
184                start = animation[key];
185            else if (key === "end")
186                end = animation[key];
187        }
188
189        if (!element || !end)
190            continue;
191
192        if (!start) {
193            var computedStyle = element.ownerDocument.defaultView.getComputedStyle(element);
194            start = {};
195            for (key in end)
196                start[key] = parseInt(computedStyle.getPropertyValue(key), 10);
197            animation.start = start;
198        } else
199            for (key in start)
200                element.style.setProperty(key, start[key] + (key in propertyUnit ? propertyUnit[key] : defaultUnit));
201    }
202
203    function animateLoop()
204    {
205        if (hasCompleted)
206            return;
207
208        var complete = new Date().getTime() - startTime;
209
210        // Make style changes.
211        for (var i = 0; i < animationsLength; ++i) {
212            var animation = animations[i];
213            var element = animation.element;
214            var start = animation.start;
215            var end = animation.end;
216            if (!element || !end)
217                continue;
218
219            var style = element.style;
220            for (key in end) {
221                var endValue = end[key];
222                if (complete < duration) {
223                    var startValue = start[key];
224                    // Linear animation.
225                    var newValue = startValue + (endValue - startValue) * complete / duration;
226                    style.setProperty(key, newValue + (key in propertyUnit ? propertyUnit[key] : defaultUnit));
227                } else
228                    style.setProperty(key, endValue + (key in propertyUnit ? propertyUnit[key] : defaultUnit));
229            }
230        }
231
232        // End condition.
233        if (complete >= duration)
234            hasCompleted = true;
235        if (callback)
236            callback(hasCompleted);
237        if (!hasCompleted)
238            window.requestAnimationFrame(animateLoop);
239    }
240
241    function forceComplete()
242    {
243        if (hasCompleted)
244            return;
245
246        duration = 0;
247        animateLoop();
248    }
249
250    window.requestAnimationFrame(animateLoop);
251    return {
252        forceComplete: forceComplete
253    };
254}
255
256WebInspector.isBeingEdited = function(element)
257{
258    if (element.classList.contains("text-prompt") || element.nodeName === "INPUT" || element.nodeName === "TEXTAREA")
259        return true;
260
261    if (!WebInspector.__editingCount)
262        return false;
263
264    while (element) {
265        if (element.__editing)
266            return true;
267        element = element.parentElement;
268    }
269    return false;
270}
271
272WebInspector.markBeingEdited = function(element, value)
273{
274    if (value) {
275        if (element.__editing)
276            return false;
277        element.classList.add("being-edited");
278        element.__editing = true;
279        WebInspector.__editingCount = (WebInspector.__editingCount || 0) + 1;
280    } else {
281        if (!element.__editing)
282            return false;
283        element.classList.remove("being-edited");
284        delete element.__editing;
285        --WebInspector.__editingCount;
286    }
287    return true;
288}
289
290/**
291 * @constructor
292 * @param {function(!Element,string,string,T,string)} commitHandler
293 * @param {function(!Element,T)} cancelHandler
294 * @param {T=} context
295 * @template T
296 */
297WebInspector.EditingConfig = function(commitHandler, cancelHandler, context)
298{
299    this.commitHandler = commitHandler;
300    this.cancelHandler = cancelHandler
301    this.context = context;
302
303    /**
304     * Handles the "paste" event, return values are the same as those for customFinishHandler
305     * @type {function(!Element)|undefined}
306     */
307    this.pasteHandler;
308
309    /**
310     * Whether the edited element is multiline
311     * @type {boolean|undefined}
312     */
313    this.multiline;
314
315    /**
316     * Custom finish handler for the editing session (invoked on keydown)
317     * @type {function(!Element,*)|undefined}
318     */
319    this.customFinishHandler;
320}
321
322WebInspector.EditingConfig.prototype = {
323    setPasteHandler: function(pasteHandler)
324    {
325        this.pasteHandler = pasteHandler;
326    },
327
328    /**
329     * @param {string} initialValue
330     * @param {!Object} mode
331     * @param {string} theme
332     * @param {boolean=} lineWrapping
333     * @param {boolean=} smartIndent
334     */
335    setMultilineOptions: function(initialValue, mode, theme, lineWrapping, smartIndent)
336    {
337        this.multiline = true;
338        this.initialValue = initialValue;
339        this.mode = mode;
340        this.theme = theme;
341        this.lineWrapping = lineWrapping;
342        this.smartIndent = smartIndent;
343    },
344
345    setCustomFinishHandler: function(customFinishHandler)
346    {
347        this.customFinishHandler = customFinishHandler;
348    }
349}
350
351WebInspector.CSSNumberRegex = /^(-?(?:\d+(?:\.\d+)?|\.\d+))$/;
352
353WebInspector.StyleValueDelimiters = " \xA0\t\n\"':;,/()";
354
355
356/**
357  * @param {!Event} event
358  * @return {?string}
359  */
360WebInspector._valueModificationDirection = function(event)
361{
362    var direction = null;
363    if (event.type === "mousewheel") {
364        if (event.wheelDeltaY > 0)
365            direction = "Up";
366        else if (event.wheelDeltaY < 0)
367            direction = "Down";
368    } else {
369        if (event.keyIdentifier === "Up" || event.keyIdentifier === "PageUp")
370            direction = "Up";
371        else if (event.keyIdentifier === "Down" || event.keyIdentifier === "PageDown")
372            direction = "Down";
373    }
374    return direction;
375}
376
377/**
378 * @param {string} hexString
379 * @param {!Event} event
380 */
381WebInspector._modifiedHexValue = function(hexString, event)
382{
383    var direction = WebInspector._valueModificationDirection(event);
384    if (!direction)
385        return hexString;
386
387    var number = parseInt(hexString, 16);
388    if (isNaN(number) || !isFinite(number))
389        return hexString;
390
391    var maxValue = Math.pow(16, hexString.length) - 1;
392    var arrowKeyOrMouseWheelEvent = (event.keyIdentifier === "Up" || event.keyIdentifier === "Down" || event.type === "mousewheel");
393    var delta;
394
395    if (arrowKeyOrMouseWheelEvent)
396        delta = (direction === "Up") ? 1 : -1;
397    else
398        delta = (event.keyIdentifier === "PageUp") ? 16 : -16;
399
400    if (event.shiftKey)
401        delta *= 16;
402
403    var result = number + delta;
404    if (result < 0)
405        result = 0; // Color hex values are never negative, so clamp to 0.
406    else if (result > maxValue)
407        return hexString;
408
409    // Ensure the result length is the same as the original hex value.
410    var resultString = result.toString(16).toUpperCase();
411    for (var i = 0, lengthDelta = hexString.length - resultString.length; i < lengthDelta; ++i)
412        resultString = "0" + resultString;
413    return resultString;
414}
415
416/**
417 * @param {number} number
418 * @param {!Event} event
419 */
420WebInspector._modifiedFloatNumber = function(number, event)
421{
422    var direction = WebInspector._valueModificationDirection(event);
423    if (!direction)
424        return number;
425
426    var arrowKeyOrMouseWheelEvent = (event.keyIdentifier === "Up" || event.keyIdentifier === "Down" || event.type === "mousewheel");
427
428    // Jump by 10 when shift is down or jump by 0.1 when Alt/Option is down.
429    // Also jump by 10 for page up and down, or by 100 if shift is held with a page key.
430    var changeAmount = 1;
431    if (event.shiftKey && !arrowKeyOrMouseWheelEvent)
432        changeAmount = 100;
433    else if (event.shiftKey || !arrowKeyOrMouseWheelEvent)
434        changeAmount = 10;
435    else if (event.altKey)
436        changeAmount = 0.1;
437
438    if (direction === "Down")
439        changeAmount *= -1;
440
441    // Make the new number and constrain it to a precision of 6, this matches numbers the engine returns.
442    // Use the Number constructor to forget the fixed precision, so 1.100000 will print as 1.1.
443    var result = Number((number + changeAmount).toFixed(6));
444    if (!String(result).match(WebInspector.CSSNumberRegex))
445        return null;
446
447    return result;
448}
449
450/**
451  * @param {?Event} event
452  * @param {!Element} element
453  * @param {function(string,string)=} finishHandler
454  * @param {function(string)=} suggestionHandler
455  * @param {function(number):number=} customNumberHandler
456  * @return {boolean}
457 */
458WebInspector.handleElementValueModifications = function(event, element, finishHandler, suggestionHandler, customNumberHandler)
459{
460    var arrowKeyOrMouseWheelEvent = (event.keyIdentifier === "Up" || event.keyIdentifier === "Down" || event.type === "mousewheel");
461    var pageKeyPressed = (event.keyIdentifier === "PageUp" || event.keyIdentifier === "PageDown");
462    if (!arrowKeyOrMouseWheelEvent && !pageKeyPressed)
463        return false;
464
465    var selection = window.getSelection();
466    if (!selection.rangeCount)
467        return false;
468
469    var selectionRange = selection.getRangeAt(0);
470    if (!selectionRange.commonAncestorContainer.isSelfOrDescendant(element))
471        return false;
472
473    var originalValue = element.textContent;
474    var wordRange = selectionRange.startContainer.rangeOfWord(selectionRange.startOffset, WebInspector.StyleValueDelimiters, element);
475    var wordString = wordRange.toString();
476
477    if (suggestionHandler && suggestionHandler(wordString))
478        return false;
479
480    var replacementString;
481    var prefix, suffix, number;
482
483    var matches;
484    matches = /(.*#)([\da-fA-F]+)(.*)/.exec(wordString);
485    if (matches && matches.length) {
486        prefix = matches[1];
487        suffix = matches[3];
488        number = WebInspector._modifiedHexValue(matches[2], event);
489
490        if (customNumberHandler)
491            number = customNumberHandler(number);
492
493        replacementString = prefix + number + suffix;
494    } else {
495        matches = /(.*?)(-?(?:\d+(?:\.\d+)?|\.\d+))(.*)/.exec(wordString);
496        if (matches && matches.length) {
497            prefix = matches[1];
498            suffix = matches[3];
499            number = WebInspector._modifiedFloatNumber(parseFloat(matches[2]), event);
500
501            // Need to check for null explicitly.
502            if (number === null)
503                return false;
504
505            if (customNumberHandler)
506                number = customNumberHandler(number);
507
508            replacementString = prefix + number + suffix;
509        }
510    }
511
512    if (replacementString) {
513        var replacementTextNode = document.createTextNode(replacementString);
514
515        wordRange.deleteContents();
516        wordRange.insertNode(replacementTextNode);
517
518        var finalSelectionRange = document.createRange();
519        finalSelectionRange.setStart(replacementTextNode, 0);
520        finalSelectionRange.setEnd(replacementTextNode, replacementString.length);
521
522        selection.removeAllRanges();
523        selection.addRange(finalSelectionRange);
524
525        event.handled = true;
526        event.preventDefault();
527
528        if (finishHandler)
529            finishHandler(originalValue, replacementString);
530
531        return true;
532    }
533    return false;
534}
535
536/**
537 * @param {!Element} element
538 * @param {!WebInspector.EditingConfig=} config
539 * @return {?{cancel: function(), commit: function(), codeMirror: !CodeMirror, setWidth: function(number)}}
540 */
541WebInspector.startEditing = function(element, config)
542{
543    if (!WebInspector.markBeingEdited(element, true))
544        return null;
545
546    config = config || new WebInspector.EditingConfig(function() {}, function() {});
547    var committedCallback = config.commitHandler;
548    var cancelledCallback = config.cancelHandler;
549    var pasteCallback = config.pasteHandler;
550    var context = config.context;
551    var isMultiline = config.multiline || false;
552    var oldText = isMultiline ? config.initialValue : getContent(element);
553    var moveDirection = "";
554    var oldTabIndex;
555    var codeMirror;
556    var cssLoadView;
557
558    /**
559     * @param {?Event} e
560     */
561    function consumeCopy(e)
562    {
563        e.consume();
564    }
565
566    if (isMultiline) {
567        loadScript("CodeMirrorTextEditor.js");
568        cssLoadView = new WebInspector.CodeMirrorCSSLoadView();
569        cssLoadView.show(element);
570        WebInspector.setCurrentFocusElement(element);
571        element.addEventListener("copy", consumeCopy, false);
572        codeMirror = window.CodeMirror(element, {
573            mode: config.mode,
574            lineWrapping: config.lineWrapping,
575            smartIndent: config.smartIndent,
576            autofocus: true,
577            theme: config.theme,
578            value: oldText
579        });
580        codeMirror.getWrapperElement().classList.add("source-code");
581        codeMirror.on("cursorActivity", function(cm) {
582            cm.display.cursor.scrollIntoViewIfNeeded(false);
583        });
584    } else {
585        element.classList.add("editing");
586
587        oldTabIndex = element.getAttribute("tabIndex");
588        if (typeof oldTabIndex !== "number" || oldTabIndex < 0)
589            element.tabIndex = 0;
590        WebInspector.setCurrentFocusElement(element);
591    }
592
593    /**
594     * @param {number} width
595     */
596    function setWidth(width)
597    {
598        const padding = 30;
599        codeMirror.getWrapperElement().style.width = (width - codeMirror.getWrapperElement().offsetLeft - padding) + "px";
600        codeMirror.refresh();
601    }
602
603    /**
604     * @param {?Event=} e
605     */
606    function blurEventListener(e) {
607        if (!isMultiline || !e || !e.relatedTarget || !e.relatedTarget.isSelfOrDescendant(element))
608            editingCommitted.call(element);
609    }
610
611    function getContent(element) {
612        if (isMultiline)
613            return codeMirror.getValue();
614
615        if (element.tagName === "INPUT" && element.type === "text")
616            return element.value;
617
618        return element.textContent;
619    }
620
621    /** @this {Element} */
622    function cleanUpAfterEditing()
623    {
624        WebInspector.markBeingEdited(element, false);
625
626        element.removeEventListener("blur", blurEventListener, isMultiline);
627        element.removeEventListener("keydown", keyDownEventListener, true);
628        if (pasteCallback)
629            element.removeEventListener("paste", pasteEventListener, true);
630
631        WebInspector.restoreFocusFromElement(element);
632
633        if (isMultiline) {
634            element.removeEventListener("copy", consumeCopy, false);
635            cssLoadView.detach();
636            return;
637        }
638
639        this.classList.remove("editing");
640
641        if (typeof oldTabIndex !== "number")
642            element.removeAttribute("tabIndex");
643        else
644            this.tabIndex = oldTabIndex;
645        this.scrollTop = 0;
646        this.scrollLeft = 0;
647    }
648
649    /** @this {Element} */
650    function editingCancelled()
651    {
652        if (isMultiline)
653            codeMirror.setValue(oldText);
654        else {
655            if (this.tagName === "INPUT" && this.type === "text")
656                this.value = oldText;
657            else
658                this.textContent = oldText;
659        }
660
661        cleanUpAfterEditing.call(this);
662
663        cancelledCallback(this, context);
664    }
665
666    /** @this {Element} */
667    function editingCommitted()
668    {
669        cleanUpAfterEditing.call(this);
670
671        committedCallback(this, getContent(this), oldText, context, moveDirection);
672    }
673
674    function defaultFinishHandler(event)
675    {
676        var isMetaOrCtrl = WebInspector.isMac() ?
677            event.metaKey && !event.shiftKey && !event.ctrlKey && !event.altKey :
678            event.ctrlKey && !event.shiftKey && !event.metaKey && !event.altKey;
679        if (isEnterKey(event) && (event.isMetaOrCtrlForTest || !isMultiline || isMetaOrCtrl))
680            return "commit";
681        else if (event.keyCode === WebInspector.KeyboardShortcut.Keys.Esc.code || event.keyIdentifier === "U+001B")
682            return "cancel";
683        else if (!isMultiline && event.keyIdentifier === "U+0009") // Tab key
684            return "move-" + (event.shiftKey ? "backward" : "forward");
685    }
686
687    function handleEditingResult(result, event)
688    {
689        if (result === "commit") {
690            editingCommitted.call(element);
691            event.consume(true);
692        } else if (result === "cancel") {
693            editingCancelled.call(element);
694            event.consume(true);
695        } else if (result && result.startsWith("move-")) {
696            moveDirection = result.substring(5);
697            if (event.keyIdentifier !== "U+0009")
698                blurEventListener();
699        }
700    }
701
702    function pasteEventListener(event)
703    {
704        var result = pasteCallback(event);
705        handleEditingResult(result, event);
706    }
707
708    function keyDownEventListener(event)
709    {
710        var handler = config.customFinishHandler || defaultFinishHandler;
711        var result = handler(event);
712        handleEditingResult(result, event);
713    }
714
715    element.addEventListener("blur", blurEventListener, isMultiline);
716    element.addEventListener("keydown", keyDownEventListener, true);
717    if (pasteCallback)
718        element.addEventListener("paste", pasteEventListener, true);
719
720    return {
721        cancel: editingCancelled.bind(element),
722        commit: editingCommitted.bind(element),
723        codeMirror: codeMirror, // For testing.
724        setWidth: setWidth
725    };
726}
727
728/**
729 * @param {number} seconds
730 * @param {boolean=} higherResolution
731 * @return {string}
732 */
733Number.secondsToString = function(seconds, higherResolution)
734{
735    if (!isFinite(seconds))
736        return "-";
737
738    if (seconds === 0)
739        return "0";
740
741    var ms = seconds * 1000;
742    if (higherResolution && ms < 1000)
743        return WebInspector.UIString("%.3f\u2009ms", ms);
744    else if (ms < 1000)
745        return WebInspector.UIString("%.0f\u2009ms", ms);
746
747    if (seconds < 60)
748        return WebInspector.UIString("%.2f\u2009s", seconds);
749
750    var minutes = seconds / 60;
751    if (minutes < 60)
752        return WebInspector.UIString("%.1f\u2009min", minutes);
753
754    var hours = minutes / 60;
755    if (hours < 24)
756        return WebInspector.UIString("%.1f\u2009hrs", hours);
757
758    var days = hours / 24;
759    return WebInspector.UIString("%.1f\u2009days", days);
760}
761
762/**
763 * @param {number} bytes
764 * @return {string}
765 */
766Number.bytesToString = function(bytes)
767{
768    if (bytes < 1024)
769        return WebInspector.UIString("%.0f\u2009B", bytes);
770
771    var kilobytes = bytes / 1024;
772    if (kilobytes < 100)
773        return WebInspector.UIString("%.1f\u2009KB", kilobytes);
774    if (kilobytes < 1024)
775        return WebInspector.UIString("%.0f\u2009KB", kilobytes);
776
777    var megabytes = kilobytes / 1024;
778    if (megabytes < 100)
779        return WebInspector.UIString("%.1f\u2009MB", megabytes);
780    else
781        return WebInspector.UIString("%.0f\u2009MB", megabytes);
782}
783
784Number.withThousandsSeparator = function(num)
785{
786    var str = num + "";
787    var re = /(\d+)(\d{3})/;
788    while (str.match(re))
789        str = str.replace(re, "$1\u2009$2"); // \u2009 is a thin space.
790    return str;
791}
792
793WebInspector.useLowerCaseMenuTitles = function()
794{
795    return WebInspector.platform() === "windows";
796}
797
798WebInspector.formatLocalized = function(format, substitutions, formatters, initialValue, append)
799{
800    return String.format(WebInspector.UIString(format), substitutions, formatters, initialValue, append);
801}
802
803WebInspector.openLinkExternallyLabel = function()
804{
805    return WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Open link in new tab" : "Open Link in New Tab");
806}
807
808WebInspector.copyLinkAddressLabel = function()
809{
810    return WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Copy link address" : "Copy Link Address");
811}
812
813WebInspector.platform = function()
814{
815    if (!WebInspector._platform)
816        WebInspector._platform = InspectorFrontendHost.platform();
817    return WebInspector._platform;
818}
819
820WebInspector.isMac = function()
821{
822    if (typeof WebInspector._isMac === "undefined")
823        WebInspector._isMac = WebInspector.platform() === "mac";
824
825    return WebInspector._isMac;
826}
827
828WebInspector.isWin = function()
829{
830    if (typeof WebInspector._isWin === "undefined")
831        WebInspector._isWin = WebInspector.platform() === "windows";
832
833    return WebInspector._isWin;
834}
835
836WebInspector.PlatformFlavor = {
837    WindowsVista: "windows-vista",
838    MacTiger: "mac-tiger",
839    MacLeopard: "mac-leopard",
840    MacSnowLeopard: "mac-snowleopard",
841    MacLion: "mac-lion",
842    MacMountainLion: "mac-mountain-lion"
843}
844
845WebInspector.platformFlavor = function()
846{
847    function detectFlavor()
848    {
849        const userAgent = navigator.userAgent;
850
851        if (WebInspector.platform() === "windows") {
852            var match = userAgent.match(/Windows NT (\d+)\.(?:\d+)/);
853            if (match && match[1] >= 6)
854                return WebInspector.PlatformFlavor.WindowsVista;
855            return null;
856        } else if (WebInspector.platform() === "mac") {
857            var match = userAgent.match(/Mac OS X\s*(?:(\d+)_(\d+))?/);
858            if (!match || match[1] != 10)
859                return WebInspector.PlatformFlavor.MacSnowLeopard;
860            switch (Number(match[2])) {
861                case 4:
862                    return WebInspector.PlatformFlavor.MacTiger;
863                case 5:
864                    return WebInspector.PlatformFlavor.MacLeopard;
865                case 6:
866                    return WebInspector.PlatformFlavor.MacSnowLeopard;
867                case 7:
868                    return WebInspector.PlatformFlavor.MacLion;
869                case 8:
870                    return WebInspector.PlatformFlavor.MacMountainLion;
871                default:
872                    return "";
873            }
874        }
875    }
876
877    if (!WebInspector._platformFlavor)
878        WebInspector._platformFlavor = detectFlavor();
879
880    return WebInspector._platformFlavor;
881}
882
883WebInspector.port = function()
884{
885    if (!WebInspector._port)
886        WebInspector._port = InspectorFrontendHost.port();
887
888    return WebInspector._port;
889}
890
891WebInspector.installPortStyles = function()
892{
893    var platform = WebInspector.platform();
894    document.body.classList.add("platform-" + platform);
895    var flavor = WebInspector.platformFlavor();
896    if (flavor)
897        document.body.classList.add("platform-" + flavor);
898    var port = WebInspector.port();
899    document.body.classList.add("port-" + port);
900}
901
902WebInspector._windowFocused = function(event)
903{
904    if (event.target.document.nodeType === Node.DOCUMENT_NODE)
905        document.body.classList.remove("inactive");
906}
907
908WebInspector._windowBlurred = function(event)
909{
910    if (event.target.document.nodeType === Node.DOCUMENT_NODE)
911        document.body.classList.add("inactive");
912}
913
914WebInspector.previousFocusElement = function()
915{
916    return WebInspector._previousFocusElement;
917}
918
919WebInspector.currentFocusElement = function()
920{
921    return WebInspector._currentFocusElement;
922}
923
924WebInspector._focusChanged = function(event)
925{
926    WebInspector.setCurrentFocusElement(event.target);
927}
928
929WebInspector._textInputTypes = ["text", "search", "tel", "url", "email", "password"].keySet();
930WebInspector._isTextEditingElement = function(element)
931{
932    if (element instanceof HTMLInputElement)
933        return element.type in WebInspector._textInputTypes;
934
935    if (element instanceof HTMLTextAreaElement)
936        return true;
937
938    return false;
939}
940
941WebInspector.setCurrentFocusElement = function(x)
942{
943    if (WebInspector._glassPane && x && !WebInspector._glassPane.element.isAncestor(x))
944        return;
945    if (WebInspector._currentFocusElement !== x)
946        WebInspector._previousFocusElement = WebInspector._currentFocusElement;
947    WebInspector._currentFocusElement = x;
948
949    if (WebInspector._currentFocusElement) {
950        WebInspector._currentFocusElement.focus();
951
952        // Make a caret selection inside the new element if there isn't a range selection and there isn't already a caret selection inside.
953        // This is needed (at least) to remove caret from console when focus is moved to some element in the panel.
954        // The code below should not be applied to text fields and text areas, hence _isTextEditingElement check.
955        var selection = window.getSelection();
956        if (!WebInspector._isTextEditingElement(WebInspector._currentFocusElement) && selection.isCollapsed && !WebInspector._currentFocusElement.isInsertionCaretInside()) {
957            var selectionRange = WebInspector._currentFocusElement.ownerDocument.createRange();
958            selectionRange.setStart(WebInspector._currentFocusElement, 0);
959            selectionRange.setEnd(WebInspector._currentFocusElement, 0);
960
961            selection.removeAllRanges();
962            selection.addRange(selectionRange);
963        }
964    } else if (WebInspector._previousFocusElement)
965        WebInspector._previousFocusElement.blur();
966}
967
968WebInspector.restoreFocusFromElement = function(element)
969{
970    if (element && element.isSelfOrAncestor(WebInspector.currentFocusElement()))
971        WebInspector.setCurrentFocusElement(WebInspector.previousFocusElement());
972}
973
974WebInspector.setToolbarColors = function(backgroundColor, color)
975{
976    if (!WebInspector._themeStyleElement) {
977        WebInspector._themeStyleElement = document.createElement("style");
978        document.head.appendChild(WebInspector._themeStyleElement);
979    }
980    var parsedColor = WebInspector.Color.parse(color);
981    var shadowColor = parsedColor ? parsedColor.invert().setAlpha(0.33).toString(WebInspector.Color.Format.RGBA) : "white";
982    var prefix = WebInspector.isMac() ? "body:not(.undocked)" : "";
983    WebInspector._themeStyleElement.textContent =
984        String.sprintf(
985            "%s .toolbar-background {\
986                 background-image: none !important;\
987                 background-color: %s !important;\
988                 color: %s !important;\
989             }", prefix, backgroundColor, color) +
990        String.sprintf(
991             "%s .toolbar-background button.status-bar-item .glyph, %s .toolbar-background button.status-bar-item .long-click-glyph {\
992                 background-color: %s;\
993             }", prefix, prefix, color) +
994        String.sprintf(
995             "%s .toolbar-background button.status-bar-item .glyph.shadow, %s .toolbar-background button.status-bar-item .long-click-glyph.shadow {\
996                 background-color: %s;\
997             }", prefix, prefix, shadowColor);
998}
999
1000WebInspector.resetToolbarColors = function()
1001{
1002    if (WebInspector._themeStyleElement)
1003        WebInspector._themeStyleElement.textContent = "";
1004}
1005
1006/**
1007 * @param {!Element} element
1008 * @param {number} offset
1009 * @param {number} length
1010 * @param {!Array.<!Object>=} domChanges
1011 */
1012WebInspector.highlightSearchResult = function(element, offset, length, domChanges)
1013{
1014    var result = WebInspector.highlightSearchResults(element, [new WebInspector.SourceRange(offset, length)], domChanges);
1015    return result.length ? result[0] : null;
1016}
1017
1018/**
1019 * @param {!Element} element
1020 * @param {!Array.<!WebInspector.SourceRange>} resultRanges
1021 * @param {!Array.<!Object>=} changes
1022 */
1023WebInspector.highlightSearchResults = function(element, resultRanges, changes)
1024{
1025    return WebInspector.highlightRangesWithStyleClass(element, resultRanges, "highlighted-search-result", changes);
1026}
1027
1028/**
1029 * @param {!Element} element
1030 * @param {!Array.<!WebInspector.SourceRange>} resultRanges
1031 * @param {string} styleClass
1032 * @param {!Array.<!Object>=} changes
1033 */
1034WebInspector.highlightRangesWithStyleClass = function(element, resultRanges, styleClass, changes)
1035{
1036    changes = changes || [];
1037    var highlightNodes = [];
1038    var lineText = element.textContent;
1039    var ownerDocument = element.ownerDocument;
1040    var textNodeSnapshot = ownerDocument.evaluate(".//text()", element, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
1041
1042    var snapshotLength = textNodeSnapshot.snapshotLength;
1043    if (snapshotLength === 0)
1044        return highlightNodes;
1045
1046    var nodeRanges = [];
1047    var rangeEndOffset = 0;
1048    for (var i = 0; i < snapshotLength; ++i) {
1049        var range = {};
1050        range.offset = rangeEndOffset;
1051        range.length = textNodeSnapshot.snapshotItem(i).textContent.length;
1052        rangeEndOffset = range.offset + range.length;
1053        nodeRanges.push(range);
1054    }
1055
1056    var startIndex = 0;
1057    for (var i = 0; i < resultRanges.length; ++i) {
1058        var startOffset = resultRanges[i].offset;
1059        var endOffset = startOffset + resultRanges[i].length;
1060
1061        while (startIndex < snapshotLength && nodeRanges[startIndex].offset + nodeRanges[startIndex].length <= startOffset)
1062            startIndex++;
1063        var endIndex = startIndex;
1064        while (endIndex < snapshotLength && nodeRanges[endIndex].offset + nodeRanges[endIndex].length < endOffset)
1065            endIndex++;
1066        if (endIndex === snapshotLength)
1067            break;
1068
1069        var highlightNode = ownerDocument.createElement("span");
1070        highlightNode.className = styleClass;
1071        highlightNode.textContent = lineText.substring(startOffset, endOffset);
1072
1073        var lastTextNode = textNodeSnapshot.snapshotItem(endIndex);
1074        var lastText = lastTextNode.textContent;
1075        lastTextNode.textContent = lastText.substring(endOffset - nodeRanges[endIndex].offset);
1076        changes.push({ node: lastTextNode, type: "changed", oldText: lastText, newText: lastTextNode.textContent });
1077
1078        if (startIndex === endIndex) {
1079            lastTextNode.parentElement.insertBefore(highlightNode, lastTextNode);
1080            changes.push({ node: highlightNode, type: "added", nextSibling: lastTextNode, parent: lastTextNode.parentElement });
1081            highlightNodes.push(highlightNode);
1082
1083            var prefixNode = ownerDocument.createTextNode(lastText.substring(0, startOffset - nodeRanges[startIndex].offset));
1084            lastTextNode.parentElement.insertBefore(prefixNode, highlightNode);
1085            changes.push({ node: prefixNode, type: "added", nextSibling: highlightNode, parent: lastTextNode.parentElement });
1086        } else {
1087            var firstTextNode = textNodeSnapshot.snapshotItem(startIndex);
1088            var firstText = firstTextNode.textContent;
1089            var anchorElement = firstTextNode.nextSibling;
1090
1091            firstTextNode.parentElement.insertBefore(highlightNode, anchorElement);
1092            changes.push({ node: highlightNode, type: "added", nextSibling: anchorElement, parent: firstTextNode.parentElement });
1093            highlightNodes.push(highlightNode);
1094
1095            firstTextNode.textContent = firstText.substring(0, startOffset - nodeRanges[startIndex].offset);
1096            changes.push({ node: firstTextNode, type: "changed", oldText: firstText, newText: firstTextNode.textContent });
1097
1098            for (var j = startIndex + 1; j < endIndex; j++) {
1099                var textNode = textNodeSnapshot.snapshotItem(j);
1100                var text = textNode.textContent;
1101                textNode.textContent = "";
1102                changes.push({ node: textNode, type: "changed", oldText: text, newText: textNode.textContent });
1103            }
1104        }
1105        startIndex = endIndex;
1106        nodeRanges[startIndex].offset = endOffset;
1107        nodeRanges[startIndex].length = lastTextNode.textContent.length;
1108
1109    }
1110    return highlightNodes;
1111}
1112
1113WebInspector.applyDomChanges = function(domChanges)
1114{
1115    for (var i = 0, size = domChanges.length; i < size; ++i) {
1116        var entry = domChanges[i];
1117        switch (entry.type) {
1118        case "added":
1119            entry.parent.insertBefore(entry.node, entry.nextSibling);
1120            break;
1121        case "changed":
1122            entry.node.textContent = entry.newText;
1123            break;
1124        }
1125    }
1126}
1127
1128WebInspector.revertDomChanges = function(domChanges)
1129{
1130    for (var i = domChanges.length - 1; i >= 0; --i) {
1131        var entry = domChanges[i];
1132        switch (entry.type) {
1133        case "added":
1134            entry.node.remove();
1135            break;
1136        case "changed":
1137            entry.node.textContent = entry.oldText;
1138            break;
1139        }
1140    }
1141}
1142
1143WebInspector._coalescingLevel = 0;
1144
1145WebInspector.startBatchUpdate = function()
1146{
1147    if (!WebInspector._coalescingLevel)
1148        WebInspector._postUpdateHandlers = new Map();
1149    WebInspector._coalescingLevel++;
1150}
1151
1152WebInspector.endBatchUpdate = function()
1153{
1154    if (--WebInspector._coalescingLevel)
1155        return;
1156
1157    var handlers = WebInspector._postUpdateHandlers;
1158    delete WebInspector._postUpdateHandlers;
1159
1160    var keys = handlers.keys();
1161    for (var i = 0; i < keys.length; ++i) {
1162        var object = keys[i];
1163        var methods = handlers.get(object).keys();
1164        for (var j = 0; j < methods.length; ++j)
1165            methods[j].call(object);
1166    }
1167}
1168
1169/**
1170 * @param {!Object} object
1171 * @param {function()} method
1172 */
1173WebInspector.invokeOnceAfterBatchUpdate = function(object, method)
1174{
1175    if (!WebInspector._coalescingLevel) {
1176        method.call(object);
1177        return;
1178    }
1179
1180    var methods = WebInspector._postUpdateHandlers.get(object);
1181    if (!methods) {
1182        methods = new Map();
1183        WebInspector._postUpdateHandlers.put(object, methods);
1184    }
1185    methods.put(method);
1186}
1187
1188/**
1189 * This bogus view is needed to load/unload CodeMirror-related CSS on demand.
1190 *
1191 * @constructor
1192 * @extends {WebInspector.View}
1193 */
1194WebInspector.CodeMirrorCSSLoadView = function()
1195{
1196    WebInspector.View.call(this);
1197    this.element.classList.add("hidden");
1198    this.registerRequiredCSS("cm/codemirror.css");
1199    this.registerRequiredCSS("cm/cmdevtools.css");
1200}
1201
1202WebInspector.CodeMirrorCSSLoadView.prototype = {
1203    __proto__: WebInspector.View.prototype
1204}
1205
1206;(function() {
1207
1208/**
1209 * @this {Window}
1210 */
1211function windowLoaded()
1212{
1213    window.addEventListener("focus", WebInspector._windowFocused, false);
1214    window.addEventListener("blur", WebInspector._windowBlurred, false);
1215    document.addEventListener("focus", WebInspector._focusChanged.bind(this), true);
1216    window.removeEventListener("DOMContentLoaded", windowLoaded, false);
1217}
1218
1219window.addEventListener("DOMContentLoaded", windowLoaded, false);
1220
1221})();
1222