inspector.js   [plain text]


/*
 * Copyright (C) 2006, 2007 Apple Inc.  All rights reserved.
 * Copyright (C) 2007 Matt Lilek (pewtermoose@gmail.com).
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1.  Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer. 
 * 2.  Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution. 
 * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
 *     its contributors may be used to endorse or promote products derived
 *     from this software without specific prior written permission. 
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

var Preferences = {
    ignoreWhitespace: true,
    showUserAgentStyles: true,
    maxInlineTextChildLength: 80,
    maxTextSearchResultLength: 80,
    showInheritedComputedStyleProperties: false,
    showMissingLocalizedStrings: false
}

var WebInspector = {
    resources: [],
    resourceURLMap: {},
    backForwardList: [],
    searchResultsHeight: 100,
    localizedStrings: {},
    missingLocalizedStrings: {},

    get consolePanel()
    {
        if (!this._consolePanel)
            this._consolePanel = new WebInspector.ConsolePanel();

        return this._consolePanel;
    },

    get networkPanel()
    {
        if (!this._networkPanel)
            this._networkPanel = new WebInspector.NetworkPanel();

        return this._networkPanel;
    },

    get currentBackForwardIndex()
    {
        if (this._currentBackForwardIndex === undefined)
            this._currentBackForwardIndex = -1;

        return this._currentBackForwardIndex;
    },

    set currentBackForwardIndex(x)
    {
        if (this._currentBackForwardIndex === x)
            return;

        this._currentBackForwardIndex = x;
        this.updateBackForwardButtons();
    },

    get currentFocusElement()
    {
        return this._currentFocusElement;
    },

    set currentFocusElement(x)
    {
        if (!x || this._currentFocusElement === x)
            return;

        if (this._currentFocusElement) {
            this._currentFocusElement.removeStyleClass("focused");
            this._currentFocusElement.addStyleClass("blurred");
            if (this._currentFocusElement.blurred)
                this._currentFocusElement.blurred();
        }

        this._currentFocusElement = x;

        if (x) {
            x.addStyleClass("focused");
            x.removeStyleClass("blurred");
            if (this._currentFocusElement.focused)
                this._currentFocusElement.focused();
        }
    },

    get currentPanel()
    {
        return this._currentPanel;
    },

    set currentPanel(x)
    {
        if (this._currentPanel === x)
            return;

        if (this._currentPanel)
            this._currentPanel.hide();

        this._currentPanel = x;

        if (x)
            x.show();
    },

    get attached()
    {
        return this._attached;
    },

    set attached(x)
    {
        if (this._attached === x)
            return;

        this._attached = x;

        var body = document.body;
        if (x) {
            InspectorController.attach();
            body.removeStyleClass("detached");
            body.addStyleClass("attached");
        } else {
            InspectorController.detach();
            body.removeStyleClass("attached");
            body.addStyleClass("detached");
        }
    },

    get showingSearchResults()
    {
        return this._showingSearchResults;
    },

    set showingSearchResults(x)
    {
        if (this._showingSearchResults === x)
            return;

        this._showingSearchResults = x;

        var resultsContainer = document.getElementById("searchResults");
        var searchResultsResizer = document.getElementById("searchResultsResizer");

        if (x) {
            resultsContainer.removeStyleClass("hidden");
            searchResultsResizer.removeStyleClass("hidden");

            var animations = [
                {element: resultsContainer, end: {top: 0}},
                {element: searchResultsResizer, end: {top: WebInspector.searchResultsHeight - 3}},
                {element: document.getElementById("panels"), end: {top: WebInspector.searchResultsHeight}}
            ];

            WebInspector.animateStyle(animations, 250);
        } else {
            searchResultsResizer.addStyleClass("hidden");

            var animations = [
                {element: resultsContainer, end: {top: -WebInspector.searchResultsHeight}},
                {element: searchResultsResizer, end: {top: 0}},
                {element: document.getElementById("panels"), end: {top: 0}}
            ];

            var animationFinished = function()
            {
                resultsContainer.addStyleClass("hidden");
                resultsContainer.removeChildren();
                delete this.searchResultsTree;
            };

            WebInspector.animateStyle(animations, 250, animationFinished);
        }
    }
}

WebInspector.loaded = function()
{
    var platform = InspectorController.platform();
    document.body.addStyleClass("platform-" + platform);

    this.fileOutline = new TreeOutline(document.getElementById("list"));
    this.fileOutline.expandTreeElementsWhenArrowing = true;

    this.statusOutline = new TreeOutline(document.getElementById("status"));
    this.statusOutline.expandTreeElementsWhenArrowing = true;

    this.resourceCategories = {
        documents: new WebInspector.ResourceCategory(WebInspector.UIString("documents"), "documents"),
        stylesheets: new WebInspector.ResourceCategory(WebInspector.UIString("stylesheets"), "stylesheets"),
        images: new WebInspector.ResourceCategory(WebInspector.UIString("images"), "images"),
        scripts: new WebInspector.ResourceCategory(WebInspector.UIString("scripts"), "scripts"),
        fonts: new WebInspector.ResourceCategory(WebInspector.UIString("fonts"), "fonts"),
        databases: new WebInspector.ResourceCategory(WebInspector.UIString("databases"), "databases"),
        other: new WebInspector.ResourceCategory(WebInspector.UIString("other"), "other")
    };

    this.Tips = {
        ResourceNotCompressed: {id: 0, message: WebInspector.UIString("You could save bandwidth by having your web server compress this transfer with gzip or zlib.")}
    };

    this.Warnings = {
        IncorrectMIMEType: {id: 0, message: WebInspector.UIString("Resource interpreted as %s but transferred with MIME type %s.")}
    };

    this.consoleListItem = new WebInspector.ConsoleStatusTreeElement(WebInspector.consolePanel);
    this.statusOutline.appendChild(this.consoleListItem);

    this.networkListItem = new WebInspector.StatusTreeElement(WebInspector.UIString("Network"), "network", WebInspector.networkPanel);
    this.statusOutline.appendChild(this.networkListItem);

    this.resourceCategories.documents.listItem.expand();

    this.currentFocusElement = document.getElementById("sidebar");

    this.addMainEventListeners(document);

    window.addEventListener("unload", this.windowUnload.bind(this), true);
    window.addEventListener("resize", this.windowResize.bind(this), true);

    document.addEventListener("mousedown", this.changeFocus.bind(this), true);
    document.addEventListener("focus", this.changeFocus.bind(this), true);
    document.addEventListener("keydown", this.documentKeyDown.bind(this), true);
    document.addEventListener("beforecopy", this.documentCanCopy.bind(this), true);
    document.addEventListener("copy", this.documentCopy.bind(this), true);

    document.getElementById("back").title = WebInspector.UIString("Show previous panel.");
    document.getElementById("forward").title = WebInspector.UIString("Show next panel.");

    document.getElementById("search").setAttribute("placeholder", WebInspector.UIString("Search"));

    document.getElementById("back").addEventListener("click", this.back.bind(this), true);
    document.getElementById("forward").addEventListener("click", this.forward.bind(this), true);
    this.updateBackForwardButtons();

    document.getElementById("attachToggle").addEventListener("click", this.toggleAttach.bind(this), true);

    document.getElementById("sidebarResizeWidget").addEventListener("mousedown", this.sidebarResizerDragStart, true);
    document.getElementById("sidebarResizer").addEventListener("mousedown", this.sidebarResizerDragStart, true);
    document.getElementById("searchResultsResizer").addEventListener("mousedown", this.searchResultsResizerDragStart, true);

    if (platform === "mac-leopard")
        document.getElementById("toolbar").addEventListener("mousedown", this.toolbarDragStart, true);

    document.body.addStyleClass("detached");

    InspectorController.loaded();
}

var windowLoaded = function()
{
    var localizedStringsURL = InspectorController.localizedStringsURL();
    if (localizedStringsURL) {
        var localizedStringsScriptElement = document.createElement("script");
        localizedStringsScriptElement.addEventListener("load", WebInspector.loaded.bind(WebInspector), false);
        localizedStringsScriptElement.type = "text/javascript";
        localizedStringsScriptElement.src = localizedStringsURL;
        document.getElementsByTagName("head").item(0).appendChild(localizedStringsScriptElement);
    } else
        WebInspector.loaded();

    window.removeEventListener("load", windowLoaded, false);
    delete windowLoaded;
};

window.addEventListener("load", windowLoaded, false);

WebInspector.windowUnload = function(event)
{
    InspectorController.windowUnloading();
}

WebInspector.windowResize = function(event)
{
    if (this.currentPanel && this.currentPanel.resize)
        this.currentPanel.resize();
}

WebInspector.windowFocused = function(event)
{
    if (event.target.nodeType === Node.DOCUMENT_NODE)
        document.body.removeStyleClass("inactive");
}

WebInspector.windowBlured = function(event)
{
    if (event.target.nodeType === Node.DOCUMENT_NODE)
        document.body.addStyleClass("inactive");
}

WebInspector.changeFocus = function(event)
{
    var nextFocusElement;

    var current = event.target;
    while (current) {
        if (current.nodeName.toLowerCase() === "input")
            nextFocusElement = current;
        current = current.parentNode;
    }

    if (!nextFocusElement)
        nextFocusElement = event.target.firstParentWithClass("focusable");

    this.currentFocusElement = nextFocusElement;
}

WebInspector.documentClick = function(event)
{
    var anchor = event.target.firstParentOrSelfWithNodeName("a");
    if (!anchor || !anchor.hasStyleClass("webkit-html-resource-link"))
        return;

    if (WebInspector.showResourceForURL(anchor.getAttribute("href"))) {
        event.preventDefault();
        event.stopPropagation();
    }
}

WebInspector.documentKeyDown = function(event)
{
    if (!this.currentFocusElement)
        return;
    if (this.currentFocusElement.handleKeyEvent)
        this.currentFocusElement.handleKeyEvent(event);
    else if (this.currentFocusElement.id && this.currentFocusElement.id.length && WebInspector[this.currentFocusElement.id + "KeyDown"])
        WebInspector[this.currentFocusElement.id + "KeyDown"](event);
}

WebInspector.documentCanCopy = function(event)
{
    if (!this.currentFocusElement)
        return;
    // Calling preventDefault() will say "we support copying, so enable the Copy menu".
    if (this.currentFocusElement.handleCopyEvent)
        event.preventDefault();
    else if (this.currentFocusElement.id && this.currentFocusElement.id.length && WebInspector[this.currentFocusElement.id + "Copy"])
        event.preventDefault();
}

WebInspector.documentCopy = function(event)
{
    if (!this.currentFocusElement)
        return;
    if (this.currentFocusElement.handleCopyEvent)
        this.currentFocusElement.handleCopyEvent(event);
    else if (this.currentFocusElement.id && this.currentFocusElement.id.length && WebInspector[this.currentFocusElement.id + "Copy"])
        WebInspector[this.currentFocusElement.id + "Copy"](event);
}

WebInspector.sidebarKeyDown = function(event)
{
    var nextSelectedElement;

    if (this.fileOutline.selectedTreeElement) {
        if (!this.fileOutline.handleKeyEvent(event) && event.keyIdentifier === "Down" && !event.altKey) {
            var nextSelectedElement = this.statusOutline.children[0];
            while (nextSelectedElement && !nextSelectedElement.selectable)
                nextSelectedElement = nextSelectedElement.traverseNextTreeElement(false);
        }
    } else if (this.statusOutline.selectedTreeElement) {
        if (!this.statusOutline.handleKeyEvent(event) && event.keyIdentifier === "Up" && !event.altKey) {
            var nextSelectedElement = this.fileOutline.children[0];
            var lastSelectable = null;

            while (nextSelectedElement) {
                if (nextSelectedElement.selectable)
                    lastSelectable = nextSelectedElement;
                nextSelectedElement = nextSelectedElement.traverseNextTreeElement(false);
            }

            nextSelectedElement = lastSelectable;
        }
    }

    if (nextSelectedElement) {
        nextSelectedElement.reveal();
        nextSelectedElement.select();

        event.preventDefault();
        event.stopPropagation();
    }
}

WebInspector.sidebarCopy = function(event)
{
    event.clipboardData.clearData();
    event.preventDefault();

    var selectedElement = this.fileOutline.selectedTreeElement;
    if (!selectedElement || !selectedElement.representedObject || !selectedElement.representedObject.url)
        return;

    event.clipboardData.setData("URL", this.fileOutline.selectedTreeElement.representedObject.url);
}

WebInspector.mainKeyDown = function(event)
{
    if (this.currentPanel && this.currentPanel.handleKeyEvent)
        this.currentPanel.handleKeyEvent(event);
}

WebInspector.mainCopy = function(event)
{
    if (this.currentPanel && this.currentPanel.handleCopyEvent)
        this.currentPanel.handleCopyEvent(event);
}

WebInspector.searchResultsKeyDown = function(event)
{
    if (this.searchResultsTree)
        this.searchResultsTree.handleKeyEvent(event);
}

WebInspector.animateStyle = function(animations, duration, callback, complete)
{
    if (complete === undefined)
        complete = 0;
    var slice = (1000 / 30); // 30 frames per second

    var defaultUnit = "px";
    var propertyUnit = {opacity: ""};

    for (var i = 0; i < animations.length; ++i) {
        var animation = animations[i];
        var element = null;
        var start = null;
        var current = null;
        var end = null;
        for (key in animation) {
            if (key === "element")
                element = animation[key];
            else if (key === "start")
                start = animation[key];
            else if (key === "current")
                current = animation[key];
            else if (key === "end")
                end = animation[key];
        }

        if (!element || !end)
            continue;

        var computedStyle = element.ownerDocument.defaultView.getComputedStyle(element);
        if (!start) {
            start = {};
            for (key in end)
                start[key] = parseInt(computedStyle.getPropertyValue(key));
            animation.start = start;
        } else if (complete == 0)
            for (key in start)
                element.style.setProperty(key, start[key] + (key in propertyUnit ? propertyUnit[key] : defaultUnit));

        if (!current) {
            current = {};
            for (key in start)
                current[key] = start[key];
            animation.current = current;
        }

        function cubicInOut(t, b, c, d)
        {
            if ((t/=d/2) < 1) return c/2*t*t*t + b;
            return c/2*((t-=2)*t*t + 2) + b;
        }

        var style = element.style;
        for (key in end) {
            var startValue = start[key];
            var currentValue = current[key];
            var endValue = end[key];
            if ((complete + slice) < duration) {
                var delta = (endValue - startValue) / (duration / slice);
                var newValue = cubicInOut(complete, startValue, endValue - startValue, duration);
                style.setProperty(key, newValue + (key in propertyUnit ? propertyUnit[key] : defaultUnit));
                current[key] = newValue;
            } else {
                style.setProperty(key, endValue + (key in propertyUnit ? propertyUnit[key] : defaultUnit));
            }
        }
    }

    if (complete < duration)
        setTimeout(WebInspector.animateStyle, slice, animations, duration, callback, complete + slice);
    else if (callback)
        callback();
}

WebInspector.toggleAttach = function()
{
    this.attached = !this.attached;
}

WebInspector.toolbarDragStart = function(event)
{
    var toolbar = document.getElementById("toolbar");
    if (event.target !== toolbar || WebInspector.attached)
        return;

    toolbar.lastScreenX = event.screenX;
    toolbar.lastScreenY = event.screenY;

    document.addEventListener("mousemove", WebInspector.toolbarDrag, true);
    document.addEventListener("mouseup", WebInspector.toolbarDragEnd, true);
    document.body.style.cursor = "default";

    event.preventDefault();
}

WebInspector.toolbarDragEnd = function(event)
{
    var toolbar = document.getElementById("toolbar");
    delete toolbar.lastScreenX;
    delete toolbar.lastScreenY;

    document.removeEventListener("mousemove", WebInspector.toolbarDrag, true);
    document.removeEventListener("mouseup", WebInspector.toolbarDragEnd, true);
    document.body.style.removeProperty("cursor");

    event.preventDefault();
}

WebInspector.toolbarDrag = function(event)
{
    var toolbar = document.getElementById("toolbar");

    var x = event.screenX - toolbar.lastScreenX;
    var y = event.screenY - toolbar.lastScreenY;

    toolbar.lastScreenX = event.screenX;
    toolbar.lastScreenY = event.screenY;

    // We cannot call window.moveBy here because it restricts the movement of the window
    // at the edges.
    InspectorController.moveByUnrestricted(x, y);

    event.preventDefault();
}

WebInspector.sidebarResizerDragStart = function(event)
{
    WebInspector.elementDragStart(document.getElementById("sidebar"), WebInspector.sidebarResizerDrag, WebInspector.sidebarResizerDragEnd, event, "col-resize");
}

WebInspector.sidebarResizerDragEnd = function(event)
{
    WebInspector.elementDragEnd(document.getElementById("sidebar"), WebInspector.sidebarResizerDrag, WebInspector.sidebarResizerDragEnd, event);
}

WebInspector.sidebarResizerDrag = function(event)
{
    var x = event.pageX;

    // FIXME: We can should come up with a better hueristic for constraining the size of the sidebar.
    var newWidth = Number.constrain(x, 100, window.innerWidth - 100);

    document.getElementById("sidebar").style.width = newWidth + "px";
    document.getElementById("sidebarResizer").style.left = (newWidth - 3) + "px";
    document.getElementById("main").style.left = newWidth + "px";
    document.getElementById("toolbarButtons").style.left = newWidth + "px";

    if (WebInspector.currentPanel && WebInspector.currentPanel.resize)
        WebInspector.currentPanel.resize();

    event.preventDefault();
}

WebInspector.searchResultsResizerDragStart = function(event)
{
    WebInspector.elementDragStart(document.getElementById("searchResults"), WebInspector.searchResultsResizerDrag, WebInspector.searchResultsResizerDragEnd, event, "row-resize");
}

WebInspector.searchResultsResizerDragEnd = function(event)
{
    WebInspector.elementDragEnd(document.getElementById("searchResults"), WebInspector.searchResultsResizerDrag, WebInspector.searchResultsResizerDragEnd, event);
}

WebInspector.searchResultsResizerDrag = function(event)
{
    var y = event.pageY - document.getElementById("main").offsetTop;
    var newHeight = Number.constrain(y, 100, window.innerHeight - 100);

    WebInspector.searchResultsHeight = newHeight;

    document.getElementById("searchResults").style.height = WebInspector.searchResultsHeight + "px";
    document.getElementById("panels").style.top = newHeight + "px";
    document.getElementById("searchResultsResizer").style.top = (newHeight - 3) + "px";

    event.preventDefault();
}

WebInspector.elementDragStart = function(element, dividerDrag, elementDragEnd, event, cursor) 
{
    if (WebInspector.draggingElement)
        return elementDragEnd(event);

    WebInspector.draggingElement = true;

    document.addEventListener("mousemove", dividerDrag, true);
    document.addEventListener("mouseup", elementDragEnd, true);
    document.body.style.cursor = cursor;

    event.preventDefault();
}

WebInspector.elementDragEnd = function(element, dividerDrag, elementDragEnd, event) 
{
    document.removeEventListener("mousemove", dividerDrag, true);
    document.removeEventListener("mouseup", elementDragEnd, true);
    document.body.style.removeProperty("cursor");

    delete WebInspector.draggingElement;

    event.preventDefault();
}

WebInspector.back = function()
{
    if (this.currentBackForwardIndex <= 0) {
        console.error("Can't go back from index " + this.currentBackForwardIndex);
        return;
    }

    this.navigateToPanel(this.backForwardList[--this.currentBackForwardIndex], null, true);
}

WebInspector.forward = function()
{
    if (this.currentBackForwardIndex >= this.backForwardList.length - 1) {
        console.error("Can't go forward from index " + this.currentBackForwardIndex);
        return;
    }

    this.navigateToPanel(this.backForwardList[++this.currentBackForwardIndex], null, true);
}

WebInspector.updateBackForwardButtons = function()
{
    var index = this.currentBackForwardIndex;

    document.getElementById("back").disabled = index <= 0;
    document.getElementById("forward").disabled = index >= this.backForwardList.length - 1;
}

WebInspector.showConsole = function()
{
    this.navigateToPanel(WebInspector.consolePanel);
}

WebInspector.showTimeline = function()
{
    this.navigateToPanel(WebInspector.networkPanel);
}

WebInspector.addResource = function(resource)
{
    this.resources.push(resource);

    if (resource.mainResource)
        this.mainResource = resource;

    if (resource.url) {
        this.resourceURLMap[resource.url] = resource;
        this.networkPanel.addResourceToTimeline(resource);
    }
}

WebInspector.removeResource = function(resource)
{
    resource.detach();

    resource.category.removeResource(resource);

    if (resource.url)
        delete this.resourceURLMap[resource.url];

    var resourcesLength = this.resources.length;
    for (var i = 0; i < resourcesLength; ++i) {
        if (this.resources[i] === resource) {
            this.resources.splice(i, 1);
            break;
        }
    }
}

WebInspector.clearResources = function()
{
    for (var category in this.resourceCategories)
        this.resourceCategories[category].removeAllResources();
    this.resources = [];
    this.backForwardList = [];
    this.currentBackForwardIndex = -1;
    delete this.mainResource;
}

WebInspector.clearDatabaseResources = function()
{
    this.resourceCategories.databases.removeAllResources();
}

WebInspector.resourceURLChanged = function(resource, oldURL)
{
    delete this.resourceURLMap[oldURL];
    this.resourceURLMap[resource.url] = resource;
}

WebInspector.addMessageToConsole = function(msg)
{
    this.consolePanel.addMessage(msg);
    switch (msg.level) {
        case WebInspector.ConsoleMessage.MessageLevel.Warning:
            ++this.consoleListItem.warnings;
            break;
        case WebInspector.ConsoleMessage.MessageLevel.Error:
            ++this.consoleListItem.errors;
            break;
    }
}

WebInspector.clearConsoleMessages = function()
{
    this.consolePanel.clearMessages();
    this.consoleListItem.warnings = this.consoleListItem.errors = 0;
}

WebInspector.clearNetworkTimeline = function()
{
    if (this._networkPanel)
        this._networkPanel.clearTimeline();
}

WebInspector.drawLoadingPieChart = function(canvas, percent) {
    var g = canvas.getContext("2d");
    var darkColor = "rgb(122, 168, 218)";
    var lightColor = "rgb(228, 241, 251)";
    var cx = 8;
    var cy = 8;
    var r = 7;

    g.beginPath();
    g.arc(cx, cy, r, 0, Math.PI * 2, false); 
    g.closePath();

    g.lineWidth = 1;
    g.strokeStyle = darkColor;
    g.fillStyle = lightColor;
    g.fill();
    g.stroke();

    var startangle = -Math.PI / 2;
    var endangle = startangle + (percent * Math.PI * 2);

    g.beginPath();
    g.moveTo(cx, cy);
    g.arc(cx, cy, r, startangle, endangle, false); 
    g.closePath();

    g.fillStyle = darkColor;
    g.fill();
}

WebInspector.updateFocusedNode = function(node)
{
    if (!node)
        // FIXME: Should we deselect if null is passed in?
        return;

    for (var i = 0; i < this.resourceCategories.documents.resources.length; ++i) {
        var resource = this.resourceCategories.documents.resources[i];
        if (resource.documentNode !== node.ownerDocument)
            continue;

        this.navigateToPanel(resource.panel, "dom");
        resource.panel.focusedDOMNode = node;

        this.currentFocusElement = document.getElementById("main");

        break;
    }
}

WebInspector.resourceForURL = function(url)
{
    for (var resourceURL in this.resourceURLMap) {
        if (resourceURL.hasSubstring(url))
            return this.resourceURLMap[resourceURL];
    }

    return null;
}

WebInspector.showResourceForURL = function(url)
{
    var resource = this.resourceForURL(url);
    if (!resource)
        return false;

    this.navigateToResource(resource);
    return true;
}

WebInspector.linkifyURL = function(url, linkText, classes, isExternal)
{
    if (linkText === undefined)
        linkText = url.escapeHTML();
    classes = (classes === undefined) ? "" : classes + " ";
    classes += isExternal ? "webkit-html-external-link" : "webkit-html-resource-link";
    var link = "<a href=\"" + url + "\" class=\"" + classes + "\" title=\"" + url + "\" target=\"_blank\">" + linkText + "</a>";
    return link;
}

WebInspector.addMainEventListeners = function(doc)
{
    doc.defaultView.addEventListener("focus", function(event) { WebInspector.windowFocused(event) }, true);
    doc.defaultView.addEventListener("blur", function(event) { WebInspector.windowBlured(event) }, true);
    doc.addEventListener("click", function(event) { WebInspector.documentClick(event) }, true);
}

WebInspector.performSearch = function(query)
{
    if (!query || !query.length) {
        this.showingSearchResults = false;
        return;
    }

    var resultsContainer = document.getElementById("searchResults");
    resultsContainer.removeChildren();

    var isXPath = query.indexOf("/") !== -1;

    var xpathQuery;
    if (isXPath)
        xpathQuery = query;
    else {
        var escapedQuery = query.escapeCharacters("'");
        xpathQuery = "//*[contains(name(),'" + escapedQuery + "') or contains(@*,'" + escapedQuery + "')] | //text()[contains(.,'" + escapedQuery + "')] | //comment()[contains(.,'" + escapedQuery + "')]";
    }

    var resourcesToSearch = [].concat(this.resourceCategories.documents.resources, this.resourceCategories.stylesheets.resources, this.resourceCategories.scripts.resources, this.resourceCategories.other.resources);

    var files = [];
    for (var i = 0; i < resourcesToSearch.length; ++i) {
        var resource = resourcesToSearch[i];

        var sourceResults = [];
        if (!isXPath && "source" in resource.panel.views) {
            resource.panel.setupSourceFrameIfNeeded();
            sourceResults = InspectorController.search(resource.panel.views.source.frameElement.contentDocument, query);
        }

        var domResults = [];
        const searchResultsProperty = "__includedInInspectorSearchResults";
        function addNodesToDOMResults(nodes, length, getItem)
        {
            for (var i = 0; i < length; ++i) {
                var node = getItem(nodes, i);
                if (searchResultsProperty in node)
                    continue;
                node[searchResultsProperty] = true;
                domResults.push(node);
            }
        }

        function cleanUpDOMResultsNodes()
        {
            for (var i = 0; i < domResults.length; ++i)
                delete domResults[i][searchResultsProperty];
        }

        if (resource.category === this.resourceCategories.documents) {
            var doc = resource.documentNode;
            try {
                var result = Document.prototype.evaluate.call(doc, xpathQuery, doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE);
                addNodesToDOMResults(result, result.snapshotLength, function(l, i) { return l.snapshotItem(i); });
            } catch(err) {
                // ignore any exceptions. the query might be malformed, but we allow that.
            }

            var result = Document.prototype.querySelectorAll.call(doc, query);
            addNodesToDOMResults(result, result.length, function(l, i) { return l.item(i); });

            cleanUpDOMResultsNodes();
        }

        if ((!sourceResults || !sourceResults.length) && !domResults.length)
            continue;

        files.push({resource: resource, sourceResults: sourceResults, domResults: domResults});
    }

    if (!files.length)
        return;

    this.showingSearchResults = true;

    var fileList = document.createElement("ol");
    fileList.className = "outline-disclosure";
    resultsContainer.appendChild(fileList);

    this.searchResultsTree = new TreeOutline(fileList);
    this.searchResultsTree.expandTreeElementsWhenArrowing = true;

    var sourceResultSelected = function(element)
    {
        var selection = window.getSelection();
        selection.removeAllRanges();
        selection.addRange(element.representedObject.range);

        WebInspector.navigateToPanel(element.representedObject.panel, "source");
        element.representedObject.line.scrollIntoView(true);
        resultsContainer.scrollToElement(element.listItemElement);
    }

    var domResultSelected = function(element)
    {
        WebInspector.navigateToPanel(element.representedObject.panel, "dom");
        element.representedObject.panel.focusedDOMNode = element.representedObject.node;
        resultsContainer.scrollToElement(element.listItemElement);
    }

    for (var i = 0; i < files.length; ++i) {
        var file = files[i];

        var fileItem = new TreeElement(file.resource.displayName, {}, true);
        fileItem.expanded = true;
        fileItem.selectable = false;
        this.searchResultsTree.appendChild(fileItem);

        if (file.sourceResults && file.sourceResults.length) {
            for (var j = 0; j < file.sourceResults.length; ++j) {
                var range = file.sourceResults[j];

                var line = range.startContainer;
                while (line.parentNode && line.nodeName.toLowerCase() != "tr")
                    line = line.parentNode;
                var lineRange = file.resource.panel.views.source.frameElement.contentDocument.createRange();
                lineRange.selectNodeContents(line);

                // Don't include any error bubbles in the search result
                var end = line.lastChild.lastChild;
                if (end.nodeName.toLowerCase() == "div" && end.hasStyleClass("webkit-html-message-bubble")) {
                    while (end && end.nodeName.toLowerCase() == "div" && end.hasStyleClass("webkit-html-message-bubble"))
                        end = end.previousSibling;
                    lineRange.setEndAfter(end);
                }

                var beforeRange = file.resource.panel.views.source.frameElement.contentDocument.createRange();
                beforeRange.setStart(lineRange.startContainer, lineRange.startOffset);
                beforeRange.setEnd(range.startContainer, range.startOffset);

                var afterRange = file.resource.panel.views.source.frameElement.contentDocument.createRange();
                afterRange.setStart(range.endContainer, range.endOffset);
                afterRange.setEnd(lineRange.endContainer, lineRange.endOffset);

                var beforeText = beforeRange.toString().trimLeadingWhitespace();
                var text = range.toString();
                var afterText = afterRange.toString().trimTrailingWhitespace();

                var length = beforeText.length + text.length + afterText.length;
                if (length > Preferences.maxTextSearchResultLength) {
                    var beforeAfterLength = (Preferences.maxTextSearchResultLength - text.length) / 2;
                    if (beforeText.length > beforeAfterLength)
                        beforeText = "\u2026" + beforeText.substr(-beforeAfterLength);
                    if (afterText.length > beforeAfterLength)
                        afterText = afterText.substr(0, beforeAfterLength) + "\u2026";
                }

                var title = "<div class=\"selection selected\"></div>";
                if (j == 0)
                    title += "<div class=\"search-results-section\">" + WebInspector.UIString("Source") + "</div>";
                title += beforeText.escapeHTML() + "<span class=\"search-matched-string\">" + text.escapeHTML() + "</span>" + afterText.escapeHTML();
                var item = new TreeElement(title, {panel: file.resource.panel, line: line, range: range}, false);
                item.onselect = sourceResultSelected;
                fileItem.appendChild(item);
            }
        }

        if (file.domResults.length) {
            for (var j = 0; j < file.domResults.length; ++j) {
                var node = file.domResults[j];
                var title = "<div class=\"selection selected\"></div>";
                if (j == 0)
                    title += "<div class=\"search-results-section\">" + WebInspector.UIString("DOM") + "</div>";
                title += nodeTitleInfo.call(node).title;
                var item = new TreeElement(title, {panel: file.resource.panel, node: node}, false);
                item.onselect = domResultSelected;
                fileItem.appendChild(item);
            }
        }
    }
}

WebInspector.navigateToResource = function(resource)
{
    this.navigateToPanel(resource.panel);
}

WebInspector.navigateToPanel = function(panel, view, fromBackForwardAction)
{
    if (this.currentPanel === panel) {
        if (panel && view)
            panel.currentView = view;
        return;
    }

    if (!fromBackForwardAction) {
        var oldIndex = this.currentBackForwardIndex;
        if (oldIndex >= 0)
            this.backForwardList.splice(oldIndex + 1, this.backForwardList.length - oldIndex);
        this.currentBackForwardIndex++;
        this.backForwardList.push(panel);
    }

    this.currentPanel = panel;
    if (panel && view)
        panel.currentView = view;
}

WebInspector.UIString = function(string)
{
    if (string in this.localizedStrings)
        string = this.localizedStrings[string];
    else {
        if (!(string in this.missingLocalizedStrings)) {
            console.error("Localized string \"" + string + "\" not found.");
            this.missingLocalizedStrings[string] = true;
        }

        if (Preferences.showMissingLocalizedStrings)
            string += " (not localized)";
    }

    return String.vsprintf(string, Array.prototype.slice.call(arguments, 1));
}

WebInspector.StatusTreeElement = function(title, iconClass, panel)
{
    TreeElement.call(this, "<span class=\"title only\">" + title + "</span><span class=\"icon " + iconClass + "\"></span>", null, false);
    this.panel = panel;
}

WebInspector.StatusTreeElement.prototype = {
    onselect: function()
    {
        var selectedElement = WebInspector.fileOutline.selectedTreeElement;
        if (selectedElement)
            selectedElement.deselect();
        if (this.panel)
            WebInspector.navigateToPanel(this.panel);
    },

    ondeselect: function()
    {
        if (this.panel)
            this.panel.hide();
    }
}

WebInspector.StatusTreeElement.prototype.__proto__ = TreeElement.prototype;

WebInspector.ConsoleStatusTreeElement = function(panel)
{
    WebInspector.StatusTreeElement.call(this, WebInspector.UIString("Console"), "console", panel);
}

WebInspector.ConsoleStatusTreeElement.prototype = {
    get warnings()
    {
        if (!("_warnings" in this))
            this._warnings = 0;

        return this._warnings;
    },

    set warnings(x)
    {
        if (this._warnings === x)
            return;

        this._warnings = x;

        this._updateTitle();
    },

    get errors()
    {
        if (!("_errors" in this))
            this._errors = 0;

        return this._errors;
    },

    set errors(x)
    {
        if (this._errors === x)
            return;

        this._errors = x;

        this._updateTitle();
    },

    _updateTitle: function()
    {
        var title = "<span class=\"title";
        if (!this.warnings && !this.errors)
            title += " only";
        title += "\">" + WebInspector.UIString("Console") + "</span><span class=\"icon console\"></span>";

        if (this.warnings || this.errors) {
            title += "<span class=\"info\">";
            if (this.errors) {
                title += this.errors + " error";
                if (this.errors > 1)
                    title += "s";
            }
            if (this.warnings) {
                if (this.errors)
                    title += ", ";
                title += this.warnings + " warning";
                if (this.warnings > 1)
                    title += "s";
            }
            title += "</span>";
        }

        this.title = title;
    }
}

WebInspector.ConsoleStatusTreeElement.prototype.__proto__ = WebInspector.StatusTreeElement.prototype;

// This table maps MIME types to the Resource.Types which are valid for them.
// The following line:
//    "text/html":                {0: 1},
// means that text/html is a valid MIME type for resources that have type
// WebInspector.Resource.Type.Document (which has a value of 0).
WebInspector.MIMETypes = {
    "text/html":                   {0: true},
    "text/xml":                    {0: true},
    "text/plain":                  {0: true},
    "application/xhtml+xml":       {0: true},
    "text/css":                    {1: true},
    "text/xsl":                    {1: true},
    "image/jpeg":                  {2: true},
    "image/png":                   {2: true},
    "image/gif":                   {2: true},
    "image/bmp":                   {2: true},
    "image/x-icon":                {2: true},
    "image/x-xbitmap":             {2: true},
    "font/ttf":                    {3: true},
    "font/opentype":               {3: true},
    "application/x-font-type1":    {3: true},
    "application/x-font-ttf":      {3: true},
    "application/x-truetype-font": {3: true},
    "text/javascript":             {4: true},
    "text/ecmascript":             {4: true},
    "application/javascript":      {4: true},
    "application/ecmascript":      {4: true},
    "application/x-javascript":    {4: true},
    "text/javascript1.1":          {4: true},
    "text/javascript1.2":          {4: true},
    "text/javascript1.3":          {4: true},
    "text/jscript":                {4: true},
    "text/livescript":             {4: true},
}