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