0054d442b17387f26d9fa710122e91531db783be
[WebKit-https.git] / Source / WebCore / inspector / front-end / UIUtils.js
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(Event): boolean} elementDragStart
35  * @param {function(Event)} elementDrag
36  * @param {?function(Event)} elementDragEnd
37  * @param {string} cursor
38  */
39 WebInspector.installDragHandle = function(element, elementDragStart, elementDrag, elementDragEnd, cursor)
40 {
41     element.addEventListener("mousedown", WebInspector._elementDragStart.bind(WebInspector, elementDragStart, elementDrag, elementDragEnd, cursor), false);
42 }
43
44 /**
45  * @param {?function(Event)} elementDragStart
46  * @param {function(Event)} elementDrag
47  * @param {?function(Event)} elementDragEnd
48  * @param {string} cursor
49  * @param {Event} event
50  */
51 WebInspector._elementDragStart = function(elementDragStart, elementDrag, elementDragEnd, cursor, event)
52 {
53     // Only drag upon left button. Right will likely cause a context menu. So will ctrl-click on mac.
54     if (event.button || (WebInspector.isMac() && event.ctrlKey))
55         return;
56
57     if (WebInspector._elementDraggingEventListener)
58         return;
59
60     if (elementDragStart && !elementDragStart(event))
61         return;
62
63     if (WebInspector._elementDraggingGlassPane) {
64         WebInspector._elementDraggingGlassPane.dispose();
65         delete WebInspector._elementDraggingGlassPane;
66     }
67
68     var targetDocument = event.target.ownerDocument;
69
70     WebInspector._elementDraggingEventListener = elementDrag;
71     WebInspector._elementEndDraggingEventListener = elementDragEnd;
72     WebInspector._mouseOutWhileDraggingTargetDocument = targetDocument;
73
74     targetDocument.addEventListener("mousemove", WebInspector._elementDraggingEventListener, true);
75     targetDocument.addEventListener("mouseup", WebInspector._elementDragEnd, true);
76     targetDocument.addEventListener("mouseout", WebInspector._mouseOutWhileDragging, true);
77
78     targetDocument.body.style.cursor = cursor;
79
80     event.preventDefault();
81 }
82
83 WebInspector._mouseOutWhileDragging = function()
84 {
85     WebInspector._unregisterMouseOutWhileDragging();
86     WebInspector._elementDraggingGlassPane = new WebInspector.GlassPane();
87 }
88
89 WebInspector._unregisterMouseOutWhileDragging = function()
90 {
91     if (!WebInspector._mouseOutWhileDraggingTargetDocument)
92         return;
93     WebInspector._mouseOutWhileDraggingTargetDocument.removeEventListener("mouseout", WebInspector._mouseOutWhileDragging, true);
94     delete WebInspector._mouseOutWhileDraggingTargetDocument;
95 }
96
97 WebInspector._elementDragEnd = function(event)
98 {
99     var targetDocument = event.target.ownerDocument;
100     targetDocument.removeEventListener("mousemove", WebInspector._elementDraggingEventListener, true);
101     targetDocument.removeEventListener("mouseup", WebInspector._elementDragEnd, true);
102     WebInspector._unregisterMouseOutWhileDragging();
103
104     targetDocument.body.style.removeProperty("cursor");
105
106     if (WebInspector._elementDraggingGlassPane)
107         WebInspector._elementDraggingGlassPane.dispose();
108
109     var elementDragEnd = WebInspector._elementEndDraggingEventListener;
110
111     delete WebInspector._elementDraggingGlassPane;
112     delete WebInspector._elementDraggingEventListener;
113     delete WebInspector._elementEndDraggingEventListener;
114
115     event.preventDefault();
116     if (elementDragEnd)
117         elementDragEnd(event);
118 }
119
120 /**
121  * @constructor
122  */
123 WebInspector.GlassPane = function()
124 {
125     this.element = document.createElement("div");
126     this.element.style.cssText = "position:absolute;top:0;bottom:0;left:0;right:0;background-color:transparent;z-index:1000;";
127     this.element.id = "glass-pane-for-drag";
128     document.body.appendChild(this.element);
129 }
130
131 WebInspector.GlassPane.prototype = {
132     dispose: function()
133     {
134         if (this.element.parentElement)
135             this.element.parentElement.removeChild(this.element);
136     }
137 }
138
139 WebInspector.animateStyle = function(animations, duration, callback)
140 {
141     var interval;
142     var complete = 0;
143     var hasCompleted = false;
144
145     const intervalDuration = (1000 / 30); // 30 frames per second.
146     const animationsLength = animations.length;
147     const propertyUnit = {opacity: ""};
148     const defaultUnit = "px";
149
150     function cubicInOut(t, b, c, d)
151     {
152         if ((t/=d/2) < 1) return c/2*t*t*t + b;
153         return c/2*((t-=2)*t*t + 2) + b;
154     }
155
156     // Pre-process animations.
157     for (var i = 0; i < animationsLength; ++i) {
158         var animation = animations[i];
159         var element = null, start = null, end = null, key = null;
160         for (key in animation) {
161             if (key === "element")
162                 element = animation[key];
163             else if (key === "start")
164                 start = animation[key];
165             else if (key === "end")
166                 end = animation[key];
167         }
168
169         if (!element || !end)
170             continue;
171
172         if (!start) {
173             var computedStyle = element.ownerDocument.defaultView.getComputedStyle(element);
174             start = {};
175             for (key in end)
176                 start[key] = parseInt(computedStyle.getPropertyValue(key), 10);
177             animation.start = start;
178         } else
179             for (key in start)
180                 element.style.setProperty(key, start[key] + (key in propertyUnit ? propertyUnit[key] : defaultUnit));
181     }
182
183     function animateLoop()
184     {
185         if (hasCompleted)
186             return;
187         
188         // Advance forward.
189         complete += intervalDuration;
190         var next = complete + intervalDuration;
191
192         // Make style changes.
193         for (var i = 0; i < animationsLength; ++i) {
194             var animation = animations[i];
195             var element = animation.element;
196             var start = animation.start;
197             var end = animation.end;
198             if (!element || !end)
199                 continue;
200
201             var style = element.style;
202             for (key in end) {
203                 var endValue = end[key];
204                 if (next < duration) {
205                     var startValue = start[key];
206                     var newValue = cubicInOut(complete, startValue, endValue - startValue, duration);
207                     style.setProperty(key, newValue + (key in propertyUnit ? propertyUnit[key] : defaultUnit));
208                 } else
209                     style.setProperty(key, endValue + (key in propertyUnit ? propertyUnit[key] : defaultUnit));
210             }
211         }
212
213         // End condition.
214         if (complete >= duration) {
215             hasCompleted = true;
216             clearInterval(interval);
217             if (callback)
218                 callback();
219         }
220     }
221
222     function forceComplete()
223     {
224         if (hasCompleted)
225             return;
226
227         complete = duration;
228         animateLoop();
229     }
230
231     function cancel()
232     {
233         hasCompleted = true;
234         clearInterval(interval);
235     }
236
237     interval = setInterval(animateLoop, intervalDuration);
238     return {
239         cancel: cancel,
240         forceComplete: forceComplete
241     };
242 }
243
244 WebInspector.isBeingEdited = function(element)
245 {
246     if (element.hasStyleClass("text-prompt") || element.nodeName === "INPUT")
247         return true;
248
249     if (!WebInspector.__editingCount)
250         return false;
251
252     while (element) {
253         if (element.__editing)
254             return true;
255         element = element.parentElement;
256     }
257     return false;
258 }
259
260 WebInspector.markBeingEdited = function(element, value)
261 {
262     if (value) {
263         if (element.__editing)
264             return false;
265         element.__editing = true;
266         WebInspector.__editingCount = (WebInspector.__editingCount || 0) + 1;
267     } else {
268         if (!element.__editing)
269             return false;
270         delete element.__editing;
271         --WebInspector.__editingCount;
272     }
273     return true;
274 }
275
276 /**
277  * @constructor
278  * @param {function(Element,string,string,*,string)} commitHandler
279  * @param {function(Element,*)} cancelHandler
280  * @param {*=} context
281  */
282 WebInspector.EditingConfig = function(commitHandler, cancelHandler, context)
283 {
284     this.commitHandler = commitHandler;
285     this.cancelHandler = cancelHandler
286     this.context = context;
287
288     /**
289      * Handles the "paste" event, return values are the same as those for customFinishHandler
290      * @type {function(Element)|undefined}
291      */
292     this.pasteHandler;
293
294     /** 
295      * Whether the edited element is multiline
296      * @type {boolean|undefined}
297      */
298     this.multiline;
299
300     /**
301      * Custom finish handler for the editing session (invoked on keydown)
302      * @type {function(Element,*)|undefined}
303      */
304     this.customFinishHandler;
305 }
306
307 WebInspector.EditingConfig.prototype = {
308     setPasteHandler: function(pasteHandler)
309     {
310         this.pasteHandler = pasteHandler;
311     },
312
313     setMultiline: function(multiline)
314     {
315         this.multiline = multiline;
316     },
317
318     setCustomFinishHandler: function(customFinishHandler)
319     {
320         this.customFinishHandler = customFinishHandler;
321     }
322 }
323
324 WebInspector.CSSNumberRegex = /^(-?(?:\d+(?:\.\d+)?|\.\d+))$/;
325
326 WebInspector.StyleValueDelimiters = " \xA0\t\n\"':;,/()";
327
328
329 /**
330   * @param {Event} event
331   * @return {?string}
332   */
333 WebInspector._valueModificationDirection = function(event)
334 {
335     var direction = null;
336     if (event.type === "mousewheel") {
337         if (event.wheelDeltaY > 0)
338             direction = "Up";
339         else if (event.wheelDeltaY < 0)
340             direction = "Down";
341     } else {
342         if (event.keyIdentifier === "Up" || event.keyIdentifier === "PageUp")
343             direction = "Up";
344         else if (event.keyIdentifier === "Down" || event.keyIdentifier === "PageDown")
345             direction = "Down";        
346     }
347     return direction;
348 }
349
350 /**
351  * @param {string} hexString
352  * @param {Event} event
353  */
354 WebInspector._modifiedHexValue = function(hexString, event)
355 {
356     var direction = WebInspector._valueModificationDirection(event);
357     if (!direction)
358         return hexString;
359
360     var number = parseInt(hexString, 16);
361     if (isNaN(number) || !isFinite(number))
362         return hexString;
363
364     var maxValue = Math.pow(16, hexString.length) - 1;
365     var arrowKeyOrMouseWheelEvent = (event.keyIdentifier === "Up" || event.keyIdentifier === "Down" || event.type === "mousewheel");
366     var delta;
367
368     if (arrowKeyOrMouseWheelEvent)
369         delta = (direction === "Up") ? 1 : -1;
370     else
371         delta = (event.keyIdentifier === "PageUp") ? 16 : -16;
372
373     if (event.shiftKey)
374         delta *= 16;
375
376     var result = number + delta;
377     if (result < 0)
378         result = 0; // Color hex values are never negative, so clamp to 0.
379     else if (result > maxValue)
380         return hexString;
381
382     // Ensure the result length is the same as the original hex value.
383     var resultString = result.toString(16).toUpperCase();
384     for (var i = 0, lengthDelta = hexString.length - resultString.length; i < lengthDelta; ++i)
385         resultString = "0" + resultString;
386     return resultString;
387 }
388
389 /**
390  * @param {number} number
391  * @param {Event} event
392  */
393 WebInspector._modifiedFloatNumber = function(number, event)
394 {
395     var direction = WebInspector._valueModificationDirection(event);
396     if (!direction)
397         return number;
398     
399     var arrowKeyOrMouseWheelEvent = (event.keyIdentifier === "Up" || event.keyIdentifier === "Down" || event.type === "mousewheel");
400
401     // Jump by 10 when shift is down or jump by 0.1 when Alt/Option is down.
402     // Also jump by 10 for page up and down, or by 100 if shift is held with a page key.
403     var changeAmount = 1;
404     if (event.shiftKey && !arrowKeyOrMouseWheelEvent)
405         changeAmount = 100;
406     else if (event.shiftKey || !arrowKeyOrMouseWheelEvent)
407         changeAmount = 10;
408     else if (event.altKey)
409         changeAmount = 0.1;
410
411     if (direction === "Down")
412         changeAmount *= -1;
413
414     // Make the new number and constrain it to a precision of 6, this matches numbers the engine returns.
415     // Use the Number constructor to forget the fixed precision, so 1.100000 will print as 1.1.
416     var result = Number((number + changeAmount).toFixed(6));
417     if (!String(result).match(WebInspector.CSSNumberRegex))
418         return null;
419
420     return result;
421 }
422
423 /**
424   * @param {Event} event
425   * @param {Element} element
426   * @param {function(string,string)=} finishHandler
427   * @param {function(string)=} suggestionHandler
428   * @param {function(number):number=} customNumberHandler
429  */
430 WebInspector.handleElementValueModifications = function(event, element, finishHandler, suggestionHandler, customNumberHandler)
431 {
432     var arrowKeyOrMouseWheelEvent = (event.keyIdentifier === "Up" || event.keyIdentifier === "Down" || event.type === "mousewheel");
433     var pageKeyPressed = (event.keyIdentifier === "PageUp" || event.keyIdentifier === "PageDown");
434     if (!arrowKeyOrMouseWheelEvent && !pageKeyPressed)
435         return false;
436
437     var selection = window.getSelection();
438     if (!selection.rangeCount)
439         return false;
440
441     var selectionRange = selection.getRangeAt(0);
442     if (!selectionRange.commonAncestorContainer.isSelfOrDescendant(element))
443         return false;
444
445     var originalValue = element.textContent;
446     var wordRange = selectionRange.startContainer.rangeOfWord(selectionRange.startOffset, WebInspector.StyleValueDelimiters, element);
447     var wordString = wordRange.toString();
448     
449     if (suggestionHandler && suggestionHandler(wordString))
450         return false;
451
452     var replacementString;
453     var prefix, suffix, number;
454
455     var matches;
456     matches = /(.*#)([\da-fA-F]+)(.*)/.exec(wordString);
457     if (matches && matches.length) {
458         prefix = matches[1];
459         suffix = matches[3];
460         number = WebInspector._modifiedHexValue(matches[2], event);
461         
462         if (customNumberHandler)
463             number = customNumberHandler(number);
464
465         replacementString = prefix + number + suffix;
466     } else {
467         matches = /(.*?)(-?(?:\d+(?:\.\d+)?|\.\d+))(.*)/.exec(wordString);
468         if (matches && matches.length) {
469             prefix = matches[1];
470             suffix = matches[3];
471             number = WebInspector._modifiedFloatNumber(parseFloat(matches[2]), event);
472             
473             // Need to check for null explicitly.
474             if (number === null)                
475                 return false;
476             
477             if (customNumberHandler)
478                 number = customNumberHandler(number);
479
480             replacementString = prefix + number + suffix;
481         }
482     }
483
484     if (replacementString) {
485         var replacementTextNode = document.createTextNode(replacementString);
486
487         wordRange.deleteContents();
488         wordRange.insertNode(replacementTextNode);
489
490         var finalSelectionRange = document.createRange();
491         finalSelectionRange.setStart(replacementTextNode, 0);
492         finalSelectionRange.setEnd(replacementTextNode, replacementString.length);
493
494         selection.removeAllRanges();
495         selection.addRange(finalSelectionRange);
496
497         event.handled = true;
498         event.preventDefault();
499                 
500         if (finishHandler)
501             finishHandler(originalValue, replacementString);
502
503         return true;
504     }
505     return false;
506 }
507
508 /** 
509  * @param {Element} element
510  * @param {WebInspector.EditingConfig=} config
511  */
512 WebInspector.startEditing = function(element, config)
513 {
514     if (!WebInspector.markBeingEdited(element, true))
515         return null;
516
517     config = config || new WebInspector.EditingConfig(function() {}, function() {});
518     var committedCallback = config.commitHandler;
519     var cancelledCallback = config.cancelHandler;
520     var pasteCallback = config.pasteHandler;
521     var context = config.context;
522     var oldText = getContent(element);
523     var moveDirection = "";
524
525     element.addStyleClass("editing");
526
527     var oldTabIndex = element.getAttribute("tabIndex");
528     if (typeof oldTabIndex !== "number" || oldTabIndex < 0)
529         element.tabIndex = 0;
530
531     function blurEventListener() {
532         editingCommitted.call(element);
533     }
534
535     function getContent(element) {
536         if (element.tagName === "INPUT" && element.type === "text")
537             return element.value;
538         else
539             return element.textContent;
540     }
541
542     /** @this {Element} */
543     function cleanUpAfterEditing()
544     {
545         WebInspector.markBeingEdited(element, false);
546
547         this.removeStyleClass("editing");
548         
549         if (typeof oldTabIndex !== "number")
550             element.removeAttribute("tabIndex");
551         else
552             this.tabIndex = oldTabIndex;
553         this.scrollTop = 0;
554         this.scrollLeft = 0;
555
556         element.removeEventListener("blur", blurEventListener, false);
557         element.removeEventListener("keydown", keyDownEventListener, true);
558         if (pasteCallback)
559             element.removeEventListener("paste", pasteEventListener, true);
560
561         WebInspector.restoreFocusFromElement(element);
562     }
563
564     /** @this {Element} */
565     function editingCancelled()
566     {
567         if (this.tagName === "INPUT" && this.type === "text")
568             this.value = oldText;
569         else
570             this.textContent = oldText;
571
572         cleanUpAfterEditing.call(this);
573
574         cancelledCallback(this, context);
575     }
576
577     /** @this {Element} */
578     function editingCommitted()
579     {
580         cleanUpAfterEditing.call(this);
581
582         committedCallback(this, getContent(this), oldText, context, moveDirection);
583     }
584
585     function defaultFinishHandler(event)
586     {
587         var isMetaOrCtrl = WebInspector.isMac() ?
588             event.metaKey && !event.shiftKey && !event.ctrlKey && !event.altKey :
589             event.ctrlKey && !event.shiftKey && !event.metaKey && !event.altKey;
590         if (isEnterKey(event) && (event.isMetaOrCtrlForTest || !config.multiline || isMetaOrCtrl))
591             return "commit";
592         else if (event.keyCode === WebInspector.KeyboardShortcut.Keys.Esc.code || event.keyIdentifier === "U+001B")
593             return "cancel";
594         else if (event.keyIdentifier === "U+0009") // Tab key
595             return "move-" + (event.shiftKey ? "backward" : "forward");
596     }
597
598     function handleEditingResult(result, event)
599     {
600         if (result === "commit") {
601             editingCommitted.call(element);
602             event.consume(true);
603         } else if (result === "cancel") {
604             editingCancelled.call(element);
605             event.consume(true);
606         } else if (result && result.startsWith("move-")) {
607             moveDirection = result.substring(5);
608             if (event.keyIdentifier !== "U+0009")
609                 blurEventListener();
610         }
611     }
612
613     function pasteEventListener(event)
614     {
615         var result = pasteCallback(event);
616         handleEditingResult(result, event);
617     }
618
619     function keyDownEventListener(event)
620     {
621         var handler = config.customFinishHandler || defaultFinishHandler;
622         var result = handler(event);
623         handleEditingResult(result, event);
624     }
625
626     element.addEventListener("blur", blurEventListener, false);
627     element.addEventListener("keydown", keyDownEventListener, true);
628     if (pasteCallback)
629         element.addEventListener("paste", pasteEventListener, true);
630
631     WebInspector.setCurrentFocusElement(element);
632     return {
633         cancel: editingCancelled.bind(element),
634         commit: editingCommitted.bind(element)
635     };
636 }
637
638 /**
639  * @param {number} seconds
640  * @param {boolean=} higherResolution
641  * @return {string}
642  */
643 Number.secondsToString = function(seconds, higherResolution)
644 {
645     if (!isFinite(seconds))
646         return "-";
647
648     if (seconds === 0)
649         return "0";
650
651     var ms = seconds * 1000;
652     if (higherResolution && ms < 1000)
653         return WebInspector.UIString("%.3f\u2009ms", ms);
654     else if (ms < 1000)
655         return WebInspector.UIString("%.0f\u2009ms", ms);
656
657     if (seconds < 60)
658         return WebInspector.UIString("%.2f\u2009s", seconds);
659
660     var minutes = seconds / 60;
661     if (minutes < 60)
662         return WebInspector.UIString("%.1f\u2009min", minutes);
663
664     var hours = minutes / 60;
665     if (hours < 24)
666         return WebInspector.UIString("%.1f\u2009hrs", hours);
667
668     var days = hours / 24;
669     return WebInspector.UIString("%.1f\u2009days", days);
670 }
671
672 /**
673  * @param {number} bytes
674  * @return {string}
675  */
676 Number.bytesToString = function(bytes)
677 {
678     if (bytes < 1024)
679         return WebInspector.UIString("%.0f\u2009B", bytes);
680
681     var kilobytes = bytes / 1024;
682     if (kilobytes < 100)
683         return WebInspector.UIString("%.1f\u2009KB", kilobytes);
684     if (kilobytes < 1024)
685         return WebInspector.UIString("%.0f\u2009KB", kilobytes);
686
687     var megabytes = kilobytes / 1024;
688     if (megabytes < 100)
689         return WebInspector.UIString("%.1f\u2009MB", megabytes);
690     else
691         return WebInspector.UIString("%.0f\u2009MB", megabytes);
692 }
693
694 Number.withThousandsSeparator = function(num)
695 {
696     var str = num + "";
697     var re = /(\d+)(\d{3})/;
698     while (str.match(re))
699         str = str.replace(re, "$1\u2009$2"); // \u2009 is a thin space.
700     return str;
701 }
702
703 WebInspector.useLowerCaseMenuTitles = function()
704 {
705     return WebInspector.platform() === "windows" && Preferences.useLowerCaseMenuTitlesOnWindows;
706 }
707
708 WebInspector.formatLocalized = function(format, substitutions, formatters, initialValue, append)
709 {
710     return String.format(WebInspector.UIString(format), substitutions, formatters, initialValue, append);
711 }
712
713 WebInspector.openLinkExternallyLabel = function()
714 {
715     return WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Open link in new tab" : "Open Link in New Tab");
716 }
717
718 WebInspector.copyLinkAddressLabel = function()
719 {
720     return WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Copy link address" : "Copy Link Address");
721 }
722
723 WebInspector.platform = function()
724 {
725     if (!WebInspector._platform)
726         WebInspector._platform = InspectorFrontendHost.platform();
727     return WebInspector._platform;
728 }
729
730 WebInspector.isMac = function()
731 {
732     if (typeof WebInspector._isMac === "undefined")
733         WebInspector._isMac = WebInspector.platform() === "mac";
734
735     return WebInspector._isMac;
736 }
737
738 WebInspector.isWin = function()
739 {
740     if (typeof WebInspector._isWin === "undefined")
741         WebInspector._isWin = WebInspector.platform() === "windows";
742
743     return WebInspector._isWin;
744 }
745
746 WebInspector.PlatformFlavor = {
747     WindowsVista: "windows-vista",
748     MacTiger: "mac-tiger",
749     MacLeopard: "mac-leopard",
750     MacSnowLeopard: "mac-snowleopard",
751     MacLion: "mac-lion",
752     MacMountainLion: "mac-mountain-lion"
753 }
754
755 WebInspector.platformFlavor = function()
756 {
757     function detectFlavor()
758     {
759         const userAgent = navigator.userAgent;
760
761         if (WebInspector.platform() === "windows") {
762             var match = userAgent.match(/Windows NT (\d+)\.(?:\d+)/);
763             if (match && match[1] >= 6)
764                 return WebInspector.PlatformFlavor.WindowsVista;
765             return null;
766         } else if (WebInspector.platform() === "mac") {
767             var match = userAgent.match(/Mac OS X\s*(?:(\d+)_(\d+))?/);
768             if (!match || match[1] != 10)
769                 return WebInspector.PlatformFlavor.MacSnowLeopard;
770             switch (Number(match[2])) {
771                 case 4:
772                     return WebInspector.PlatformFlavor.MacTiger;
773                 case 5:
774                     return WebInspector.PlatformFlavor.MacLeopard;
775                 case 6:
776                     return WebInspector.PlatformFlavor.MacSnowLeopard;
777                 case 7:
778                     return WebInspector.PlatformFlavor.MacLion;
779                 case 8:
780                     return WebInspector.PlatformFlavor.MacMountainLion;
781                 default:
782                     return "";
783             }
784         }
785     }
786
787     if (!WebInspector._platformFlavor)
788         WebInspector._platformFlavor = detectFlavor();
789
790     return WebInspector._platformFlavor;
791 }
792
793 WebInspector.port = function()
794 {
795     if (!WebInspector._port)
796         WebInspector._port = InspectorFrontendHost.port();
797
798     return WebInspector._port;
799 }
800
801 WebInspector.installPortStyles = function()
802 {
803     var platform = WebInspector.platform();
804     document.body.addStyleClass("platform-" + platform);
805     var flavor = WebInspector.platformFlavor();
806     if (flavor)
807         document.body.addStyleClass("platform-" + flavor);
808     var port = WebInspector.port();
809     document.body.addStyleClass("port-" + port);
810 }
811
812 WebInspector._windowFocused = function(event)
813 {
814     if (event.target.document.nodeType === Node.DOCUMENT_NODE)
815         document.body.removeStyleClass("inactive");
816 }
817
818 WebInspector._windowBlurred = function(event)
819 {
820     if (event.target.document.nodeType === Node.DOCUMENT_NODE)
821         document.body.addStyleClass("inactive");
822 }
823
824 WebInspector.previousFocusElement = function()
825 {
826     return WebInspector._previousFocusElement;
827 }
828
829 WebInspector.currentFocusElement = function()
830 {
831     return WebInspector._currentFocusElement;
832 }
833
834 WebInspector._focusChanged = function(event)
835 {
836     WebInspector.setCurrentFocusElement(event.target);
837 }
838
839 WebInspector._textInputTypes = ["text", "search", "tel", "url", "email", "password"].keySet(); 
840 WebInspector._isTextEditingElement = function(element)
841 {
842     if (element instanceof HTMLInputElement)
843         return element.type in WebInspector._textInputTypes;
844
845     if (element instanceof HTMLTextAreaElement)
846         return true;
847
848     return false;
849 }
850
851 WebInspector.setCurrentFocusElement = function(x)
852 {
853     if (WebInspector._currentFocusElement !== x)
854         WebInspector._previousFocusElement = WebInspector._currentFocusElement;
855     WebInspector._currentFocusElement = x;
856
857     if (WebInspector._currentFocusElement) {
858         WebInspector._currentFocusElement.focus();
859
860         // Make a caret selection inside the new element if there isn't a range selection and there isn't already a caret selection inside.
861         // This is needed (at least) to remove caret from console when focus is moved to some element in the panel.
862         // The code below should not be applied to text fields and text areas, hence _isTextEditingElement check.
863         var selection = window.getSelection();
864         if (!WebInspector._isTextEditingElement(WebInspector._currentFocusElement) && selection.isCollapsed && !WebInspector._currentFocusElement.isInsertionCaretInside()) {
865             var selectionRange = WebInspector._currentFocusElement.ownerDocument.createRange();
866             selectionRange.setStart(WebInspector._currentFocusElement, 0);
867             selectionRange.setEnd(WebInspector._currentFocusElement, 0);
868
869             selection.removeAllRanges();
870             selection.addRange(selectionRange);
871         }
872     } else if (WebInspector._previousFocusElement)
873         WebInspector._previousFocusElement.blur();
874 }
875
876 WebInspector.restoreFocusFromElement = function(element)
877 {
878     if (element && element.isSelfOrAncestor(WebInspector.currentFocusElement()))
879         WebInspector.setCurrentFocusElement(WebInspector.previousFocusElement());
880 }
881
882 WebInspector.setToolbarColors = function(backgroundColor, color)
883 {
884     if (!WebInspector._themeStyleElement) {
885         WebInspector._themeStyleElement = document.createElement("style");
886         document.head.appendChild(WebInspector._themeStyleElement);
887     }
888     WebInspector._themeStyleElement.textContent =
889         "#toolbar {\
890              background-image: none !important;\
891              background-color: " + backgroundColor + " !important;\
892          }\
893          \
894          .toolbar-label {\
895              color: " + color + " !important;\
896              text-shadow: none;\
897          }";
898 }
899
900 WebInspector.resetToolbarColors = function()
901 {
902     if (WebInspector._themeStyleElement)
903         WebInspector._themeStyleElement.textContent = "";
904 }
905
906 /**
907  * @param {Element} element
908  * @param {number} offset
909  * @param {number} length
910  * @param {Array.<Object>=} domChanges
911  */
912 WebInspector.highlightSearchResult = function(element, offset, length, domChanges)
913 {
914     var result = WebInspector.highlightSearchResults(element, [{offset: offset, length: length }], domChanges);
915     return result.length ? result[0] : null;
916 }
917
918 /**
919  * @param {Element} element
920  * @param {Array.<Object>} resultRanges
921  * @param {Array.<Object>=} changes
922  */
923 WebInspector.highlightSearchResults = function(element, resultRanges, changes)
924 {
925     return WebInspector.highlightRangesWithStyleClass(element, resultRanges, "webkit-search-result", changes);
926 }
927
928 /**
929  * @param {Element} element
930  * @param {Array.<Object>} resultRanges
931  * @param {string} styleClass
932  * @param {Array.<Object>=} changes
933  */
934 WebInspector.highlightRangesWithStyleClass = function(element, resultRanges, styleClass, changes)
935 {
936     changes = changes || [];
937     var highlightNodes = [];
938     var lineText = element.textContent;
939     var ownerDocument = element.ownerDocument;
940     var textNodeSnapshot = ownerDocument.evaluate(".//text()", element, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
941
942     var snapshotLength = textNodeSnapshot.snapshotLength;
943     if (snapshotLength === 0)
944         return highlightNodes;
945
946     var nodeRanges = [];
947     var rangeEndOffset = 0;
948     for (var i = 0; i < snapshotLength; ++i) {
949         var range = {};
950         range.offset = rangeEndOffset;
951         range.length = textNodeSnapshot.snapshotItem(i).textContent.length;
952         rangeEndOffset = range.offset + range.length;
953         nodeRanges.push(range);
954     }
955
956     var startIndex = 0;
957     for (var i = 0; i < resultRanges.length; ++i) {
958         var startOffset = resultRanges[i].offset;
959         var endOffset = startOffset + resultRanges[i].length;
960
961         while (startIndex < snapshotLength && nodeRanges[startIndex].offset + nodeRanges[startIndex].length <= startOffset)
962             startIndex++;
963         var endIndex = startIndex;
964         while (endIndex < snapshotLength && nodeRanges[endIndex].offset + nodeRanges[endIndex].length < endOffset)
965             endIndex++;
966         if (endIndex === snapshotLength)
967             break;
968
969         var highlightNode = ownerDocument.createElement("span");
970         highlightNode.className = styleClass;
971         highlightNode.textContent = lineText.substring(startOffset, endOffset);
972
973         var lastTextNode = textNodeSnapshot.snapshotItem(endIndex);
974         var lastText = lastTextNode.textContent;
975         lastTextNode.textContent = lastText.substring(endOffset - nodeRanges[endIndex].offset);
976         changes.push({ node: lastTextNode, type: "changed", oldText: lastText, newText: lastTextNode.textContent });
977
978         if (startIndex === endIndex) {
979             lastTextNode.parentElement.insertBefore(highlightNode, lastTextNode);
980             changes.push({ node: highlightNode, type: "added", nextSibling: lastTextNode, parent: lastTextNode.parentElement });
981             highlightNodes.push(highlightNode);
982
983             var prefixNode = ownerDocument.createTextNode(lastText.substring(0, startOffset - nodeRanges[startIndex].offset));
984             lastTextNode.parentElement.insertBefore(prefixNode, highlightNode);
985             changes.push({ node: prefixNode, type: "added", nextSibling: highlightNode, parent: lastTextNode.parentElement });
986         } else {
987             var firstTextNode = textNodeSnapshot.snapshotItem(startIndex);
988             var firstText = firstTextNode.textContent;
989             var anchorElement = firstTextNode.nextSibling;
990
991             firstTextNode.parentElement.insertBefore(highlightNode, anchorElement);
992             changes.push({ node: highlightNode, type: "added", nextSibling: anchorElement, parent: firstTextNode.parentElement });
993             highlightNodes.push(highlightNode);
994
995             firstTextNode.textContent = firstText.substring(0, startOffset - nodeRanges[startIndex].offset);
996             changes.push({ node: firstTextNode, type: "changed", oldText: firstText, newText: firstTextNode.textContent });
997
998             for (var j = startIndex + 1; j < endIndex; j++) {
999                 var textNode = textNodeSnapshot.snapshotItem(j);
1000                 var text = textNode.textContent;
1001                 textNode.textContent = "";
1002                 changes.push({ node: textNode, type: "changed", oldText: text, newText: textNode.textContent });
1003             }
1004         }
1005         startIndex = endIndex;
1006         nodeRanges[startIndex].offset = endOffset;
1007         nodeRanges[startIndex].length = lastTextNode.textContent.length;
1008
1009     }
1010     return highlightNodes;
1011 }
1012
1013 WebInspector.applyDomChanges = function(domChanges)
1014 {
1015     for (var i = 0, size = domChanges.length; i < size; ++i) {
1016         var entry = domChanges[i];
1017         switch (entry.type) {
1018         case "added":
1019             entry.parent.insertBefore(entry.node, entry.nextSibling);
1020             break;
1021         case "changed":
1022             entry.node.textContent = entry.newText;
1023             break;
1024         }
1025     }
1026 }
1027
1028 WebInspector.revertDomChanges = function(domChanges)
1029 {
1030     for (var i = domChanges.length - 1; i >= 0; --i) {
1031         var entry = domChanges[i];
1032         switch (entry.type) {
1033         case "added":
1034             if (entry.node.parentElement)
1035                 entry.node.parentElement.removeChild(entry.node);
1036             break;
1037         case "changed":
1038             entry.node.textContent = entry.oldText;
1039             break;
1040         }
1041     }
1042 }
1043
1044 WebInspector._coalescingLevel = 0;
1045
1046 WebInspector.startBatchUpdate = function()
1047 {
1048     if (!WebInspector._coalescingLevel)
1049         WebInspector._postUpdateHandlers = new Map();
1050     WebInspector._coalescingLevel++;
1051 }
1052
1053 WebInspector.endBatchUpdate = function()
1054 {
1055     if (--WebInspector._coalescingLevel)
1056         return;
1057
1058     var handlers = WebInspector._postUpdateHandlers;
1059     delete WebInspector._postUpdateHandlers;
1060
1061     var keys = handlers.keys();
1062     for (var i = 0; i < keys.length; ++i) {
1063         var object = keys[i];
1064         var methods = handlers.get(object).keys();
1065         for (var j = 0; j < methods.length; ++j)
1066             methods[j].call(object);
1067     }
1068 }
1069
1070 /**
1071  * @param {Object} object
1072  * @param {function()} method
1073  */
1074 WebInspector.invokeOnceAfterBatchUpdate = function(object, method)
1075 {
1076     if (!WebInspector._coalescingLevel) {
1077         method.call(object);
1078         return;
1079     }
1080     
1081     var methods = WebInspector._postUpdateHandlers.get(object);
1082     if (!methods) {
1083         methods = new Map();
1084         WebInspector._postUpdateHandlers.put(object, methods);
1085     }
1086     methods.put(method);
1087 }
1088
1089 ;(function() {
1090
1091 function windowLoaded()
1092 {
1093     window.addEventListener("focus", WebInspector._windowFocused, false);
1094     window.addEventListener("blur", WebInspector._windowBlurred, false);
1095     document.addEventListener("focus", WebInspector._focusChanged.bind(this), true);
1096     window.removeEventListener("DOMContentLoaded", windowLoaded, false);
1097 }
1098
1099 window.addEventListener("DOMContentLoaded", windowLoaded, false);
1100
1101 })();