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