gecko/browser/devtools/inspector/InspectorPanel.jsm

718 lines
22 KiB
JavaScript
Raw Normal View History

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
this.EXPORTED_SYMBOLS = ["InspectorPanel"];
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
Cu.import("resource:///modules/devtools/EventEmitter.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MarkupView",
"resource:///modules/devtools/MarkupView.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Selection",
"resource:///modules/devtools/Selection.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "HTMLBreadcrumbs",
"resource:///modules/devtools/Breadcrumbs.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Highlighter",
"resource:///modules/devtools/Highlighter.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ToolSidebar",
"resource:///modules/devtools/Sidebar.jsm");
const LAYOUT_CHANGE_TIMER = 250;
/**
* Represents an open instance of the Inspector for a tab.
* The inspector controls the highlighter, the breadcrumbs,
* the markup view, and the sidebar (computed view, rule view
* and layout view).
*/
this.InspectorPanel = function InspectorPanel(iframeWindow, toolbox) {
this._toolbox = toolbox;
this._target = toolbox._target;
this.panelDoc = iframeWindow.document;
this.panelWin = iframeWindow;
this.panelWin.inspector = this;
EventEmitter.decorate(this);
}
InspectorPanel.prototype = {
/**
* open is effectively an asynchronous constructor
*/
open: function InspectorPanel_open() {
let deferred = Promise.defer();
this.preventNavigateAway = this.preventNavigateAway.bind(this);
this.onNavigatedAway = this.onNavigatedAway.bind(this);
this.target.on("will-navigate", this.preventNavigateAway);
this.target.on("navigate", this.onNavigatedAway);
this.nodemenu = this.panelDoc.getElementById("inspector-node-popup");
this.lastNodemenuItem = this.nodemenu.lastChild;
this._setupNodeMenu = this._setupNodeMenu.bind(this);
this._resetNodeMenu = this._resetNodeMenu.bind(this);
this.nodemenu.addEventListener("popupshowing", this._setupNodeMenu, true);
this.nodemenu.addEventListener("popuphiding", this._resetNodeMenu, true);
// Initialize the search related items
this.searchBox = this.panelDoc.getElementById("inspector-searchbox");
this._lastSearched = null;
this._searchResults = null;
this._searchIndex = 0;
this._onHTMLSearch = this._onHTMLSearch.bind(this);
this._onSearchKeypress = this._onSearchKeypress.bind(this);
this.searchBox.addEventListener("command", this._onHTMLSearch, true);
this.searchBox.addEventListener("keypress", this._onSearchKeypress, true);
// Create an empty selection
this._selection = new Selection();
this.onNewSelection = this.onNewSelection.bind(this);
this.selection.on("new-node", this.onNewSelection);
this.onBeforeNewSelection = this.onBeforeNewSelection.bind(this);
this.selection.on("before-new-node", this.onBeforeNewSelection);
this.onDetached = this.onDetached.bind(this);
this.selection.on("detached", this.onDetached);
this.breadcrumbs = new HTMLBreadcrumbs(this);
if (this.target.isLocalTab) {
this.browser = this.target.tab.linkedBrowser;
this.scheduleLayoutChange = this.scheduleLayoutChange.bind(this);
this.browser.addEventListener("resize", this.scheduleLayoutChange, true);
this.highlighter = new Highlighter(this.target, this, this._toolbox);
let button = this.panelDoc.getElementById("inspector-inspect-toolbutton");
button.hidden = false;
this.onLockStateChanged = function() {
if (this.highlighter.locked) {
button.removeAttribute("checked");
this._toolbox.raise();
} else {
button.setAttribute("checked", "true");
}
}.bind(this);
this.highlighter.on("locked", this.onLockStateChanged);
this.highlighter.on("unlocked", this.onLockStateChanged);
// Show a warning when the debugger is paused.
// We show the warning only when the inspector
// is selected.
this.updateDebuggerPausedWarning = function() {
let notificationBox = this._toolbox.getNotificationBox();
let notification = notificationBox.getNotificationWithValue("inspector-script-paused");
if (!notification && this._toolbox.currentToolId == "inspector" &&
this.target.isThreadPaused) {
let message = this.strings.GetStringFromName("debuggerPausedWarning.message");
notificationBox.appendNotification(message,
"inspector-script-paused", "", notificationBox.PRIORITY_WARNING_HIGH);
}
if (notification && this._toolbox.currentToolId != "inspector") {
notificationBox.removeNotification(notification);
}
if (notification && !this.target.isThreadPaused) {
notificationBox.removeNotification(notification);
}
}.bind(this);
this.target.on("thread-paused", this.updateDebuggerPausedWarning);
this.target.on("thread-resumed", this.updateDebuggerPausedWarning);
this._toolbox.on("select", this.updateDebuggerPausedWarning);
this.updateDebuggerPausedWarning();
}
this._initMarkup();
this.isReady = false;
this.once("markuploaded", function() {
this.isReady = true;
// All the components are initialized. Let's select a node.
if (this.target.isLocalTab) {
let root = this.browser.contentDocument.documentElement;
this._selection.setNode(root);
} else if (this.target.window) {
let root = this.target.window.document.documentElement;
this._selection.setNode(root);
}
if (this.highlighter) {
this.highlighter.unlock();
}
this.emit("ready");
deferred.resolve(this);
}.bind(this));
this.setupSidebar();
return deferred.promise;
},
/**
* Selection object (read only)
*/
get selection() {
return this._selection;
},
/**
* Target getter.
*/
get target() {
return this._target;
},
/**
* Target setter.
*/
set target(value) {
this._target = value;
},
/**
* Expose gViewSourceUtils so that other tools can make use of them.
*/
get viewSourceUtils() {
return this.panelWin.gViewSourceUtils;
},
/**
* Indicate that a tool has modified the state of the page. Used to
* decide whether to show the "are you sure you want to navigate"
* notification.
*/
markDirty: function InspectorPanel_markDirty() {
this.isDirty = true;
},
/**
* Build the sidebar.
*/
setupSidebar: function InspectorPanel_setupSidebar() {
let tabbox = this.panelDoc.querySelector("#inspector-sidebar");
this.sidebar = new ToolSidebar(tabbox, this);
let defaultTab = Services.prefs.getCharPref("devtools.inspector.activeSidebar");
this._setDefaultSidebar = function(event, toolId) {
Services.prefs.setCharPref("devtools.inspector.activeSidebar", toolId);
}.bind(this);
this.sidebar.on("select", this._setDefaultSidebar);
this.toggleHighlighter = this.toggleHighlighter.bind(this);
this.sidebar.addTab("ruleview",
"chrome://browser/content/devtools/cssruleview.xul",
"ruleview" == defaultTab);
this.sidebar.addTab("computedview",
"chrome://browser/content/devtools/csshtmltree.xul",
"computedview" == defaultTab);
this.sidebar.addTab("layoutview",
"chrome://browser/content/devtools/layoutview/view.xhtml",
"layoutview" == defaultTab);
let ruleViewTab = this.sidebar.getTab("ruleview");
ruleViewTab.addEventListener("mouseover", this.toggleHighlighter, false);
ruleViewTab.addEventListener("mouseout", this.toggleHighlighter, false);
this.sidebar.show();
},
/**
* Reset the inspector on navigate away.
*/
onNavigatedAway: function InspectorPanel_onNavigatedAway(event, newWindow) {
this.selection.setNode(null);
this._destroyMarkup();
this.isDirty = false;
let self = this;
function onDOMReady() {
newWindow.removeEventListener("DOMContentLoaded", onDOMReady, true);
if (!self.selection.node) {
self.selection.setNode(newWindow.document.documentElement);
}
self._initMarkup();
}
if (newWindow.document.readyState == "loading") {
newWindow.addEventListener("DOMContentLoaded", onDOMReady, true);
} else {
onDOMReady();
}
},
/**
* Show a message if the inspector is dirty.
*/
preventNavigateAway: function InspectorPanel_preventNavigateAway(event, request) {
if (!this.isDirty) {
return;
}
request.suspend();
let notificationBox = null;
if (this.target.isLocalTab) {
let gBrowser = this.target.tab.ownerDocument.defaultView.gBrowser;
notificationBox = gBrowser.getNotificationBox();
}
else {
notificationBox = this._toolbox.getNotificationBox();
}
let notification = notificationBox.
getNotificationWithValue("inspector-page-navigation");
if (notification) {
notificationBox.removeNotification(notification, true);
}
let cancelRequest = function onCancelRequest() {
if (request) {
request.cancel(Cr.NS_BINDING_ABORTED);
request.resume(); // needed to allow the connection to be cancelled.
request = null;
}
};
let eventCallback = function onNotificationCallback(event) {
if (event == "removed") {
cancelRequest();
}
};
let buttons = [
{
id: "inspector.confirmNavigationAway.buttonLeave",
label: this.strings.GetStringFromName("confirmNavigationAway.buttonLeave"),
accessKey: this.strings.GetStringFromName("confirmNavigationAway.buttonLeaveAccesskey"),
callback: function onButtonLeave() {
if (request) {
request.resume();
request = null;
}
}.bind(this),
},
{
id: "inspector.confirmNavigationAway.buttonStay",
label: this.strings.GetStringFromName("confirmNavigationAway.buttonStay"),
accessKey: this.strings.GetStringFromName("confirmNavigationAway.buttonStayAccesskey"),
callback: cancelRequest
},
];
let message = this.strings.GetStringFromName("confirmNavigationAway.message2");
notification = notificationBox.appendNotification(message,
"inspector-page-navigation", "chrome://browser/skin/Info.png",
notificationBox.PRIORITY_WARNING_HIGH, buttons, eventCallback);
// Make sure this not a transient notification, to avoid the automatic
// transient notification removal.
notification.persistence = -1;
},
/**
* When a new node is selected.
*/
onNewSelection: function InspectorPanel_onNewSelection() {
this.cancelLayoutChange();
},
/**
* When a new node is selected, before the selection has changed.
*/
onBeforeNewSelection: function InspectorPanel_onBeforeNewSelection(event,
node) {
if (this.breadcrumbs.indexOf(node) == -1) {
// only clear locks if we'd have to update breadcrumbs
this.clearPseudoClasses();
}
},
/**
* When a node is deleted, select its parent node.
*/
onDetached: function InspectorPanel_onDetached(event, parentNode) {
this.cancelLayoutChange();
this.breadcrumbs.cutAfter(this.breadcrumbs.indexOf(parentNode));
this.selection.setNode(parentNode, "detached");
},
/**
* Destroy the inspector.
*/
destroy: function InspectorPanel__destroy() {
if (this._destroyed) {
return Promise.resolve(null);
}
this._destroyed = true;
this.cancelLayoutChange();
if (this.browser) {
this.browser.removeEventListener("resize", this.scheduleLayoutChange, true);
this.browser = null;
}
this.target.off("will-navigate", this.preventNavigateAway);
this.target.off("navigate", this.onNavigatedAway);
if (this.highlighter) {
this.highlighter.off("locked", this.onLockStateChanged);
this.highlighter.off("unlocked", this.onLockStateChanged);
this.highlighter.destroy();
}
this.target.off("thread-paused", this.updateDebuggerPausedWarning);
this.target.off("thread-resumed", this.updateDebuggerPausedWarning);
this._toolbox.off("select", this.updateDebuggerPausedWarning);
this._toolbox = null;
this.sidebar.off("select", this._setDefaultSidebar);
this.sidebar.destroy();
this.sidebar = null;
this.nodemenu.removeEventListener("popupshowing", this._setupNodeMenu, true);
this.nodemenu.removeEventListener("popuphiding", this._resetNodeMenu, true);
this.searchBox.removeEventListener("command", this._onHTMLSearch, true);
this.searchBox.removeEventListener("keypress", this._onSearchKeypress, true);
this.breadcrumbs.destroy();
this.selection.off("new-node", this.onNewSelection);
this.selection.off("before-new-node", this.onBeforeNewSelection);
this.selection.off("detached", this.onDetached);
this._destroyMarkup();
this._selection.destroy();
this._selection = null;
this.panelWin.inspector = null;
this.target = null;
this.panelDoc = null;
this.panelWin = null;
this.breadcrumbs = null;
this.lastNodemenuItem = null;
this.nodemenu = null;
this.searchBox = null;
this.highlighter = null;
return Promise.resolve(null);
},
/**
* The command callback for the HTML search box. This function is
* automatically invoked as the user is typing.
*/
_onHTMLSearch: function InspectorPanel__onHTMLSearch() {
let query = this.searchBox.value;
if (query == this._lastSearched) {
return;
}
this._lastSearched = query;
this._searchIndex = 0;
if (query.length == 0) {
this.searchBox.removeAttribute("filled");
this.searchBox.classList.remove("devtools-no-search-result");
return;
}
this.searchBox.setAttribute("filled", true);
this._searchResults = this.browser.contentDocument.querySelectorAll(query);
if (this._searchResults.length > 0) {
this.searchBox.classList.remove("devtools-no-search-result");
this.cancelLayoutChange();
this.selection.setNode(this._searchResults[0]);
} else {
this.searchBox.classList.add("devtools-no-search-result");
}
},
/**
* Search for the search box value as a query selector.
*/
_onSearchKeypress: function InspectorPanel__onSearchKeypress(aEvent) {
let query = this.searchBox.value;
switch(aEvent.keyCode) {
case aEvent.DOM_VK_ENTER:
case aEvent.DOM_VK_RETURN:
if (query == this._lastSearched) {
this._searchIndex = (this._searchIndex + 1) % this._searchResults.length;
} else {
this._onHTMLSearch();
return;
}
break;
case aEvent.DOM_VK_UP:
if (--this._searchIndex < 0) {
this._searchIndex = this._searchResults.length - 1;
}
break;
case aEvent.DOM_VK_DOWN:
this._searchIndex = (this._searchIndex + 1) % this._searchResults.length;
break;
default:
return;
}
aEvent.preventDefault();
aEvent.stopPropagation();
this.cancelLayoutChange();
if (this._searchResults.length > 0) {
this.selection.setNode(this._searchResults[this._searchIndex]);
}
},
/**
* Show the node menu.
*/
showNodeMenu: function InspectorPanel_showNodeMenu(aButton, aPosition, aExtraItems) {
if (aExtraItems) {
for (let item of aExtraItems) {
this.nodemenu.appendChild(item);
}
}
this.nodemenu.openPopup(aButton, aPosition, 0, 0, true, false);
},
hideNodeMenu: function InspectorPanel_hideNodeMenu() {
this.nodemenu.hidePopup();
},
/**
* Disable the delete item if needed. Update the pseudo classes.
*/
_setupNodeMenu: function InspectorPanel_setupNodeMenu() {
// Set the pseudo classes
for (let name of ["hover", "active", "focus"]) {
let menu = this.panelDoc.getElementById("node-menu-pseudo-" + name);
let checked = DOMUtils.hasPseudoClassLock(this.selection.node, ":" + name);
menu.setAttribute("checked", checked);
}
// Disable delete item if needed
let deleteNode = this.panelDoc.getElementById("node-menu-delete");
if (this.selection.isRoot()) {
deleteNode.setAttribute("disabled", "true");
} else {
deleteNode.removeAttribute("disabled");
}
},
_resetNodeMenu: function InspectorPanel_resetNodeMenu() {
// Remove any extra items
while (this.lastNodemenuItem.nextSibling) {
let toDelete = this.lastNodemenuItem.nextSibling;
toDelete.parentNode.removeChild(toDelete);
}
},
_initMarkup: function InspectorPanel_initMarkup() {
let doc = this.panelDoc;
this._markupBox = doc.getElementById("markup-box");
// create tool iframe
this._markupFrame = doc.createElement("iframe");
this._markupFrame.setAttribute("flex", "1");
this._markupFrame.setAttribute("tooltip", "aHTMLTooltip");
this._markupFrame.setAttribute("context", "inspector-node-popup");
// This is needed to enable tooltips inside the iframe document.
this._boundMarkupFrameLoad = function InspectorPanel_initMarkupPanel_onload() {
this._markupFrame.contentWindow.focus();
this._onMarkupFrameLoad();
}.bind(this);
this._markupFrame.addEventListener("load", this._boundMarkupFrameLoad, true);
this._markupBox.setAttribute("hidden", true);
this._markupBox.appendChild(this._markupFrame);
this._markupFrame.setAttribute("src", "chrome://browser/content/devtools/markup-view.xhtml");
},
_onMarkupFrameLoad: function InspectorPanel__onMarkupFrameLoad() {
this._markupFrame.removeEventListener("load", this._boundMarkupFrameLoad, true);
delete this._boundMarkupFrameLoad;
this._markupBox.removeAttribute("hidden");
let controllerWindow = this._toolbox.doc.defaultView;
this.markup = new MarkupView(this, this._markupFrame, controllerWindow);
this.emit("markuploaded");
},
_destroyMarkup: function InspectorPanel__destroyMarkup() {
if (this._boundMarkupFrameLoad) {
this._markupFrame.removeEventListener("load", this._boundMarkupFrameLoad, true);
delete this._boundMarkupFrameLoad;
}
if (this.markup) {
this.markup.destroy();
delete this.markup;
}
if (this._markupFrame) {
this._markupFrame.parentNode.removeChild(this._markupFrame);
delete this._markupFrame;
}
},
/**
* Toggle a pseudo class.
*/
togglePseudoClass: function InspectorPanel_togglePseudoClass(aPseudo) {
if (this.selection.isElementNode()) {
if (DOMUtils.hasPseudoClassLock(this.selection.node, aPseudo)) {
this.breadcrumbs.nodeHierarchy.forEach(function(crumb) {
DOMUtils.removePseudoClassLock(crumb.node, aPseudo);
});
} else {
let hierarchical = aPseudo == ":hover" || aPseudo == ":active";
let node = this.selection.node;
do {
DOMUtils.addPseudoClassLock(node, aPseudo);
node = node.parentNode;
} while (hierarchical && node.parentNode)
}
}
this.selection.emit("pseudoclass");
},
/**
* Clear any pseudo-class locks applied to the current hierarchy.
*/
clearPseudoClasses: function InspectorPanel_clearPseudoClasses() {
this.breadcrumbs.nodeHierarchy.forEach(function(crumb) {
DOMUtils.clearPseudoClassLocks(crumb.node);
});
},
/**
* Toggle the highlighter when ruleview is hovered.
*/
toggleHighlighter: function InspectorPanel_toggleHighlighter(event)
{
if (event.type == "mouseover") {
this.highlighter.hide();
}
else if (event.type == "mouseout") {
this.highlighter.show();
}
},
/**
* Copy the innerHTML of the selected Node to the clipboard.
*/
copyInnerHTML: function InspectorPanel_copyInnerHTML()
{
if (!this.selection.isNode()) {
return;
}
let toCopy = this.selection.node.innerHTML;
if (toCopy) {
clipboardHelper.copyString(toCopy);
}
},
/**
* Copy the outerHTML of the selected Node to the clipboard.
*/
copyOuterHTML: function InspectorPanel_copyOuterHTML()
{
if (!this.selection.isNode()) {
return;
}
let toCopy = this.selection.node.outerHTML;
if (toCopy) {
clipboardHelper.copyString(toCopy);
}
},
/**
* Delete the selected node.
*/
deleteNode: function IUI_deleteNode() {
if (!this.selection.isNode() ||
this.selection.isRoot()) {
return;
}
let toDelete = this.selection.node;
let parent = this.selection.node.parentNode;
// If the markup panel is active, use the markup panel to delete
// the node, making this an undoable action.
if (this.markup) {
this.markup.deleteNode(toDelete);
} else {
// remove the node from content
parent.removeChild(toDelete);
}
},
/**
* Schedule a low-priority change event for things like paint
* and resize.
*/
scheduleLayoutChange: function Inspector_scheduleLayoutChange()
{
if (this._timer) {
return null;
}
this._timer = this.panelWin.setTimeout(function() {
this.emit("layout-change");
this._timer = null;
}.bind(this), LAYOUT_CHANGE_TIMER);
},
/**
* Cancel a pending low-priority change event if any is
* scheduled.
*/
cancelLayoutChange: function Inspector_cancelLayoutChange()
{
if (this._timer) {
this.panelWin.clearTimeout(this._timer);
delete this._timer;
}
},
}
/////////////////////////////////////////////////////////////////////////
//// Initializers
XPCOMUtils.defineLazyGetter(InspectorPanel.prototype, "strings",
function () {
return Services.strings.createBundle(
"chrome://browser/locale/devtools/inspector.properties");
});
XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() {
return Cc["@mozilla.org/widget/clipboardhelper;1"].
getService(Ci.nsIClipboardHelper);
});
XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () {
return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
});