aeb923c35257011beebecf7d89574116199e20be
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / LogContentView.js
1 /*
2  * Copyright (C) 2013 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WebInspector.LogContentView = function(representedObject)
27 {
28     WebInspector.ContentView.call(this, representedObject);
29
30     this._nestingLevel = 0;
31     this._selectedMessages = [];
32
33     this.element.classList.add(WebInspector.LogContentView.StyleClassName);
34
35     this.messagesElement = document.createElement("div");
36     this.messagesElement.className = "console-messages";
37     this.messagesElement.tabIndex = 0;
38     this.messagesElement.setAttribute("role", "log");
39     this.messagesElement.addEventListener("mousedown", this._mousedown.bind(this));
40     this.messagesElement.addEventListener("focus", this._didFocus.bind(this));
41     this.messagesElement.addEventListener("blur", this._didBlur.bind(this));
42     this.messagesElement.addEventListener("keydown", this._keyDown.bind(this));
43     this.messagesElement.addEventListener("dragstart", this._ondragstart.bind(this), true);
44     this.element.appendChild(this.messagesElement);
45
46     this.prompt = WebInspector.quickConsole.prompt;
47
48     this._keyboardShortcutCommandA = new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.CommandOrControl, "A");
49     this._keyboardShortcutEsc = new WebInspector.KeyboardShortcut(null, WebInspector.KeyboardShortcut.Key.Escape);
50
51     this._logViewController = new WebInspector.JavaScriptLogViewController(this.messagesElement, this.element, this.prompt, this, "console-prompt-history");
52
53     this._searchBar = new WebInspector.SearchBar("log-search-bar", WebInspector.UIString("Filter Console Log"), this);
54     this._searchBar.addEventListener(WebInspector.SearchBar.Event.TextChanged, this._searchTextDidChange, this);
55
56     var scopeBarItems = [
57         new WebInspector.ScopeBarItem(WebInspector.LogContentView.Scopes.All, WebInspector.UIString("All")),
58         new WebInspector.ScopeBarItem(WebInspector.LogContentView.Scopes.Errors, WebInspector.UIString("Errors")),
59         new WebInspector.ScopeBarItem(WebInspector.LogContentView.Scopes.Warnings, WebInspector.UIString("Warnings")),
60         new WebInspector.ScopeBarItem(WebInspector.LogContentView.Scopes.Logs, WebInspector.UIString("Logs"))
61     ];
62
63     this._scopeBar = new WebInspector.ScopeBar("log-scope-bar", scopeBarItems, scopeBarItems[0]);
64     this._scopeBar.addEventListener(WebInspector.ScopeBar.Event.SelectionChanged, this._scopeBarSelectionDidChange, this);
65
66     var trashImage;
67     if (WebInspector.Platform.isLegacyMacOS)
68         trashImage = {src: "Images/Legacy/NavigationItemTrash.svg", width: 16, height: 16};
69     else
70         trashImage = {src: "Images/NavigationItemTrash.svg", width: 15, height: 15};
71
72     this._clearLogNavigationItem = new WebInspector.ButtonNavigationItem("clear-log", WebInspector.UIString("Clear log (%s or %s)").format(this._logViewController.messagesClearKeyboardShortcut.displayName, this._logViewController.messagesAlternateClearKeyboardShortcut.displayName), trashImage.src, trashImage.width, trashImage.height);
73     this._clearLogNavigationItem.addEventListener(WebInspector.ButtonNavigationItem.Event.Clicked, this._clearLog, this);
74
75     this._clearLogOnReloadSetting = new WebInspector.Setting("clear-log-on-reload", true);
76
77     var toolTip = WebInspector.UIString("Show split console");
78     var altToolTip = WebInspector.UIString("Show full-height console");
79
80     this._toggleSplitNavigationItem = new WebInspector.ToggleButtonNavigationItem("split-toggle", toolTip, altToolTip, platformImagePath("SplitToggleDown.svg"), platformImagePath("SplitToggleUp.svg"), 16, 16);
81     this._toggleSplitNavigationItem.addEventListener(WebInspector.ButtonNavigationItem.Event.Clicked, this._toggleSplit, this);
82     this._toggleSplitNavigationItem.toggled = WebInspector.isShowingSplitConsole();
83
84     this.messagesElement.addEventListener("contextmenu", this._handleContextMenuEvent.bind(this), false);
85
86     WebInspector.logManager.addEventListener(WebInspector.LogManager.Event.Cleared, this._sessionsCleared, this);
87     WebInspector.logManager.addEventListener(WebInspector.LogManager.Event.SessionStarted, this._sessionStarted, this);
88     WebInspector.logManager.addEventListener(WebInspector.LogManager.Event.MessageAdded, this._messageAdded, this);
89     WebInspector.logManager.addEventListener(WebInspector.LogManager.Event.PreviousMessageRepeatCountUpdated, this._previousMessageRepeatCountUpdated, this);
90     WebInspector.logManager.addEventListener(WebInspector.LogManager.Event.ActiveLogCleared, this._activeLogCleared, this);
91 };
92
93 WebInspector.LogContentView.Scopes = {
94     All: "log-all",
95     Errors: "log-errors",
96     Warnings: "log-warnings",
97     Logs: "log-logs"
98 };
99
100 WebInspector.LogContentView.StyleClassName = "log";
101 WebInspector.LogContentView.ItemWrapperStyleClassName = "console-item";
102 WebInspector.LogContentView.FilteredOutStyleClassName = "filtered-out";
103 WebInspector.LogContentView.SelectedStyleClassName = "selected";
104 WebInspector.LogContentView.SearchInProgressStyleClassName = "search-in-progress";
105 WebInspector.LogContentView.FilteredOutBySearchStyleClassName = "filtered-out-by-search";
106 WebInspector.LogContentView.HighlightedStyleClassName = "highlighted";
107
108 WebInspector.LogContentView.prototype = {
109     constructor: WebInspector.LogContentView,
110
111     // Public
112     get allowedNavigationSidebarPanels()
113     {
114         // Don't show any sidebars when the Console is opened.
115         return null;
116     },
117
118     get navigationItems()
119     {
120         return [this._searchBar, this._scopeBar, this._clearLogNavigationItem, this._toggleSplitNavigationItem];
121     },
122
123     get scopeBar()
124     {
125         return this._scopeBar;
126     },
127
128     get logViewController()
129     {
130         return this._logViewController;
131     },
132
133     updateLayout: function()
134     {
135         WebInspector.ContentView.prototype.updateLayout.call(this);
136
137         this._scrollElementHeight = this.messagesElement.getBoundingClientRect().height;
138     },
139
140     shown: function()
141     {
142         this._toggleSplitNavigationItem.toggled = WebInspector.isShowingSplitConsole();
143
144         this.prompt.focus();
145     },
146
147     get scrollableElements()
148     {
149         return [this.element];
150     },
151
152     get shouldKeepElementsScrolledToBottom()
153     {
154         return true;
155     },
156
157     get searchInProgress()
158     {
159         return this.messagesElement.classList.contains(WebInspector.LogContentView.SearchInProgressStyleClassName);
160     },
161
162     didClearMessages: function()
163     {
164         if (this._ignoreDidClearMessages)
165             return;
166         WebInspector.logManager.requestClearMessages();
167     },
168
169     didAppendConsoleMessage: function(message)
170     {
171         WebInspector.quickConsole.updateLayout();
172
173         // Nest the message.
174         if (message.type !== WebInspector.ConsoleMessage.MessageType.EndGroup) {
175             var x = 16 * this._nestingLevel;
176             var messageElement = message.toMessageElement();
177             messageElement.style.left = x + "px";
178             messageElement.style.width = "calc(100% - " + x + "px)";
179         }
180
181         // Update the nesting level.
182         switch (message.type) {
183         case WebInspector.ConsoleMessage.MessageType.StartGroup:
184         case WebInspector.ConsoleMessage.MessageType.StartGroupCollapsed:
185             ++this._nestingLevel;
186             break;
187         case WebInspector.ConsoleMessage.MessageType.EndGroup:
188             --this._nestingLevel;
189             break;
190         }
191
192         this._clearFocusableChildren();
193
194         // Some results don't populate until further backend dispatches occur (like the DOM tree).
195         // We want to remove focusable children after those pending dispatches too.
196         InspectorBackend.runAfterPendingDispatches(this._clearFocusableChildren.bind(this));
197
198         // We only auto show the console if the message is a result.
199         // This is when the user evaluated something directly in the prompt.
200         if (message.type !== WebInspector.ConsoleMessage.MessageType.Result)
201             return;
202
203         if (!WebInspector.isShowingConsoleView())
204             WebInspector.showSplitConsole();
205
206         this._logViewController.scrollToBottom();
207     },
208
209     promptDidChangeHeight: function()
210     {
211         WebInspector.quickConsole.updateLayout();
212     },
213
214     get supportsSave()
215     {
216         return true;
217     },
218
219     get saveData()
220     {
221         return {url: "web-inspector:///Console.txt", content: this._formatMessagesAsData(false), forceSaveAs: true};
222     },
223
224     handleCopyEvent: function(event)
225     {
226         if (!this._selectedMessages.length)
227             return;
228
229         event.clipboardData.setData("text/plain", this._formatMessagesAsData(true));
230         event.stopPropagation();
231         event.preventDefault();
232     },
233
234     focusSearchBar: function()
235     {
236         this._searchBar.focus();
237     },
238
239     highlightPreviousSearchMatch: function()
240     {
241         if (!this.searchInProgress || isEmptyObject(this._searchMatches))
242             return;
243
244         var index = this._selectedSearchMatch ? this._searchMatches.indexOf(this._selectedSearchMatch) : this._searchMatches.length;
245         this._highlightSearchMatchAtIndex(index - 1);
246     },
247
248     highlightNextSearchMatch: function()
249     {
250         if (!this.searchInProgress || isEmptyObject(this._searchMatches))
251             return;
252
253         var index = this._selectedSearchMatch ? this._searchMatches.indexOf(this._selectedSearchMatch) + 1 : 0;
254         this._highlightSearchMatchAtIndex(index);
255     },
256
257     searchBarWantsToLoseFocus: function(searchBar)
258     {
259         if (this._selectedMessages.length)
260             this.messagesElement.focus();
261         else
262             this.prompt.focus();
263     },
264
265     searchBarDidActivate: function(searchBar)
266     {
267         if (!isEmptyObject(this._searchMatches))
268             this._highlightSearchMatchAtIndex(0);
269         this.prompt.focus();
270     },
271
272     // Private
273
274     _formatMessagesAsData: function(onlySelected)
275     {
276         var messages = this._allMessages();
277
278         if (onlySelected) {
279             messages = this._allMessages().filter(function(message) {
280                 return message.classList.contains(WebInspector.LogContentView.SelectedStyleClassName);
281             });
282         }
283
284         var data = "";
285
286         var isPrefixOptional = messages.length <= 1 && onlySelected;
287         messages.forEach(function (messageElement, index) {
288             var messageObject = messageElement.message;
289             if (!messageObject)
290                 messageObject = messageElement.command;
291             if (!messageObject)
292                 return;
293
294             if (index > 0)
295                 data += "\n";
296             data += messageObject.toClipboardString(isPrefixOptional);
297         });
298
299         return data;
300     },
301
302     _sessionsCleared: function(event)
303     {
304         this._ignoreDidClearMessages = true;
305         this._logViewController.clear();
306         this._ignoreDidClearMessages = false;
307     },
308
309     _sessionStarted: function(event)
310     {
311         if (this._clearLogOnReloadSetting.value)  {
312             this._clearLog();
313             return;
314         }
315
316         this._logViewController.startNewSession();
317     },
318
319     _messageAdded: function(event)
320     {
321         var message = this._logViewController.appendConsoleMessage(event.data.message);
322         if (message.type !== WebInspector.ConsoleMessage.MessageType.EndGroup)
323             this._filterMessages([message.toMessageElement()]);
324     },
325
326     _previousMessageRepeatCountUpdated: function(event)
327     {
328         this._logViewController.updatePreviousMessageRepeatCount(event.data.count);
329     },
330
331     _handleContextMenuEvent: function(event)
332     {
333         if (!window.getSelection().isCollapsed) {
334             // If there is a selection, we want to show our normal context menu
335             // (with Copy, etc.), and not Clear Log.
336             return;
337         }
338
339         // We don't want to show the custom menu for links in the console.
340         if (event.target.enclosingNodeOrSelfWithNodeName("a"))
341             return;
342
343         var contextMenu = new WebInspector.ContextMenu(event);
344         contextMenu.appendItem(WebInspector.UIString("Clear Log"), this._clearLog.bind(this));
345         contextMenu.appendSeparator();
346
347         var clearLogOnReloadUIString = this._clearLogOnReloadSetting.value ? WebInspector.UIString("Keep Log on Reload") : WebInspector.UIString("Clear Log on Reload");
348
349         contextMenu.appendItem(clearLogOnReloadUIString, this._toggleClearLogOnReloadSetting.bind(this));
350
351         contextMenu.show();
352     },
353
354     _mousedown: function(event)
355     {
356         if (event.button !== 0 || event.ctrlKey)
357             return;
358
359         if (event.defaultPrevented) {
360             // Default was prevented on the event, so this means something deeper (like a disclosure triangle)
361             // handled the mouse down. In this case we want to clear the selection and don't make a new selection.
362             this._clearMessagesSelection();
363             return;
364         }
365
366         if (!this._focused) {
367             this.messagesElement.focus();
368             if (this._selectedMessages.length)
369                 return;
370         }
371
372         this._mouseDownWrapper = event.target.enclosingNodeOrSelfWithClass(WebInspector.LogContentView.ItemWrapperStyleClassName);
373         this._mouseDownShiftKey = event.shiftKey;
374         this._mouseDownCommandKey = event.metaKey;
375         this._mouseMoveIsRowSelection = false;
376
377         window.addEventListener("mousemove", this);
378         window.addEventListener("mouseup", this);
379     },
380
381     _targetInMessageCanBeSelected: function(target, message)
382     {
383         if (target.enclosingNodeOrSelfWithNodeName("a"))
384             return false;
385         return true;
386     },
387
388     _mousemove: function(event)
389     {
390         var selection = window.getSelection();
391         var wrapper = event.target.enclosingNodeOrSelfWithClass(WebInspector.LogContentView.ItemWrapperStyleClassName);
392
393         if (!wrapper) {
394             // No wrapper under the mouse, so look at the selection to try and find one.
395             if (!selection.isCollapsed) {
396                 wrapper = selection.focusNode.parentNode.enclosingNodeOrSelfWithClass(WebInspector.LogContentView.ItemWrapperStyleClassName);
397                 selection.removeAllRanges();
398             }
399
400             if (!wrapper)
401                 return;
402         }
403
404         if (!selection.isCollapsed)
405             this._clearMessagesSelection();
406
407         if (wrapper === this._mouseDownWrapper && !this._mouseMoveIsRowSelection)
408             return;
409
410         selection.removeAllRanges();
411
412         if (!this._mouseMoveIsRowSelection)
413             this._updateMessagesSelection(this._mouseDownWrapper, this._mouseDownCommandKey, this._mouseDownShiftKey);
414
415         this._updateMessagesSelection(wrapper, false, true);
416
417         this._mouseMoveIsRowSelection = true;
418
419         event.preventDefault();
420         event.stopPropagation();
421     },
422
423     _mouseup: function(event)
424     {
425         window.removeEventListener("mousemove", this);
426         window.removeEventListener("mouseup", this);
427
428         var selection = window.getSelection();
429         var wrapper = event.target.enclosingNodeOrSelfWithClass(WebInspector.LogContentView.ItemWrapperStyleClassName);
430
431         if (wrapper && (selection.isCollapsed || event.shiftKey)) {
432             selection.removeAllRanges();
433
434             if (this._targetInMessageCanBeSelected(event.target, wrapper)) {
435                 var sameWrapper = wrapper === this._mouseDownWrapper;
436                 this._updateMessagesSelection(wrapper, sameWrapper ? this._mouseDownCommandKey : false, sameWrapper ? this._mouseDownShiftKey : true);
437             }
438         } else if (!selection.isCollapsed) {
439             // There is a text selection, clear the row selection.
440             this._clearMessagesSelection();
441         } else if (!this._mouseDownWrapper) {
442             // The mouse didn't hit a console item, so clear the row selection.
443             this._clearMessagesSelection();
444
445             // Focus the prompt. Focusing the prompt needs to happen after the click to work.
446             setTimeout(function () { this.prompt.focus(); }.bind(this), 0);
447         }
448
449         delete this._mouseMoveIsRowSelection;
450         delete this._mouseDownWrapper;
451         delete this._mouseDownShiftKey;
452         delete this._mouseDownCommandKey;
453     },
454
455     _ondragstart: function(event)
456     {
457         if (event.target.enclosingNodeOrSelfWithClass(WebInspector.DOMTreeOutline.StyleClassName)) {
458             event.stopPropagation();
459             event.preventDefault();
460         }
461     },
462
463     handleEvent: function(event)
464     {
465         switch (event.type) {
466         case "mousemove":
467             this._mousemove(event);
468             break;
469         case "mouseup":
470             this._mouseup(event);
471             break;
472         }
473     },
474
475     _updateMessagesSelection: function(message, multipleSelection, rangeSelection)
476     {
477         var alreadySelectedMessage = this._selectedMessages.contains(message);
478         if (alreadySelectedMessage && this._selectedMessages.length && multipleSelection) {
479             message.classList.remove(WebInspector.LogContentView.SelectedStyleClassName);
480             this._selectedMessages.remove(message);
481             return;
482         }
483
484         if (!multipleSelection && !rangeSelection)
485             this._clearMessagesSelection();
486
487         if (rangeSelection) {
488             var messages = this._visibleMessages();
489
490             var refIndex = this._referenceMessageForRangeSelection ? messages.indexOf(this._referenceMessageForRangeSelection) : 0;
491             var targetIndex = messages.indexOf(message);
492
493             var newRange = [Math.min(refIndex, targetIndex), Math.max(refIndex, targetIndex)];
494
495             if (this._selectionRange && this._selectionRange[0] === newRange[0] && this._selectionRange[1] === newRange[1])
496                 return;
497
498             var startIndex = this._selectionRange ? Math.min(this._selectionRange[0], newRange[0]) : newRange[0];
499             var endIndex = this._selectionRange ? Math.max(this._selectionRange[1], newRange[1]) : newRange[1];
500
501             for (var i = startIndex; i <= endIndex; ++i) {
502                 var messageInRange = messages[i];
503                 if (i >= newRange[0] && i <= newRange[1] && !messageInRange.classList.contains(WebInspector.LogContentView.SelectedStyleClassName)) {
504                     messageInRange.classList.add(WebInspector.LogContentView.SelectedStyleClassName);
505                     this._selectedMessages.push(messageInRange);
506                 } else if (i < newRange[0] || i > newRange[1] && messageInRange.classList.contains(WebInspector.LogContentView.SelectedStyleClassName)) {
507                     messageInRange.classList.remove(WebInspector.LogContentView.SelectedStyleClassName);
508                     this._selectedMessages.remove(messageInRange);
509                 }
510             }
511
512             this._selectionRange = newRange;
513         } else {
514             message.classList.add(WebInspector.LogContentView.SelectedStyleClassName);
515             this._selectedMessages.push(message);
516         }
517
518         if (!rangeSelection)
519             this._referenceMessageForRangeSelection = message;
520
521         if (!alreadySelectedMessage)
522             this._ensureMessageIsVisible(this._selectedMessages.lastValue);
523     },
524
525     _ensureMessageIsVisible: function(message)
526     {
527         if (!message)
528             return;
529
530         var y = this._positionForMessage(message).y;
531         if (y < 0) {
532             this.element.scrollTop += y;
533             return;
534         }
535
536         var nextMessage = this._nextMessage(message);
537         if (nextMessage) {
538             y = this._positionForMessage(nextMessage).y;
539             if (y > this._scrollElementHeight)
540                 this.element.scrollTop += y - this._scrollElementHeight;
541         } else {
542             y += message.getBoundingClientRect().height;
543             if (y > this._scrollElementHeight)
544                 this.element.scrollTop += y - this._scrollElementHeight;
545         }
546     },
547
548     _positionForMessage: function(message)
549     {
550         var pagePoint = window.webkitConvertPointFromNodeToPage(message, new WebKitPoint(0, 0));
551         return window.webkitConvertPointFromPageToNode(this.element, pagePoint);
552     },
553
554     _isMessageVisible: function(message)
555     {
556         var node = message;
557
558         if (node.classList.contains(WebInspector.LogContentView.FilteredOutStyleClassName))
559             return false;
560
561         if (this.searchInProgress && node.classList.contains(WebInspector.LogContentView.FilteredOutBySearchStyleClassName))
562             return false;
563
564         if (message.classList.contains("console-group-title"))
565             node = node.parentNode.parentNode;
566
567         while (node && node !== this.messagesElement) {
568             if (node.classList.contains("collapsed"))
569                 return false;
570             node = node.parentNode;
571         }
572
573         return true;
574     },
575
576     _isMessageSelected: function(message)
577     {
578         return message.classList.contains(WebInspector.LogContentView.SelectedStyleClassName);
579     },
580
581     _clearMessagesSelection: function()
582     {
583         this._selectedMessages.forEach(function(message) {
584             message.classList.remove(WebInspector.LogContentView.SelectedStyleClassName);
585         });
586         this._selectedMessages = [];
587         delete this._referenceMessageForRangeSelection;
588     },
589
590     _selectAllMessages: function()
591     {
592         this._clearMessagesSelection();
593
594         var messages = this._visibleMessages();
595         for (var i = 0; i < messages.length; ++i) {
596             var message = messages[i];
597             message.classList.add(WebInspector.LogContentView.SelectedStyleClassName);
598             this._selectedMessages.push(message);
599         }
600     },
601
602     _allMessages: function()
603     {
604         return Array.from(this.messagesElement.querySelectorAll(".console-message, .console-user-command"));
605     },
606
607     _unfilteredMessages: function()
608     {
609         return this._allMessages().filter(function(message) {
610             return !message.classList.contains(WebInspector.LogContentView.FilteredOutStyleClassName);
611         });
612     },
613
614     _visibleMessages: function()
615     {
616         var unfilteredMessages = this._unfilteredMessages();
617
618         if (!this.searchInProgress)
619             return unfilteredMessages;
620
621         return unfilteredMessages.filter(function(message) {
622             return !message.classList.contains(WebInspector.LogContentView.FilteredOutBySearchStyleClassName);
623         });
624     },
625
626     _activeLogCleared: function(event)
627     {
628         this._ignoreDidClearMessages = true;
629         this._logViewController.clear();
630         this._ignoreDidClearMessages = false;
631     },
632
633     _toggleSplit: function()
634     {
635         if (WebInspector.isShowingSplitConsole())
636             WebInspector.showFullHeightConsole();
637         else
638             WebInspector.showSplitConsole();
639     },
640
641     _toggleClearLogOnReloadSetting: function()
642     {
643         this._clearLogOnReloadSetting.value = !this._clearLogOnReloadSetting.value;
644     },
645
646     _clearLog: function()
647     {
648         this._ignoreDidClearMessages = true;
649         this._logViewController.clear();
650         this._ignoreDidClearMessages = false;
651     },
652
653     _scopeBarSelectionDidChange: function(event)
654     {
655         this._filterMessages(this._allMessages());
656     },
657
658     _filterMessages: function(messages)
659     {
660         var showsAll = this._scopeBar.item(WebInspector.LogContentView.Scopes.All).selected;
661         var showsErrors = this._scopeBar.item(WebInspector.LogContentView.Scopes.Errors).selected;
662         var showsWarnings = this._scopeBar.item(WebInspector.LogContentView.Scopes.Warnings).selected;
663         var showsLogs = this._scopeBar.item(WebInspector.LogContentView.Scopes.Logs).selected;
664
665         messages.forEach(function(message) {
666             var visible = showsAll || message.command instanceof WebInspector.ConsoleCommand || message.message instanceof WebInspector.ConsoleCommandResult;
667             if (!visible) {
668                 switch(message.message.level) {
669                     case WebInspector.ConsoleMessage.MessageLevel.Warning:
670                         visible = showsWarnings;
671                         break;
672                     case WebInspector.ConsoleMessage.MessageLevel.Error:
673                         visible = showsErrors;
674                         break;
675                     case WebInspector.ConsoleMessage.MessageLevel.Log:
676                         visible = showsLogs;
677                         break;
678                 }
679             }
680
681             var classList = message.classList;
682             if (visible)
683                 classList.remove(WebInspector.LogContentView.FilteredOutStyleClassName);
684             else {
685                 this._selectedMessages.remove(message);
686                 classList.remove(WebInspector.LogContentView.SelectedStyleClassName);
687                 classList.add(WebInspector.LogContentView.FilteredOutStyleClassName);
688             }
689         }, this);
690
691         this._performSearch();
692     },
693
694     _didFocus: function(event)
695     {
696         this._focused = true;
697     },
698
699     _didBlur: function(event)
700     {
701         this._focused = false;
702     },
703
704     _keyDown: function(event)
705     {
706         if (this._keyboardShortcutCommandA.matchesEvent(event))
707             this._commandAWasPressed(event);
708         else if (this._keyboardShortcutEsc.matchesEvent(event))
709             this._escapeWasPressed(event);
710         else if (event.keyIdentifier === "Up")
711             this._upArrowWasPressed(event);
712         else if (event.keyIdentifier === "Down")
713             this._downArrowWasPressed(event);
714         else if (event.keyIdentifier === "Left")
715             this._leftArrowWasPressed(event);
716         else if (event.keyIdentifier === "Right")
717             this._rightArrowWasPressed(event);
718     },
719
720     _commandAWasPressed: function(event)
721     {
722         this._selectAllMessages();
723         event.preventDefault();
724     },
725
726     _escapeWasPressed: function(event)
727     {
728         if (this._selectedMessages.length)
729             this._clearMessagesSelection();
730         else
731             this.prompt.focus();
732
733         event.preventDefault();
734     },
735
736     _upArrowWasPressed: function(event)
737     {
738         var messages = this._visibleMessages();
739
740         if (!this._selectedMessages.length) {
741             if (messages.length)
742                 this._updateMessagesSelection(messages.lastValue, false, false);
743             return;
744         }
745
746         var lastMessage = this._selectedMessages.lastValue;
747         var previousMessage = this._previousMessage(lastMessage);
748         if (previousMessage)
749             this._updateMessagesSelection(previousMessage, false, event.shiftKey);
750         else if (!event.shiftKey) {
751             this._clearMessagesSelection();
752             this._updateMessagesSelection(messages[0], false, false);
753         }
754
755         event.preventDefault();
756     },
757
758     _downArrowWasPressed: function(event)
759     {
760         var messages = this._visibleMessages();
761
762         if (!this._selectedMessages.length) {
763             if (messages.length)
764                 this._updateMessagesSelection(messages[0], false, false);
765             return;
766         }
767
768         var lastMessage = this._selectedMessages.lastValue;
769         var nextMessage = this._nextMessage(lastMessage);
770         if (nextMessage)
771             this._updateMessagesSelection(nextMessage, false, event.shiftKey);
772         else if (!event.shiftKey) {
773             this._clearMessagesSelection();
774             this._updateMessagesSelection(messages.lastValue, false, false);
775         }
776
777         event.preventDefault();
778     },
779
780     _leftArrowWasPressed: function(event)
781     {
782         if (this._selectedMessages.length !== 1)
783             return;
784
785         var currentMessage = this._selectedMessages[0];
786         if (currentMessage.classList.contains("console-group-title"))
787             currentMessage.parentNode.classList.add("collapsed");
788         else {
789             var outlineTitle = currentMessage.querySelector("ol.outline-disclosure > li.parent");
790             if (outlineTitle) {
791                 if (event.altKey)
792                     outlineTitle.treeElement.collapseRecursively();
793                 else
794                     outlineTitle.treeElement.collapse();
795             } else {
796                 // FIXME: <https://webkit.org/b/141949> Web Inspector: Right/Left arrow no longer works in console to expand/collapse ObjectTrees
797                 var outlineSection = currentMessage.querySelector(".console-formatted-object > .section");
798                 if (outlineSection)
799                     outlineSection._section.collapse();
800             }
801         }
802     },
803
804     _rightArrowWasPressed: function(event)
805     {
806         if (this._selectedMessages.length !== 1)
807             return;
808
809         var currentMessage = this._selectedMessages[0];
810         if (currentMessage.classList.contains("console-group-title"))
811             currentMessage.parentNode.classList.remove("collapsed");
812         else {
813             var outlineTitle = currentMessage.querySelector("ol.outline-disclosure > li.parent");
814             if (outlineTitle) {
815                 outlineTitle.treeElement.onexpand = function() {
816                     setTimeout(function () {
817                         this._ensureMessageIsVisible(currentMessage);
818                         this._clearFocusableChildren();
819                         delete outlineTitle.treeElement.onexpand;
820                     }.bind(this));
821                 }.bind(this);
822
823                 if (event.altKey)
824                     outlineTitle.treeElement.expandRecursively();
825                 else
826                     outlineTitle.treeElement.expand();
827             } else {
828                 // FIXME: <https://webkit.org/b/141949> Web Inspector: Right/Left arrow no longer works in console to expand/collapse ObjectTrees
829                 var outlineSection = currentMessage.querySelector(".console-formatted-object > .section");
830                 if (outlineSection) {
831                     outlineSection._section.addEventListener(WebInspector.Section.Event.VisibleContentDidChange, this._propertiesSectionDidUpdateContent, this);
832                     outlineSection._section.expand();
833                 }
834             }
835         }
836     },
837
838     _propertiesSectionDidUpdateContent: function(event)
839     {
840         var section = event.target;
841         section.removeEventListener(WebInspector.Section.Event.VisibleContentDidChange, this._propertiesSectionDidUpdateContent, this);
842
843         var message = section.element.enclosingNodeOrSelfWithClass(WebInspector.LogContentView.ItemWrapperStyleClassName);
844         if (!this._isMessageSelected(message))
845             return;
846
847         setTimeout(function () {
848             this._ensureMessageIsVisible(message);
849             this._clearFocusableChildren();
850         }.bind(this));
851     },
852
853     _previousMessage: function(message)
854     {
855         var messages = this._visibleMessages();
856         for (var i = messages.indexOf(message) - 1; i >= 0; --i) {
857             if (this._isMessageVisible(messages[i]))
858                 return messages[i];
859         }
860     },
861
862     _nextMessage: function(message)
863     {
864         var messages = this._visibleMessages();
865         for (var i = messages.indexOf(message) + 1; i < messages.length; ++i) {
866             if (this._isMessageVisible(messages[i]))
867                 return messages[i];
868         }
869     },
870
871     _clearFocusableChildren: function()
872     {
873         var focusableElements = this.messagesElement.querySelectorAll("[tabindex]");
874         for (var i = 0, count = focusableElements.length; i < count; ++i)
875             focusableElements[i].removeAttribute("tabindex");
876     },
877
878     _searchTextDidChange: function(event)
879     {
880         this._performSearch();
881     },
882
883     _performSearch: function()
884     {
885         if (!isEmptyObject(this._searchHighlightDOMChanges))
886             WebInspector.revertDomChanges(this._searchHighlightDOMChanges);
887
888         var searchTerms = this._searchBar.text;
889
890         if (searchTerms === "") {
891             delete this._selectedSearchMatch;
892             this._matchingSearchElements = [];
893             this.messagesElement.classList.remove(WebInspector.LogContentView.SearchInProgressStyleClassName);
894             return;
895         }
896
897         this.messagesElement.classList.add(WebInspector.LogContentView.SearchInProgressStyleClassName);
898
899         this._searchHighlightDOMChanges = [];
900         this._searchMatches = [];
901         this._selectedSearchMathIsValid = false;
902
903         var searchRegex = new RegExp(searchTerms.escapeForRegExp(), "gi");
904         this._unfilteredMessages().forEach(function(message) {
905             var matchRanges = [];
906             var text = message.textContent;
907             var match = searchRegex.exec(text);
908             while (match) {
909                 matchRanges.push({ offset: match.index, length: match[0].length });
910                 match = searchRegex.exec(text);
911             }
912
913             if (!isEmptyObject(matchRanges))
914                 this._highlightRanges(message, matchRanges);
915
916             var classList = message.classList;
917             if (!isEmptyObject(matchRanges) || message.command instanceof WebInspector.ConsoleCommand || message.message instanceof WebInspector.ConsoleCommandResult)
918                 classList.remove(WebInspector.LogContentView.FilteredOutBySearchStyleClassName);
919             else
920                 classList.add(WebInspector.LogContentView.FilteredOutBySearchStyleClassName);
921         }, this);
922
923         if (!this._selectedSearchMathIsValid && this._selectedSearchMatch) {
924             this._selectedSearchMatch.highlight.classList.remove(WebInspector.LogContentView.SelectedStyleClassName);
925             delete this._selectedSearchMatch;
926         }
927     },
928
929     _highlightRanges: function(message, matchRanges)
930     {
931         var highlightedElements = WebInspector.highlightRangesWithStyleClass(message, matchRanges, WebInspector.LogContentView.HighlightedStyleClassName, this._searchHighlightDOMChanges);
932
933         console.assert(highlightedElements.length === matchRanges.length);
934
935         matchRanges.forEach(function (range, index) {
936             this._searchMatches.push({
937                 message: message,
938                 range: range,
939                 highlight: highlightedElements[index]
940             });
941
942             if (this._selectedSearchMatch && !this._selectedSearchMathIsValid && this._selectedSearchMatch.message === message) {
943                 this._selectedSearchMathIsValid = this._rangesOverlap(this._selectedSearchMatch.range, range);
944                 if (this._selectedSearchMathIsValid) {
945                     delete this._selectedSearchMatch;
946                     this._highlightSearchMatchAtIndex(this._searchMatches.length - 1);
947                 }
948             }
949         }, this);
950     },
951
952     _rangesOverlap: function(range1, range2)
953     {
954         return range1.offset <= range2.offset + range2.length && range2.offset <= range1.offset + range1.length;
955     },
956
957     _highlightSearchMatchAtIndex: function(index)
958     {
959         if (index >= this._searchMatches.length)
960             index = 0;
961         else if (index < 0)
962             index = this._searchMatches.length - 1;
963
964         if (this._selectedSearchMatch)
965             this._selectedSearchMatch.highlight.classList.remove(WebInspector.LogContentView.SelectedStyleClassName);
966
967         this._selectedSearchMatch = this._searchMatches[index];
968         this._selectedSearchMatch.highlight.classList.add(WebInspector.LogContentView.SelectedStyleClassName);
969
970         this._ensureMessageIsVisible(this._selectedSearchMatch.message);
971     }
972 };
973
974 WebInspector.LogContentView.prototype.__proto__ = WebInspector.ContentView.prototype;