author | Dave Camp <dcamp@mozilla.com> |
Fri, 12 Apr 2013 08:07:34 -0700 | |
changeset 128616 | 81089099bb0456c1f9922bc7b6c7ca8e1594bf53 |
parent 128611 | cfca520dc6f565f1d630eb0a8a6eb07a050f20eb |
child 128617 | b32114b813c5129945483c85d18cceaa1b3414de |
push id | 24533 |
push user | ryanvm@gmail.com |
push date | Fri, 12 Apr 2013 21:36:19 +0000 |
treeherder | mozilla-central@e818c908825a [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
bugs | 855914 |
milestone | 23.0a1 |
backs out | 643194ceabe44f115216587c32f2ca762a40217c |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/browser/base/content/nsContextMenu.js +++ b/browser/base/content/nsContextMenu.js @@ -432,19 +432,20 @@ nsContextMenu.prototype = { initClickToPlayItems: function() { this.showItem("context-ctp-play", this.onCTPPlugin); this.showItem("context-ctp-hide", this.onCTPPlugin); this.showItem("context-sep-ctp", this.onCTPPlugin); }, inspectNode: function CM_inspectNode() { - let {devtools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}); let gBrowser = this.browser.ownerDocument.defaultView.gBrowser; - let tt = devtools.TargetFactory.forTab(gBrowser.selectedTab); + let imported = {}; + Cu.import("resource:///modules/devtools/Target.jsm", imported); + let tt = imported.TargetFactory.forTab(gBrowser.selectedTab); return gDevTools.showToolbox(tt, "inspector").then(function(toolbox) { let inspector = toolbox.getCurrentPanel(); inspector.selection.setNode(this.target, "browser-context-menu"); }.bind(this)); }, // Set various context menu attributes based on the state of the world. setTarget: function (aNode, aRangeParent, aRangeOffset) {
deleted file mode 100644 --- a/browser/devtools/Makefile.in +++ /dev/null @@ -1,15 +0,0 @@ -# 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/. - -DEPTH = @DEPTH@ -topsrcdir = @top_srcdir@ -srcdir = @srcdir@ -VPATH = @srcdir@ - -include $(topsrcdir)/config/config.mk - -include $(topsrcdir)/config/rules.mk - -libs:: - $(NSINSTALL) $(srcdir)/main.js $(FINAL_TARGET)/modules/devtools
--- a/browser/devtools/commandline/BuiltinCommands.jsm +++ b/browser/devtools/commandline/BuiltinCommands.jsm @@ -9,23 +9,23 @@ const BRAND_SHORT_NAME = Cc["@mozilla.or .createBundle("chrome://branding/locale/brand.properties") .GetStringFromName("brandShortName"); this.EXPORTED_SYMBOLS = [ "CmdAddonFlags", "CmdCommands" ]; Cu.import("resource:///modules/devtools/gcli.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/osfile.jsm"); -Cu.import("resource:///modules/devtools/shared/event-emitter.js"); +Cu.import("resource://gre/modules/osfile.jsm") +Cu.import("resource:///modules/devtools/EventEmitter.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "devtools", - "resource:///modules/devtools/gDevTools.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "TargetFactory", + "resource:///modules/devtools/Target.jsm"); /* CmdAddon ---------------------------------------------------------------- */ (function(module) { XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm"); // We need to use an object in which to store any flags because a primitive @@ -320,16 +320,19 @@ XPCOMUtils.defineLazyModuleGetter(this, Components.utils.import("resource://gre/modules/jsdebugger.jsm", JsDebugger); let global = Components.utils.getGlobalForObject({}); JsDebugger.addDebuggerToGlobal(global); return global.Debugger; }); + XPCOMUtils.defineLazyModuleGetter(this, "TargetFactory", + "resource:///modules/devtools/Target.jsm"); + let debuggers = []; /** * 'calllog' command */ gcli.addCommand({ name: "calllog", description: gcli.lookup("calllogDesc") @@ -349,17 +352,17 @@ XPCOMUtils.defineLazyModuleGetter(this, dbg.onEnterFrame = function(frame) { // BUG 773652 - Make the output from the GCLI calllog command nicer contentWindow.console.log("Method call: " + this.callDescription(frame)); }.bind(this); debuggers.push(dbg); let gBrowser = context.environment.chromeDocument.defaultView.gBrowser; - let target = devtools.TargetFactory.forTab(gBrowser.selectedTab); + let target = TargetFactory.forTab(gBrowser.selectedTab); gDevTools.showToolbox(target, "webconsole"); return gcli.lookup("calllogStartReply"); }, callDescription: function(frame) { let name = "<anonymous>"; if (frame.callee.name) { @@ -501,17 +504,17 @@ XPCOMUtils.defineLazyModuleGetter(this, dbg.onEnterFrame = function(frame) { // BUG 773652 - Make the output from the GCLI calllog command nicer contentWindow.console.log(gcli.lookup("callLogChromeMethodCall") + ": " + this.callDescription(frame)); }.bind(this); let gBrowser = context.environment.chromeDocument.defaultView.gBrowser; - let target = devtools.TargetFactory.forTab(gBrowser.selectedTab); + let target = TargetFactory.forTab(gBrowser.selectedTab); gDevTools.showToolbox(target, "webconsole"); return gcli.lookup("calllogChromeStartReply"); }, valueToString: function(value) { if (typeof value !== "object" || value === null) return uneval(value); @@ -747,30 +750,30 @@ XPCOMUtils.defineLazyModuleGetter(this, /** * 'console close' command */ gcli.addCommand({ name: "console close", description: gcli.lookup("consolecloseDesc"), exec: function Command_consoleClose(args, context) { let gBrowser = context.environment.chromeDocument.defaultView.gBrowser; - let target = devtools.TargetFactory.forTab(gBrowser.selectedTab); + let target = TargetFactory.forTab(gBrowser.selectedTab); return gDevTools.closeToolbox(target); } }); /** * 'console open' command */ gcli.addCommand({ name: "console open", description: gcli.lookup("consoleopenDesc"), exec: function Command_consoleOpen(args, context) { let gBrowser = context.environment.chromeDocument.defaultView.gBrowser; - let target = devtools.TargetFactory.forTab(gBrowser.selectedTab); + let target = TargetFactory.forTab(gBrowser.selectedTab); return gDevTools.showToolbox(target, "webconsole"); } }); }(this)); /* CmdCookie --------------------------------------------------------------- */ (function(module) { @@ -1511,85 +1514,16 @@ XPCOMUtils.defineLazyModuleGetter(this, * @return string * The equivalent of |aString| but safe to use in a regex. */ function escapeRegex(aString) { return aString.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); } }(this)); -/* CmdTools -------------------------------------------------------------- */ - -(function(module) { - gcli.addCommand({ - name: "tools", - description: gcli.lookup("toolsDesc"), - manual: gcli.lookup("toolsManual"), - get hidden() gcli.hiddenByChromePref(), - }); - - gcli.addCommand({ - name: "tools srcdir", - description: gcli.lookup("toolsSrcdirDesc"), - manual: gcli.lookup("toolsSrcdirManual"), - get hidden() gcli.hiddenByChromePref(), - params: [ - { - name: "srcdir", - type: "string", - description: gcli.lookup("toolsSrcdirDir") - } - ], - returnType: "string", - exec: function(args, context) { - let promise = context.createPromise(); - let existsPromise = OS.File.exists(args.srcdir + "/CLOBBER"); - existsPromise.then(function(exists) { - if (exists) { - var str = Cc["@mozilla.org/supports-string;1"] - .createInstance(Ci.nsISupportsString); - str.data = args.srcdir; - Services.prefs.setComplexValue("devtools.loader.srcdir", - Components.interfaces.nsISupportsString, str); - devtools.reload(); - promise.resolve(gcli.lookupFormat("toolsSrcdirReloaded", [args.srcdir])); - return; - } - promise.reject(gcli.lookupFormat("toolsSrcdirNotFound", [args.srcdir])); - }); - return promise; - } - }); - - gcli.addCommand({ - name: "tools builtin", - description: gcli.lookup("toolsBuiltinDesc"), - manual: gcli.lookup("toolsBuiltinManual"), - get hidden() gcli.hiddenByChromePref(), - returnType: "string", - exec: function(args, context) { - Services.prefs.clearUserPref("devtools.loader.srcdir"); - devtools.reload(); - return gcli.lookup("toolsBuiltinReloaded"); - } - }); - - gcli.addCommand({ - name: "tools reload", - description: gcli.lookup("toolsReloadDesc"), - get hidden() gcli.hiddenByChromePref() || !Services.prefs.prefHasUserValue("devtools.loader.srcdir"), - - returnType: "string", - exec: function(args, context) { - devtools.reload(); - return gcli.lookup("toolsReloaded"); - } - }); -}(this)); - /* CmdRestart -------------------------------------------------------------- */ (function(module) { /** * Restart command * * @param boolean nocache * Disables loading content from cache upon restart. @@ -1974,13 +1908,13 @@ XPCOMUtils.defineLazyModuleGetter(this, let eventEmitter = new EventEmitter(); function onPaintFlashingChanged(context) { var gBrowser = context.environment.chromeDocument.defaultView.gBrowser; var tab = gBrowser.selectedTab; eventEmitter.emit("changed", tab); function fireChange() { eventEmitter.emit("changed", tab); } - var target = devtools.TargetFactory.forTab(tab); + var target = TargetFactory.forTab(tab); target.off("navigate", fireChange); target.once("navigate", fireChange); } }(this));
--- a/browser/devtools/commandline/test/helpers.js +++ b/browser/devtools/commandline/test/helpers.js @@ -19,18 +19,17 @@ this.EXPORTED_SYMBOLS = [ 'helpers' ]; var helpers = {}; this.helpers = helpers; let require = (Cu.import("resource://gre/modules/devtools/Require.jsm", {})).require; Components.utils.import("resource:///modules/devtools/gcli.jsm", {}); let console = (Cu.import("resource://gre/modules/devtools/Console.jsm", {})).console; -let devtools = (Cu.import("resource:///modules/devtools/gDevTools.jsm", {})).devtools; -let TargetFactory = devtools.TargetFactory; +let TargetFactory = (Cu.import("resource:///modules/devtools/Target.jsm", {})).TargetFactory; let Promise = (Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {})).Promise; let assert = { ok: ok, is: is, log: info }; var util = require('util/util'); var converters = require('gcli/converters');
--- a/browser/devtools/debugger/DebuggerPanel.jsm +++ b/browser/devtools/debugger/DebuggerPanel.jsm @@ -5,17 +5,17 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; this.EXPORTED_SYMBOLS = ["DebuggerPanel"]; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource:///modules/devtools/shared/event-emitter.js"); +Cu.import("resource:///modules/devtools/EventEmitter.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Promise", "resource://gre/modules/commonjs/sdk/core/promise.js"); XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer", "resource://gre/modules/devtools/dbg-server.jsm"); function DebuggerPanel(iframeWindow, toolbox) {
--- a/browser/devtools/debugger/test/head.js +++ b/browser/devtools/debugger/test/head.js @@ -7,24 +7,24 @@ const Ci = Components.interfaces; const Cu = Components.utils; let tempScope = {}; Cu.import("resource://gre/modules/Services.jsm", tempScope); Cu.import("resource://gre/modules/devtools/dbg-server.jsm", tempScope); Cu.import("resource://gre/modules/devtools/dbg-client.jsm", tempScope); Cu.import("resource:///modules/source-editor.jsm", tempScope); Cu.import("resource:///modules/devtools/gDevTools.jsm", tempScope); +Cu.import("resource:///modules/devtools/Target.jsm", tempScope); let Services = tempScope.Services; let SourceEditor = tempScope.SourceEditor; let DebuggerServer = tempScope.DebuggerServer; let DebuggerTransport = tempScope.DebuggerTransport; let DebuggerClient = tempScope.DebuggerClient; let gDevTools = tempScope.gDevTools; -let devtools = tempScope.devtools; -let TargetFactory = devtools.TargetFactory; +let TargetFactory = tempScope.TargetFactory; // Import the GCLI test helper let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/")); Services.scriptloader.loadSubScript(testDir + "/helpers.js", this); const EXAMPLE_URL = "http://example.com/browser/browser/devtools/debugger/test/"; const TAB1_URL = EXAMPLE_URL + "browser_dbg_tab1.html"; const TAB2_URL = EXAMPLE_URL + "browser_dbg_tab2.html";
--- a/browser/devtools/debugger/test/helpers.js +++ b/browser/devtools/debugger/test/helpers.js @@ -19,18 +19,17 @@ this.EXPORTED_SYMBOLS = [ 'helpers' ]; var helpers = {}; this.helpers = helpers; let require = (Cu.import("resource://gre/modules/devtools/Require.jsm", {})).require; Components.utils.import("resource:///modules/devtools/gcli.jsm", {}); let console = (Cu.import("resource://gre/modules/devtools/Console.jsm", {})).console; -let devtools = (Cu.import("resource:///modules/devtools/gDevTools.jsm", {})).devtools; -let TargetFactory = devtools.TargetFactory; +let TargetFactory = (Cu.import("resource:///modules/devtools/Target.jsm", {})).TargetFactory; let Promise = (Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {})).Promise; let assert = { ok: ok, is: is, log: info }; var util = require('util/util'); var converters = require('gcli/converters');
--- a/browser/devtools/fontinspector/test/browser_fontinspector.js +++ b/browser/devtools/fontinspector/test/browser_fontinspector.js @@ -1,14 +1,14 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ let tempScope = {}; -let {devtools, gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}); -let TargetFactory = devtools.TargetFactory; +Cu.import("resource:///modules/devtools/Target.jsm", tempScope); +let TargetFactory = tempScope.TargetFactory; let DOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); function test() { waitForExplicitFinish(); let doc; let node;
--- a/browser/devtools/framework/Makefile.in +++ b/browser/devtools/framework/Makefile.in @@ -8,9 +8,8 @@ srcdir = @srcdir@ VPATH = @srcdir@ include $(DEPTH)/config/autoconf.mk include $(topsrcdir)/config/rules.mk libs:: $(NSINSTALL) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools - $(NSINSTALL) $(srcdir)/*.js $(FINAL_TARGET)/modules/devtools/framework
new file mode 100644 --- /dev/null +++ b/browser/devtools/framework/Sidebar.jsm @@ -0,0 +1,213 @@ +/* -*- 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 = ["ToolSidebar"]; + +Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js"); +Cu.import("resource:///modules/devtools/EventEmitter.jsm"); + +const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + +/** + * ToolSidebar provides methods to register tabs in the sidebar. + * It's assumed that the sidebar contains a xul:tabbox. + * + * @param {Node} tabbox + * <tabbox> node; + * @param {ToolPanel} panel + * Related ToolPanel instance; + * @param {Boolean} showTabstripe + * Show the tabs. + */ +this.ToolSidebar = function ToolSidebar(tabbox, panel, showTabstripe=true) +{ + EventEmitter.decorate(this); + + this._tabbox = tabbox; + this._panelDoc = this._tabbox.ownerDocument; + this._toolPanel = panel; + + this._tabbox.tabpanels.addEventListener("select", this, true); + + this._tabs = new Map(); + + if (!showTabstripe) { + this._tabbox.setAttribute("hidetabs", "true"); + } +} + +ToolSidebar.prototype = { + /** + * Register a tab. A tab is a document. + * The document must have a title, which will be used as the name of the tab. + * + * @param {string} tab uniq id + * @param {string} url + */ + addTab: function ToolSidebar_addTab(id, url, selected=false) { + let iframe = this._panelDoc.createElementNS(XULNS, "iframe"); + iframe.className = "iframe-" + id; + iframe.setAttribute("flex", "1"); + iframe.setAttribute("src", url); + iframe.tooltip = "aHTMLTooltip"; + + let tab = this._tabbox.tabs.appendItem(); + tab.setAttribute("label", ""); // Avoid showing "undefined" while the tab is loading + + let onIFrameLoaded = function() { + tab.setAttribute("label", iframe.contentDocument.title); + iframe.removeEventListener("load", onIFrameLoaded, true); + if ("setPanel" in iframe.contentWindow) { + iframe.contentWindow.setPanel(this._toolPanel, iframe); + } + this.emit(id + "-ready"); + }.bind(this); + + iframe.addEventListener("load", onIFrameLoaded, true); + + let tabpanel = this._panelDoc.createElementNS(XULNS, "tabpanel"); + tabpanel.setAttribute("id", "sidebar-panel-" + id); + tabpanel.appendChild(iframe); + this._tabbox.tabpanels.appendChild(tabpanel); + + this._tooltip = this._panelDoc.createElementNS(XULNS, "tooltip"); + this._tooltip.id = "aHTMLTooltip"; + tabpanel.appendChild(this._tooltip); + this._tooltip.page = true; + + tab.linkedPanel = "sidebar-panel-" + id; + + // We store the index of this tab. + this._tabs.set(id, tab); + + if (selected) { + // For some reason I don't understand, if we call this.select in this + // event loop (after inserting the tab), the tab will never get the + // the "selected" attribute set to true. + this._panelDoc.defaultView.setTimeout(function() { + this.select(id); + }.bind(this), 10); + } + + this.emit("new-tab-registered", id); + }, + + /** + * Select a specific tab. + */ + select: function ToolSidebar_select(id) { + let tab = this._tabs.get(id); + if (tab) { + this._tabbox.selectedTab = tab; + } + }, + + /** + * Return the id of the selected tab. + */ + getCurrentTabID: function ToolSidebar_getCurrentTabID() { + let currentID = null; + for (let [id, tab] of this._tabs) { + if (this._tabbox.tabs.selectedItem == tab) { + currentID = id; + break; + } + } + return currentID; + }, + + /** + * Returns the requested tab based on the id. + * + * @param String id + * unique id of the requested tab. + */ + getTab: function ToolSidebar_getTab(id) { + return this._tabbox.tabpanels.querySelector("#sidebar-panel-" + id); + }, + + /** + * Event handler. + */ + handleEvent: function ToolSidebar_eventHandler(event) { + if (event.type == "select") { + let previousTool = this._currentTool; + this._currentTool = this.getCurrentTabID(); + if (previousTool) { + this.emit(previousTool + "-unselected"); + } + + this.emit(this._currentTool + "-selected"); + this.emit("select", this._currentTool); + } + }, + + /** + * Toggle sidebar's visibility state. + */ + toggle: function ToolSidebar_toggle() { + if (this._tabbox.hasAttribute("hidden")) { + this.show(); + } else { + this.hide(); + } + }, + + /** + * Show the sidebar. + */ + show: function ToolSidebar_show() { + this._tabbox.removeAttribute("hidden"); + }, + + /** + * Show the sidebar. + */ + hide: function ToolSidebar_hide() { + this._tabbox.setAttribute("hidden", "true"); + }, + + /** + * Return the window containing the tab content. + */ + getWindowForTab: function ToolSidebar_getWindowForTab(id) { + if (!this._tabs.has(id)) { + return null; + } + + let panel = this._panelDoc.getElementById(this._tabs.get(id).linkedPanel); + return panel.firstChild.contentWindow; + }, + + /** + * Clean-up. + */ + destroy: function ToolSidebar_destroy() { + if (this._destroyed) { + return Promise.resolve(null); + } + this._destroyed = true; + + this._tabbox.tabpanels.removeEventListener("select", this, true); + + while (this._tabbox.tabpanels.hasChildNodes()) { + this._tabbox.tabpanels.removeChild(this._tabbox.tabpanels.firstChild); + } + + while (this._tabbox.tabs.hasChildNodes()) { + this._tabbox.tabs.removeChild(this._tabbox.tabs.firstChild); + } + + this._tabs = null; + this._tabbox = null; + this._panelDoc = null; + this._toolPanel = null; + + return Promise.resolve(null); + }, +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/framework/Target.jsm @@ -0,0 +1,587 @@ +/* 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/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ "TargetFactory" ]; + +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js"); +Cu.import("resource:///modules/devtools/EventEmitter.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer", + "resource://gre/modules/devtools/dbg-server.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "DebuggerClient", + "resource://gre/modules/devtools/dbg-client.jsm"); + +const targets = new WeakMap(); +const promiseTargets = new WeakMap(); + +/** + * Functions for creating Targets + */ +this.TargetFactory = { + /** + * Construct a Target + * @param {XULTab} tab + * The tab to use in creating a new target. + * + * @return A target object + */ + forTab: function TF_forTab(tab) { + let target = targets.get(tab); + if (target == null) { + target = new TabTarget(tab); + targets.set(tab, target); + } + return target; + }, + + /** + * Return a promise of a Target for a remote tab. + * @param {Object} options + * The options object has the following properties: + * { + * form: the remote protocol form of a tab, + * client: a DebuggerClient instance, + * chrome: true if the remote target is the whole process + * } + * + * @return A promise of a target object + */ + forRemoteTab: function TF_forRemoteTab(options) { + let promise = promiseTargets.get(options); + if (promise == null) { + let target = new TabTarget(options); + promise = target.makeRemote().then(() => target); + promiseTargets.set(options, promise); + } + return promise; + }, + + /** + * Creating a target for a tab that is being closed is a problem because it + * allows a leak as a result of coming after the close event which normally + * clears things up. This function allows us to ask if there is a known + * target for a tab without creating a target + * @return true/false + */ + isKnownTab: function TF_isKnownTab(tab) { + return targets.has(tab); + }, + + /** + * Construct a Target + * @param {nsIDOMWindow} window + * The chromeWindow to use in creating a new target + * @return A target object + */ + forWindow: function TF_forWindow(window) { + let target = targets.get(window); + if (target == null) { + target = new WindowTarget(window); + targets.set(window, target); + } + return target; + }, + + /** + * Get all of the targets known to the local browser instance + * @return An array of target objects + */ + allTargets: function TF_allTargets() { + let windows = []; + let wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] + .getService(Components.interfaces.nsIWindowMediator); + let en = wm.getXULWindowEnumerator(null); + while (en.hasMoreElements()) { + windows.push(en.getNext()); + } + + return windows.map(function(window) { + return TargetFactory.forWindow(window); + }); + }, +}; + +/** + * The 'version' property allows the developer tools equivalent of browser + * detection. Browser detection is evil, however while we don't know what we + * will need to detect in the future, it is an easy way to postpone work. + * We should be looking to use 'supports()' in place of version where + * possible. + */ +function getVersion() { + // FIXME: return something better + return 20; +} + +/** + * A better way to support feature detection, but we're not yet at a place + * where we have the features well enough defined for this to make lots of + * sense. + */ +function supports(feature) { + // FIXME: return something better + return false; +}; + +/** + * A Target represents something that we can debug. Targets are generally + * read-only. Any changes that you wish to make to a target should be done via + * a Tool that attaches to the target. i.e. a Target is just a pointer saying + * "the thing to debug is over there". + * + * Providing a generalized abstraction of a web-page or web-browser (available + * either locally or remotely) is beyond the scope of this class (and maybe + * also beyond the scope of this universe) However Target does attempt to + * abstract some common events and read-only properties common to many Tools. + * + * Supported read-only properties: + * - name, isRemote, url + * + * Target extends EventEmitter and provides support for the following events: + * - close: The target window has been closed. All tools attached to this + * target should close. This event is not currently cancelable. + * - navigate: The target window has navigated to a different URL + * + * Optional events: + * - will-navigate: The target window will navigate to a different URL + * - hidden: The target is not visible anymore (for TargetTab, another tab is selected) + * - visible: The target is visible (for TargetTab, tab is selected) + * + * Target also supports 2 functions to help allow 2 different versions of + * Firefox debug each other. The 'version' property is the equivalent of + * browser detection - simple and easy to implement but gets fragile when things + * are not quite what they seem. The 'supports' property is the equivalent of + * feature detection - harder to setup, but more robust long-term. + * + * Comparing Targets: 2 instances of a Target object can point at the same + * thing, so t1 !== t2 and t1 != t2 even when they represent the same object. + * To compare to targets use 't1.equals(t2)'. + */ +function Target() { + throw new Error("Use TargetFactory.newXXX or Target.getXXX to create a Target in place of 'new Target()'"); +} + +Object.defineProperty(Target.prototype, "version", { + get: getVersion, + enumerable: true +}); + + +/** + * A TabTarget represents a page living in a browser tab. Generally these will + * be web pages served over http(s), but they don't have to be. + */ +function TabTarget(tab) { + EventEmitter.decorate(this); + this.destroy = this.destroy.bind(this); + this._handleThreadState = this._handleThreadState.bind(this); + this.on("thread-resumed", this._handleThreadState); + this.on("thread-paused", this._handleThreadState); + // Only real tabs need initialization here. Placeholder objects for remote + // targets will be initialized after a makeRemote method call. + if (tab && !["client", "form", "chrome"].every(tab.hasOwnProperty, tab)) { + this._tab = tab; + this._setupListeners(); + } else { + this._form = tab.form; + this._client = tab.client; + this._chrome = tab.chrome; + } +} + +TabTarget.prototype = { + _webProgressListener: null, + + supports: supports, + get version() { return getVersion(); }, + + get tab() { + return this._tab; + }, + + get form() { + return this._form; + }, + + get client() { + return this._client; + }, + + get chrome() { + return this._chrome; + }, + + get window() { + // Be extra careful here, since this may be called by HS_getHudByWindow + // during shutdown. + if (this._tab && this._tab.linkedBrowser) { + return this._tab.linkedBrowser.contentWindow; + } + }, + + get name() { + return this._tab ? this._tab.linkedBrowser.contentDocument.title : + this._form.title; + }, + + get url() { + return this._tab ? this._tab.linkedBrowser.contentDocument.location.href : + this._form.url; + }, + + get isRemote() { + return !this.isLocalTab; + }, + + get isLocalTab() { + return !!this._tab; + }, + + get isThreadPaused() { + return !!this._isThreadPaused; + }, + + /** + * Adds remote protocol capabilities to the target, so that it can be used + * for tools that support the Remote Debugging Protocol even for local + * connections. + */ + makeRemote: function TabTarget_makeRemote() { + if (this._remote) { + return this._remote.promise; + } + + this._remote = Promise.defer(); + + if (this.isLocalTab) { + // Since a remote protocol connection will be made, let's start the + // DebuggerServer here, once and for all tools. + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } + + this._client = new DebuggerClient(DebuggerServer.connectPipe()); + // A local TabTarget will never perform chrome debugging. + this._chrome = false; + } + + this._setupRemoteListeners(); + + if (this.isRemote) { + // In the remote debugging case, the protocol connection will have been + // already initialized in the connection screen code. + this._remote.resolve(null); + } else { + this._client.connect((aType, aTraits) => { + this._client.listTabs(aResponse => { + this._form = aResponse.tabs[aResponse.selected]; + + this._client.attachTab(this._form.actor, (aResponse, aTabClient) => { + if (!aTabClient) { + this._remote.reject("Unable to attach to the tab"); + return; + } + this.threadActor = aResponse.threadActor; + this._remote.resolve(null); + }); + }); + }); + } + + return this._remote.promise; + }, + + /** + * Listen to the different events. + */ + _setupListeners: function TabTarget__setupListeners() { + this._webProgressListener = new TabWebProgressListener(this); + this.tab.linkedBrowser.addProgressListener(this._webProgressListener); + this.tab.addEventListener("TabClose", this); + this.tab.parentNode.addEventListener("TabSelect", this); + this.tab.ownerDocument.defaultView.addEventListener("unload", this); + }, + + /** + * Setup listeners for remote debugging, updating existing ones as necessary. + */ + _setupRemoteListeners: function TabTarget__setupRemoteListeners() { + this.client.addListener("tabDetached", this.destroy); + + this._onTabNavigated = function onRemoteTabNavigated(aType, aPacket) { + let event = Object.create(null); + event.url = aPacket.url; + event.title = aPacket.title; + // Send any stored event payload (DOMWindow or nsIRequest) for backwards + // compatibility with non-remotable tools. + event._navPayload = this._navPayload; + if (aPacket.state == "start") { + this.emit("will-navigate", event); + } else { + this.emit("navigate", event); + } + this._navPayload = null; + }.bind(this); + this.client.addListener("tabNavigated", this._onTabNavigated); + }, + + /** + * Handle tabs events. + */ + handleEvent: function (event) { + switch (event.type) { + case "TabClose": + case "unload": + this.destroy(); + break; + case "TabSelect": + if (this.tab.selected) { + this.emit("visible", event); + } else { + this.emit("hidden", event); + } + break; + } + }, + + /** + * Handle script status. + */ + _handleThreadState: function(event) { + switch (event) { + case "thread-resumed": + this._isThreadPaused = false; + break; + case "thread-paused": + this._isThreadPaused = true; + break; + } + }, + + /** + * Target is not alive anymore. + */ + destroy: function() { + // If several things call destroy then we give them all the same + // destruction promise so we're sure to destroy only once + if (this._destroyer) { + return this._destroyer.promise; + } + + this._destroyer = Promise.defer(); + + // Before taking any action, notify listeners that destruction is imminent. + this.emit("close"); + + // First of all, do cleanup tasks that pertain to both remoted and + // non-remoted targets. + this.off("thread-resumed", this._handleThreadState); + this.off("thread-paused", this._handleThreadState); + + if (this._tab) { + if (this._webProgressListener) { + this._webProgressListener.destroy(); + } + + this._tab.ownerDocument.defaultView.removeEventListener("unload", this); + this._tab.removeEventListener("TabClose", this); + this._tab.parentNode.removeEventListener("TabSelect", this); + } + + // If this target was not remoted, the promise will be resolved before the + // function returns. + if (this._tab && !this._client) { + targets.delete(this._tab); + this._tab = null; + this._client = null; + this._form = null; + this._remote = null; + + this._destroyer.resolve(null); + } else if (this._client) { + // If, on the other hand, this target was remoted, the promise will be + // resolved after the remote connection is closed. + this.client.removeListener("tabNavigated", this._onTabNavigated); + this.client.removeListener("tabDetached", this.destroy); + + this._client.close(function onClosed() { + if (this._tab) { + targets.delete(this._tab); + } else { + promiseTargets.delete(this._form); + } + this._client = null; + this._tab = null; + this._form = null; + this._remote = null; + + this._destroyer.resolve(null); + }.bind(this)); + } + + return this._destroyer.promise; + }, + + toString: function() { + return 'TabTarget:' + (this._tab ? this._tab : (this._form && this._form.actor)); + }, +}; + + +/** + * WebProgressListener for TabTarget. + * + * @param object aTarget + * The TabTarget instance to work with. + */ +function TabWebProgressListener(aTarget) { + this.target = aTarget; +} + +TabWebProgressListener.prototype = { + target: null, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]), + + onStateChange: function TWPL_onStateChange(progress, request, flag, status) { + let isStart = flag & Ci.nsIWebProgressListener.STATE_START; + let isDocument = flag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT; + let isNetwork = flag & Ci.nsIWebProgressListener.STATE_IS_NETWORK; + let isRequest = flag & Ci.nsIWebProgressListener.STATE_IS_REQUEST; + + // Skip non-interesting states. + if (!isStart || !isDocument || !isRequest || !isNetwork) { + return; + } + + // emit event if the top frame is navigating + if (this.target && this.target.window == progress.DOMWindow) { + // Emit the event if the target is not remoted or store the payload for + // later emission otherwise. + if (this.target._client) { + this.target._navPayload = request; + } else { + this.target.emit("will-navigate", request); + } + } + }, + + onProgressChange: function() {}, + onSecurityChange: function() {}, + onStatusChange: function() {}, + + onLocationChange: function TWPL_onLocationChange(webProgress, request, URI, flags) { + if (this.target && + !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)) { + let window = webProgress.DOMWindow; + // Emit the event if the target is not remoted or store the payload for + // later emission otherwise. + if (this.target._client) { + this.target._navPayload = window; + } else { + this.target.emit("navigate", window); + } + } + }, + + /** + * Destroy the progress listener instance. + */ + destroy: function TWPL_destroy() { + if (this.target.tab) { + this.target.tab.linkedBrowser.removeProgressListener(this); + } + this.target._webProgressListener = null; + this.target = null; + } +}; + + +/** + * A WindowTarget represents a page living in a xul window or panel. Generally + * these will have a chrome: URL + */ +function WindowTarget(window) { + EventEmitter.decorate(this); + this._window = window; + this._setupListeners(); +} + +WindowTarget.prototype = { + supports: supports, + get version() { return getVersion(); }, + + get window() { + return this._window; + }, + + get name() { + return this._window.document.title; + }, + + get url() { + return this._window.document.location.href; + }, + + get isRemote() { + return false; + }, + + get isLocalTab() { + return false; + }, + + get isThreadPaused() { + return !!this._isThreadPaused; + }, + + /** + * Listen to the different events. + */ + _setupListeners: function() { + this._handleThreadState = this._handleThreadState.bind(this); + this.on("thread-paused", this._handleThreadState); + this.on("thread-resumed", this._handleThreadState); + }, + + _handleThreadState: function(event) { + switch (event) { + case "thread-resumed": + this._isThreadPaused = false; + break; + case "thread-paused": + this._isThreadPaused = true; + break; + } + }, + + /** + * Target is not alive anymore. + */ + destroy: function() { + if (!this._destroyed) { + this._destroyed = true; + + this.off("thread-paused", this._handleThreadState); + this.off("thread-resumed", this._handleThreadState); + this.emit("close"); + + targets.delete(this._window); + this._window = null; + } + + return Promise.resolve(null); + }, + + toString: function() { + return 'WindowTarget:' + this.window; + }, +};
new file mode 100644 --- /dev/null +++ b/browser/devtools/framework/ToolDefinitions.jsm @@ -0,0 +1,228 @@ +/* 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/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ + "defaultTools", + "webConsoleDefinition", + "debuggerDefinition", + "inspectorDefinition", + "styleEditorDefinition", + "netMonitorDefinition" + ]; + +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; + +const inspectorProps = "chrome://browser/locale/devtools/inspector.properties"; +const debuggerProps = "chrome://browser/locale/devtools/debugger.properties"; +const styleEditorProps = "chrome://browser/locale/devtools/styleeditor.properties"; +const webConsoleProps = "chrome://browser/locale/devtools/webconsole.properties"; +const profilerProps = "chrome://browser/locale/devtools/profiler.properties"; +const netMonitorProps = "chrome://browser/locale/devtools/netmonitor.properties"; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource:///modules/devtools/EventEmitter.jsm"); + +XPCOMUtils.defineLazyGetter(this, "osString", + function() Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS); + +// Panels +XPCOMUtils.defineLazyModuleGetter(this, "WebConsolePanel", + "resource:///modules/WebConsolePanel.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "DebuggerPanel", + "resource:///modules/devtools/DebuggerPanel.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "StyleEditorPanel", + "resource:///modules/devtools/StyleEditorPanel.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "InspectorPanel", + "resource:///modules/devtools/InspectorPanel.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "ProfilerPanel", + "resource:///modules/devtools/ProfilerPanel.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "NetMonitorPanel", + "resource:///modules/devtools/NetMonitorPanel.jsm"); + +// Strings +XPCOMUtils.defineLazyGetter(this, "webConsoleStrings", + function() Services.strings.createBundle(webConsoleProps)); + +XPCOMUtils.defineLazyGetter(this, "debuggerStrings", + function() Services.strings.createBundle(debuggerProps)); + +XPCOMUtils.defineLazyGetter(this, "styleEditorStrings", + function() Services.strings.createBundle(styleEditorProps)); + +XPCOMUtils.defineLazyGetter(this, "inspectorStrings", + function() Services.strings.createBundle(inspectorProps)); + +XPCOMUtils.defineLazyGetter(this, "profilerStrings", + function() Services.strings.createBundle(profilerProps)); + +XPCOMUtils.defineLazyGetter(this, "netMonitorStrings", + function() Services.strings.createBundle(netMonitorProps)); + +// Definitions +let webConsoleDefinition = { + id: "webconsole", + key: l10n("cmd.commandkey", webConsoleStrings), + accesskey: l10n("webConsoleCmd.accesskey", webConsoleStrings), + modifiers: Services.appinfo.OS == "Darwin" ? "accel,alt" : "accel,shift", + ordinal: 0, + icon: "chrome://browser/skin/devtools/tool-webconsole.png", + url: "chrome://browser/content/devtools/webconsole.xul", + label: l10n("ToolboxWebconsole.label", webConsoleStrings), + tooltip: l10n("ToolboxWebconsole.tooltip", webConsoleStrings), + + isTargetSupported: function(target) { + return true; + }, + build: function(iframeWindow, toolbox) { + let panel = new WebConsolePanel(iframeWindow, toolbox); + return panel.open(); + } +}; + +let debuggerDefinition = { + id: "jsdebugger", + key: l10n("open.commandkey", debuggerStrings), + accesskey: l10n("debuggerMenu.accesskey", debuggerStrings), + modifiers: osString == "Darwin" ? "accel,alt" : "accel,shift", + ordinal: 2, + killswitch: "devtools.debugger.enabled", + icon: "chrome://browser/skin/devtools/tool-debugger.png", + url: "chrome://browser/content/debugger.xul", + label: l10n("ToolboxDebugger.label", debuggerStrings), + tooltip: l10n("ToolboxDebugger.tooltip", debuggerStrings), + + isTargetSupported: function(target) { + return true; + }, + + build: function(iframeWindow, toolbox) { + let panel = new DebuggerPanel(iframeWindow, toolbox); + return panel.open(); + } +}; + +let inspectorDefinition = { + id: "inspector", + accesskey: l10n("inspector.accesskey", inspectorStrings), + key: l10n("inspector.commandkey", inspectorStrings), + ordinal: 1, + modifiers: osString == "Darwin" ? "accel,alt" : "accel,shift", + icon: "chrome://browser/skin/devtools/tool-inspector.png", + url: "chrome://browser/content/devtools/inspector/inspector.xul", + label: l10n("inspector.label", inspectorStrings), + tooltip: l10n("inspector.tooltip", inspectorStrings), + + isTargetSupported: function(target) { + return !target.isRemote; + }, + + build: function(iframeWindow, toolbox) { + let panel = new InspectorPanel(iframeWindow, toolbox); + return panel.open(); + } +}; + +let styleEditorDefinition = { + id: "styleeditor", + key: l10n("open.commandkey", styleEditorStrings), + ordinal: 3, + accesskey: l10n("open.accesskey", styleEditorStrings), + modifiers: "shift", + icon: "chrome://browser/skin/devtools/tool-styleeditor.png", + url: "chrome://browser/content/styleeditor.xul", + label: l10n("ToolboxStyleEditor.label", styleEditorStrings), + tooltip: l10n("ToolboxStyleEditor.tooltip", styleEditorStrings), + + isTargetSupported: function(target) { + return !target.isRemote; + }, + + build: function(iframeWindow, toolbox) { + let panel = new StyleEditorPanel(iframeWindow, toolbox); + return panel.open(); + } +}; + +let profilerDefinition = { + id: "jsprofiler", + accesskey: l10n("profiler.accesskey", profilerStrings), + key: l10n("profiler2.commandkey", profilerStrings), + ordinal: 4, + modifiers: "shift", + killswitch: "devtools.profiler.enabled", + icon: "chrome://browser/skin/devtools/tool-profiler.png", + url: "chrome://browser/content/profiler.xul", + label: l10n("profiler.label", profilerStrings), + tooltip: l10n("profiler.tooltip", profilerStrings), + + isTargetSupported: function (target) { + return true; + }, + + build: function (frame, target) { + let panel = new ProfilerPanel(frame, target); + return panel.open(); + } +}; + +let netMonitorDefinition = { + id: "netmonitor", + accesskey: l10n("netmonitor.accesskey", netMonitorStrings), + key: l10n("netmonitor.commandkey", netMonitorStrings), + ordinal: 5, + modifiers: osString == "Darwin" ? "accel,alt" : "accel,shift", + killswitch: "devtools.netmonitor.enabled", + icon: "chrome://browser/skin/devtools/tool-profiler.png", + url: "chrome://browser/content/devtools/netmonitor.xul", + label: l10n("netmonitor.label", netMonitorStrings), + tooltip: l10n("netmonitor.tooltip", netMonitorStrings), + + isTargetSupported: function(target) { + return true; + }, + + build: function(iframeWindow, toolbox) { + let panel = new NetMonitorPanel(iframeWindow, toolbox); + return panel.open(); + } +}; + +this.defaultTools = [ + styleEditorDefinition, + webConsoleDefinition, + debuggerDefinition, + inspectorDefinition, + netMonitorDefinition +]; + +if (Services.prefs.getBoolPref("devtools.profiler.enabled")) { + defaultTools.push(profilerDefinition); +} + +/** + * Lookup l10n string from a string bundle. + * + * @param {string} name + * The key to lookup. + * @param {StringBundle} bundle + * The key to lookup. + * @returns A localized version of the given key. + */ +function l10n(name, bundle) +{ + try { + return bundle.GetStringFromName(name); + } catch (ex) { + Services.console.logStringMessage("Error reading '" + name + "'"); + throw new Error("l10n error with " + name); + } +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/framework/Toolbox.jsm @@ -0,0 +1,742 @@ +/* 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/. */ + +"use strict"; + +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; + +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"); +Cu.import("resource:///modules/devtools/gDevTools.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Hosts", + "resource:///modules/devtools/ToolboxHosts.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "CommandUtils", + "resource:///modules/devtools/DeveloperToolbar.jsm"); + +XPCOMUtils.defineLazyGetter(this, "toolboxStrings", function() { + let bundle = Services.strings.createBundle("chrome://browser/locale/devtools/toolbox.properties"); + let l10n = function(aName, ...aArgs) { + try { + if (aArgs.length == 0) { + return bundle.GetStringFromName(aName); + } else { + return bundle.formatStringFromName(aName, aArgs, aArgs.length); + } + } catch (ex) { + Services.console.logStringMessage("Error reading '" + aName + "'"); + } + }; + return l10n; +}); + +XPCOMUtils.defineLazyGetter(this, "Requisition", function() { + Cu.import("resource://gre/modules/devtools/Require.jsm"); + Cu.import("resource:///modules/devtools/gcli.jsm"); + + return require('gcli/cli').Requisition; +}); + +this.EXPORTED_SYMBOLS = [ "Toolbox" ]; + +// This isn't the best place for this, but I don't know what is right now + +/** + * Implementation of 'promised', while we wait for bug 790195 to be fixed. + * @see Consuming promises in https://addons.mozilla.org/en-US/developers/docs/sdk/latest/packages/api-utils/promise.html + * @see https://bugzilla.mozilla.org/show_bug.cgi?id=790195 + * @see https://github.com/mozilla/addon-sdk/blob/master/packages/api-utils/lib/promise.js#L179 + */ +Promise.promised = (function() { + // Note: Define shortcuts and utility functions here in order to avoid + // slower property accesses and unnecessary closure creations on each + // call of this popular function. + + var call = Function.call; + var concat = Array.prototype.concat; + + // Utility function that does following: + // execute([ f, self, args...]) => f.apply(self, args) + function execute(args) { return call.apply(call, args); } + + // Utility function that takes promise of `a` array and maybe promise `b` + // as arguments and returns promise for `a.concat(b)`. + function promisedConcat(promises, unknown) { + return promises.then(function(values) { + return Promise.resolve(unknown).then(function(value) { + return values.concat([ value ]); + }); + }); + } + + return function promised(f, prototype) { + /** + Returns a wrapped `f`, which when called returns a promise that resolves to + `f(...)` passing all the given arguments to it, which by the way may be + promises. Optionally second `prototype` argument may be provided to be used + a prototype for a returned promise. + + ## Example + + var promise = promised(Array)(1, promise(2), promise(3)) + promise.then(console.log) // => [ 1, 2, 3 ] + **/ + + return function promised() { + // create array of [ f, this, args... ] + return concat.apply([ f, this ], arguments). + // reduce it via `promisedConcat` to get promised array of fulfillments + reduce(promisedConcat, Promise.resolve([], prototype)). + // finally map that to promise of `f.apply(this, args...)` + then(execute); + }; + }; +})(); + +/** + * Convert an array of promises to a single promise, which is resolved (with an + * array containing resolved values) only when all the component promises are + * resolved. + */ +Promise.all = Promise.promised(Array); + + + + +/** + * A "Toolbox" is the component that holds all the tools for one specific + * target. Visually, it's a document that includes the tools tabs and all + * the iframes where the tool panels will be living in. + * + * @param {object} target + * The object the toolbox is debugging. + * @param {string} selectedTool + * Tool to select initially + * @param {Toolbox.HostType} hostType + * Type of host that will host the toolbox (e.g. sidebar, window) + */ +this.Toolbox = function Toolbox(target, selectedTool, hostType) { + this._target = target; + this._toolPanels = new Map(); + + this._toolRegistered = this._toolRegistered.bind(this); + this._toolUnregistered = this._toolUnregistered.bind(this); + this.destroy = this.destroy.bind(this); + + this._target.on("close", this.destroy); + + if (!hostType) { + hostType = Services.prefs.getCharPref(this._prefs.LAST_HOST); + } + if (!selectedTool) { + selectedTool = Services.prefs.getCharPref(this._prefs.LAST_TOOL); + } + let definitions = gDevTools.getToolDefinitionMap(); + if (!definitions.get(selectedTool)) { + selectedTool = "webconsole"; + } + this._defaultToolId = selectedTool; + + this._host = this._createHost(hostType); + + EventEmitter.decorate(this); + + this._refreshHostTitle = this._refreshHostTitle.bind(this); + this._target.on("navigate", this._refreshHostTitle); + this.on("host-changed", this._refreshHostTitle); + this.on("select", this._refreshHostTitle); + + gDevTools.on("tool-registered", this._toolRegistered); + gDevTools.on("tool-unregistered", this._toolUnregistered); +} + +/** + * The toolbox can be 'hosted' either embedded in a browser window + * or in a separate window. + */ +Toolbox.HostType = { + BOTTOM: "bottom", + SIDE: "side", + WINDOW: "window" +} + +Toolbox.prototype = { + _URL: "chrome://browser/content/devtools/framework/toolbox.xul", + + _prefs: { + LAST_HOST: "devtools.toolbox.host", + LAST_TOOL: "devtools.toolbox.selectedTool", + SIDE_ENABLED: "devtools.toolbox.sideEnabled" + }, + + HostType: Toolbox.HostType, + + /** + * Returns a *copy* of the _toolPanels collection. + * + * @return {Map} panels + * All the running panels in the toolbox + */ + getToolPanels: function TB_getToolPanels() { + let panels = new Map(); + + for (let [key, value] of this._toolPanels) { + panels.set(key, value); + } + return panels; + }, + + /** + * Access the panel for a given tool + */ + getPanel: function TBOX_getPanel(id) { + return this.getToolPanels().get(id); + }, + + /** + * This is a shortcut for getPanel(currentToolId) because it is much more + * likely that we're going to want to get the panel that we've just made + * visible + */ + getCurrentPanel: function TBOX_getCurrentPanel() { + return this.getToolPanels().get(this.currentToolId); + }, + + /** + * Get/alter the target of a Toolbox so we're debugging something different. + * See Target.jsm for more details. + * TODO: Do we allow |toolbox.target = null;| ? + */ + get target() { + return this._target; + }, + + /** + * Get/alter the host of a Toolbox, i.e. is it in browser or in a separate + * tab. See HostType for more details. + */ + get hostType() { + return this._host.type; + }, + + /** + * Get/alter the currently displayed tool. + */ + get currentToolId() { + return this._currentToolId; + }, + + set currentToolId(value) { + this._currentToolId = value; + }, + + /** + * Get the iframe containing the toolbox UI. + */ + get frame() { + return this._host.frame; + }, + + /** + * Shortcut to the document containing the toolbox UI + */ + get doc() { + return this.frame.contentDocument; + }, + + /** + * Open the toolbox + */ + open: function TBOX_open() { + let deferred = Promise.defer(); + + this._host.create().then(function(iframe) { + let domReady = function() { + iframe.removeEventListener("DOMContentLoaded", domReady, true); + + this.isReady = true; + + let closeButton = this.doc.getElementById("toolbox-close"); + closeButton.addEventListener("command", this.destroy, true); + + this._buildDockButtons(); + this._buildTabs(); + this._buildButtons(); + this._addKeysToWindow(); + + this.selectTool(this._defaultToolId).then(function(panel) { + this.emit("ready"); + deferred.resolve(); + }.bind(this)); + }.bind(this); + + iframe.addEventListener("DOMContentLoaded", domReady, true); + iframe.setAttribute("src", this._URL); + }.bind(this)); + + return deferred.promise; + }, + + /** + * Adds the keys and commands to the Toolbox Window in window mode. + */ + _addKeysToWindow: function TBOX__addKeysToWindow() { + if (this.hostType != Toolbox.HostType.WINDOW) { + return; + } + let doc = this.doc.defaultView.parent.document; + for (let [id, toolDefinition] of gDevTools._tools) { + if (toolDefinition.key) { + // Prevent multiple entries for the same tool. + if (doc.getElementById("key_" + id)) { + continue; + } + let key = doc.createElement("key"); + key.id = "key_" + id; + + if (toolDefinition.key.startsWith("VK_")) { + key.setAttribute("keycode", toolDefinition.key); + } else { + key.setAttribute("key", toolDefinition.key); + } + + key.setAttribute("modifiers", toolDefinition.modifiers); + key.setAttribute("oncommand", "void(0);"); // needed. See bug 371900 + key.addEventListener("command", function(toolId) { + this.selectTool(toolId); + }.bind(this, id), true); + doc.getElementById("toolbox-keyset").appendChild(key); + } + } + }, + + /** + * Build the buttons for changing hosts. Called every time + * the host changes. + */ + _buildDockButtons: function TBOX_createDockButtons() { + let dockBox = this.doc.getElementById("toolbox-dock-buttons"); + + while (dockBox.firstChild) { + dockBox.removeChild(dockBox.firstChild); + } + + if (!this._target.isLocalTab) { + return; + } + + let closeButton = this.doc.getElementById("toolbox-close"); + if (this.hostType === this.HostType.WINDOW) { + closeButton.setAttribute("hidden", "true"); + } else { + closeButton.removeAttribute("hidden"); + } + + let sideEnabled = Services.prefs.getBoolPref(this._prefs.SIDE_ENABLED); + + for each (let position in this.HostType) { + if (position == this.hostType || + (!sideEnabled && position == this.HostType.SIDE)) { + continue; + } + + let button = this.doc.createElement("toolbarbutton"); + button.id = "toolbox-dock-" + position; + button.className = "toolbox-dock-button"; + button.setAttribute("tooltiptext", toolboxStrings("toolboxDockButtons." + + position + ".tooltip")); + button.addEventListener("command", function(position) { + this.switchHost(position); + }.bind(this, position)); + + dockBox.appendChild(button); + } + }, + + /** + * Add tabs to the toolbox UI for registered tools + */ + _buildTabs: function TBOX_buildTabs() { + for (let definition of gDevTools.getToolDefinitionArray()) { + this._buildTabForTool(definition); + } + }, + + /** + * Add buttons to the UI as specified in the devtools.window.toolbarSpec pref + */ + _buildButtons: function TBOX_buildButtons() { + if (!this.target.isLocalTab) { + return; + } + + let toolbarSpec = CommandUtils.getCommandbarSpec("devtools.toolbox.toolbarSpec"); + let environment = { chromeDocument: this.target.tab.ownerDocument }; + let requisition = new Requisition(environment); + + let buttons = CommandUtils.createButtons(toolbarSpec, this._target, this.doc, requisition); + + let container = this.doc.getElementById("toolbox-buttons"); + buttons.forEach(function(button) { + container.appendChild(button); + }.bind(this)); + }, + + /** + * Build a tab for one tool definition and add to the toolbox + * + * @param {string} toolDefinition + * Tool definition of the tool to build a tab for. + */ + _buildTabForTool: function TBOX_buildTabForTool(toolDefinition) { + if (!toolDefinition.isTargetSupported(this._target)) { + return; + } + + let tabs = this.doc.getElementById("toolbox-tabs"); + let deck = this.doc.getElementById("toolbox-deck"); + + let id = toolDefinition.id; + + let radio = this.doc.createElement("radio"); + radio.className = "toolbox-tab devtools-tab"; + radio.id = "toolbox-tab-" + id; + radio.setAttribute("flex", "1"); + radio.setAttribute("toolid", id); + radio.setAttribute("tooltiptext", toolDefinition.tooltip); + + radio.addEventListener("command", function(id) { + this.selectTool(id); + }.bind(this, id)); + + if (toolDefinition.icon) { + let image = this.doc.createElement("image"); + image.setAttribute("src", toolDefinition.icon); + radio.appendChild(image); + } + + let label = this.doc.createElement("label"); + label.setAttribute("value", toolDefinition.label) + label.setAttribute("crop", "end"); + label.setAttribute("flex", "1"); + + let vbox = this.doc.createElement("vbox"); + vbox.className = "toolbox-panel"; + vbox.id = "toolbox-panel-" + id; + + radio.appendChild(label); + tabs.appendChild(radio); + deck.appendChild(vbox); + + this._addKeysToWindow(); + }, + + /** + * Switch to the tool with the given id + * + * @param {string} id + * The id of the tool to switch to + */ + selectTool: function TBOX_selectTool(id) { + let deferred = Promise.defer(); + + let selected = this.doc.querySelector(".devtools-tab[selected]"); + if (selected) { + selected.removeAttribute("selected"); + } + let tab = this.doc.getElementById("toolbox-tab-" + id); + tab.setAttribute("selected", "true"); + + if (this._currentToolId == id) { + // Return the existing panel in order to have a consistent return value. + return Promise.resolve(this._toolPanels.get(id)); + } + + if (!this.isReady) { + throw new Error("Can't select tool, wait for toolbox 'ready' event"); + } + let tab = this.doc.getElementById("toolbox-tab-" + id); + + if (!tab) { + throw new Error("No tool found"); + } + + let tabstrip = this.doc.getElementById("toolbox-tabs"); + + // select the right tab + let index = -1; + let tabs = tabstrip.childNodes; + for (let i = 0; i < tabs.length; i++) { + if (tabs[i] === tab) { + index = i; + break; + } + } + tabstrip.selectedIndex = index; + + // and select the right iframe + let deck = this.doc.getElementById("toolbox-deck"); + deck.selectedIndex = index; + + let definition = gDevTools.getToolDefinitionMap().get(id); + + this._currentToolId = id; + + let iframe = this.doc.getElementById("toolbox-panel-iframe-" + id); + if (!iframe) { + iframe = this.doc.createElement("iframe"); + iframe.className = "toolbox-panel-iframe"; + iframe.id = "toolbox-panel-iframe-" + id; + iframe.setAttribute("flex", 1); + iframe.setAttribute("forceOwnRefreshDriver", ""); + iframe.tooltip = "aHTMLTooltip"; + + let vbox = this.doc.getElementById("toolbox-panel-" + id); + vbox.appendChild(iframe); + + let boundLoad = function() { + iframe.removeEventListener("DOMContentLoaded", boundLoad, true); + + let built = definition.build(iframe.contentWindow, this); + Promise.resolve(built).then(function(panel) { + this._toolPanels.set(id, panel); + + this.emit(id + "-ready", panel); + this.emit("select", id); + this.emit(id + "-selected", panel); + gDevTools.emit(id + "-ready", this, panel); + + deferred.resolve(panel); + }.bind(this)); + }.bind(this); + + iframe.addEventListener("DOMContentLoaded", boundLoad, true); + iframe.setAttribute("src", definition.url); + } else { + let panel = this._toolPanels.get(id); + // only emit 'select' event if the iframe has been loaded + if (panel) { + this.emit("select", id); + this.emit(id + "-selected", panel); + deferred.resolve(panel); + } + } + + Services.prefs.setCharPref(this._prefs.LAST_TOOL, id); + + return deferred.promise; + }, + + /** + * Raise the toolbox host. + */ + raise: function TBOX_raise() { + this._host.raise(); + }, + + /** + * Refresh the host's title. + */ + _refreshHostTitle: function TBOX_refreshHostTitle() { + let toolName; + let toolId = this.currentToolId; + if (toolId) { + let toolDef = gDevTools.getToolDefinitionMap().get(toolId); + toolName = toolDef.label; + } else { + // no tool is selected + toolName = toolboxStrings("toolbox.defaultTitle"); + } + let title = toolboxStrings("toolbox.titleTemplate", + toolName, this.target.url); + this._host.setTitle(title); + }, + + /** + * Create a host object based on the given host type. + * + * Warning: some hosts require that the toolbox target provides a reference to + * the attached tab. Not all Targets have a tab property - make sure you correctly + * mix and match hosts and targets. + * + * @param {string} hostType + * The host type of the new host object + * + * @return {Host} host + * The created host object + */ + _createHost: function TBOX_createHost(hostType) { + if (!Hosts[hostType]) { + throw new Error('Unknown hostType: '+ hostType); + } + let newHost = new Hosts[hostType](this.target.tab); + + // clean up the toolbox if its window is closed + newHost.on("window-closed", this.destroy); + + return newHost; + }, + + /** + * Switch to a new host for the toolbox UI. E.g. + * bottom, sidebar, separate window. + * + * @param {string} hostType + * The host type of the new host object + */ + switchHost: function TBOX_switchHost(hostType) { + if (hostType == this._host.type) { + return; + } + + if (!this._target.isLocalTab) { + return; + } + + let newHost = this._createHost(hostType); + return newHost.create().then(function(iframe) { + // change toolbox document's parent to the new host + iframe.QueryInterface(Ci.nsIFrameLoaderOwner); + iframe.swapFrameLoaders(this.frame); + + this._host.off("window-closed", this.destroy); + this._host.destroy(); + + this._host = newHost; + + Services.prefs.setCharPref(this._prefs.LAST_HOST, this._host.type); + + this._buildDockButtons(); + this._addKeysToWindow(); + + this.emit("host-changed"); + }.bind(this)); + }, + + /** + * Handler for the tool-registered event. + * @param {string} event + * Name of the event ("tool-registered") + * @param {string} toolId + * Id of the tool that was registered + */ + _toolRegistered: function TBOX_toolRegistered(event, toolId) { + let defs = gDevTools.getToolDefinitionMap(); + let tool = defs.get(toolId); + + this._buildTabForTool(tool); + }, + + /** + * Handler for the tool-unregistered event. + * @param {string} event + * Name of the event ("tool-unregistered") + * @param {string} toolId + * Id of the tool that was unregistered + */ + _toolUnregistered: function TBOX_toolUnregistered(event, toolId) { + let radio = this.doc.getElementById("toolbox-tab-" + toolId); + let panel = this.doc.getElementById("toolbox-panel-" + toolId); + + if (radio) { + if (this._currentToolId == toolId) { + let nextToolName = null; + if (radio.nextSibling) { + nextToolName = radio.nextSibling.getAttribute("toolid"); + } + if (radio.previousSibling) { + nextToolName = radio.previousSibling.getAttribute("toolid"); + } + if (nextToolName) { + this.selectTool(nextToolName); + } + } + radio.parentNode.removeChild(radio); + } + + if (panel) { + panel.parentNode.removeChild(panel); + } + + if (this.hostType == Toolbox.HostType.WINDOW) { + let doc = this.doc.defaultView.parent.document; + let key = doc.getElementById("key_" + id); + if (key) { + key.parentNode.removeChild(key); + } + } + + if (this._toolPanels.has(toolId)) { + let instance = this._toolPanels.get(toolId); + instance.destroy(); + this._toolPanels.delete(toolId); + } + }, + + + /** + * Get the toolbox's notification box + * + * @return The notification box element. + */ + getNotificationBox: function TBOX_getNotificationBox() { + return this.doc.getElementById("toolbox-notificationbox"); + }, + + /** + * Remove all UI elements, detach from target and clear up + */ + destroy: function TBOX_destroy() { + // If several things call destroy then we give them all the same + // destruction promise so we're sure to destroy only once + if (this._destroyer) { + return this._destroyer; + } + // Assign the "_destroyer" property before calling the other + // destroyer methods to guarantee that the Toolbox's destroy + // method is only executed once. + let deferred = Promise.defer(); + this._destroyer = deferred.promise; + + this._target.off("navigate", this._refreshHostTitle); + this.off("select", this._refreshHostTitle); + this.off("host-changed", this._refreshHostTitle); + + gDevTools.off("tool-registered", this._toolRegistered); + gDevTools.off("tool-unregistered", this._toolUnregistered); + + let outstanding = []; + + for (let [id, panel] of this._toolPanels) { + outstanding.push(panel.destroy()); + } + + let container = this.doc.getElementById("toolbox-buttons"); + while(container.firstChild) { + container.removeChild(container.firstChild); + } + + outstanding.push(this._host.destroy()); + + // Targets need to be notified that the toolbox is being torn down, so that + // remote protocol connections can be gracefully terminated. + if (this._target) { + this._target.off("close", this.destroy); + outstanding.push(this._target.destroy()); + } + this._target = null; + + Promise.all(outstanding).then(function() { + this.emit("destroyed"); + // Free _host after the call to destroyed in order to let a chance + // to destroyed listeners to still query toolbox attributes + this._host = null; + deferred.resolve(); + }.bind(this)); + + return this._destroyer; + } +};
new file mode 100644 --- /dev/null +++ b/browser/devtools/framework/ToolboxHosts.jsm @@ -0,0 +1,283 @@ +/* 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/. */ + +"use strict"; + +const Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js"); +Cu.import("resource:///modules/devtools/EventEmitter.jsm"); + +this.EXPORTED_SYMBOLS = [ "Hosts" ]; + +/** + * A toolbox host represents an object that contains a toolbox (e.g. the + * sidebar or a separate window). Any host object should implement the + * following functions: + * + * create() - create the UI and emit a 'ready' event when the UI is ready to use + * destroy() - destroy the host's UI + */ + +this.Hosts = { + "bottom": BottomHost, + "side": SidebarHost, + "window": WindowHost +} + +/** + * Host object for the dock on the bottom of the browser + */ +function BottomHost(hostTab) { + this.hostTab = hostTab; + + EventEmitter.decorate(this); +} + +BottomHost.prototype = { + type: "bottom", + + heightPref: "devtools.toolbox.footer.height", + + /** + * Create a box at the bottom of the host tab. + */ + create: function BH_create() { + let deferred = Promise.defer(); + + let gBrowser = this.hostTab.ownerDocument.defaultView.gBrowser; + let ownerDocument = gBrowser.ownerDocument; + + this._splitter = ownerDocument.createElement("splitter"); + this._splitter.setAttribute("class", "devtools-horizontal-splitter"); + + this.frame = ownerDocument.createElement("iframe"); + this.frame.className = "devtools-toolbox-bottom-iframe"; + this.frame.height = Services.prefs.getIntPref(this.heightPref); + + this._nbox = gBrowser.getNotificationBox(this.hostTab.linkedBrowser); + this._nbox.appendChild(this._splitter); + this._nbox.appendChild(this.frame); + + let frameLoad = function() { + this.frame.removeEventListener("DOMContentLoaded", frameLoad, true); + this.emit("ready", this.frame); + + deferred.resolve(this.frame); + }.bind(this); + + this.frame.tooltip = "aHTMLTooltip"; + this.frame.addEventListener("DOMContentLoaded", frameLoad, true); + + // we have to load something so we can switch documents if we have to + this.frame.setAttribute("src", "about:blank"); + + focusTab(this.hostTab); + + return deferred.promise; + }, + + /** + * Raise the host. + */ + raise: function BH_raise() { + focusTab(this.hostTab); + }, + + /** + * Set the toolbox title. + */ + setTitle: function BH_setTitle(title) { + // Nothing to do for this host type. + }, + + /** + * Destroy the bottom dock. + */ + destroy: function BH_destroy() { + if (!this._destroyed) { + this._destroyed = true; + + Services.prefs.setIntPref(this.heightPref, this.frame.height); + this._nbox.removeChild(this._splitter); + this._nbox.removeChild(this.frame); + } + + return Promise.resolve(null); + } +} + + +/** + * Host object for the in-browser sidebar + */ +function SidebarHost(hostTab) { + this.hostTab = hostTab; + + EventEmitter.decorate(this); +} + +SidebarHost.prototype = { + type: "side", + + widthPref: "devtools.toolbox.sidebar.width", + + /** + * Create a box in the sidebar of the host tab. + */ + create: function SH_create() { + let deferred = Promise.defer(); + + let gBrowser = this.hostTab.ownerDocument.defaultView.gBrowser; + let ownerDocument = gBrowser.ownerDocument; + + this._splitter = ownerDocument.createElement("splitter"); + this._splitter.setAttribute("class", "devtools-side-splitter"); + + this.frame = ownerDocument.createElement("iframe"); + this.frame.className = "devtools-toolbox-side-iframe"; + this.frame.width = Services.prefs.getIntPref(this.widthPref); + + this._sidebar = gBrowser.getSidebarContainer(this.hostTab.linkedBrowser); + this._sidebar.appendChild(this._splitter); + this._sidebar.appendChild(this.frame); + + let frameLoad = function() { + this.frame.removeEventListener("DOMContentLoaded", frameLoad, true); + this.emit("ready", this.frame); + + deferred.resolve(this.frame); + }.bind(this); + + this.frame.addEventListener("DOMContentLoaded", frameLoad, true); + this.frame.tooltip = "aHTMLTooltip"; + this.frame.setAttribute("src", "about:blank"); + + focusTab(this.hostTab); + + return deferred.promise; + }, + + /** + * Raise the host. + */ + raise: function SH_raise() { + focusTab(this.hostTab); + }, + + /** + * Set the toolbox title. + */ + setTitle: function SH_setTitle(title) { + // Nothing to do for this host type. + }, + + /** + * Destroy the sidebar. + */ + destroy: function SH_destroy() { + if (!this._destroyed) { + this._destroyed = true; + + Services.prefs.setIntPref(this.widthPref, this.frame.width); + this._sidebar.removeChild(this._splitter); + this._sidebar.removeChild(this.frame); + } + + return Promise.resolve(null); + } +} + +/** + * Host object for the toolbox in a separate window + */ +function WindowHost() { + this._boundUnload = this._boundUnload.bind(this); + + EventEmitter.decorate(this); +} + +WindowHost.prototype = { + type: "window", + + WINDOW_URL: "chrome://browser/content/devtools/framework/toolbox-window.xul", + + /** + * Create a new xul window to contain the toolbox. + */ + create: function WH_create() { + let deferred = Promise.defer(); + + let flags = "chrome,centerscreen,resizable,dialog=no"; + let win = Services.ww.openWindow(null, this.WINDOW_URL, "_blank", + flags, null); + + let frameLoad = function(event) { + win.removeEventListener("load", frameLoad, true); + this.frame = win.document.getElementById("toolbox-iframe"); + this.emit("ready", this.frame); + + deferred.resolve(this.frame); + }.bind(this); + + win.addEventListener("load", frameLoad, true); + win.addEventListener("unload", this._boundUnload); + + win.focus(); + + this._window = win; + + return deferred.promise; + }, + + /** + * Catch the user closing the window. + */ + _boundUnload: function(event) { + if (event.target.location != this.WINDOW_URL) { + return; + } + this._window.removeEventListener("unload", this._boundUnload); + + this.emit("window-closed"); + }, + + /** + * Raise the host. + */ + raise: function RH_raise() { + this._window.focus(); + }, + + /** + * Set the toolbox title. + */ + setTitle: function WH_setTitle(title) { + this._window.document.title = title; + }, + + /** + * Destroy the window. + */ + destroy: function WH_destroy() { + if (!this._destroyed) { + this._destroyed = true; + + this._window.removeEventListener("unload", this._boundUnload); + this._window.close(); + } + + return Promise.resolve(null); + } +} + +/** + * Switch to the given tab in a browser and focus the browser window + */ +function focusTab(tab) { + let browserWindow = tab.ownerDocument.defaultView; + browserWindow.focus(); + browserWindow.gBrowser.selectedTab = tab; +}
--- a/browser/devtools/framework/gDevTools.jsm +++ b/browser/devtools/framework/gDevTools.jsm @@ -1,201 +1,27 @@ /* 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/. */ "use strict"; -this.EXPORTED_SYMBOLS = [ "gDevTools", "DevTools", "gDevToolsBrowser", "devtools" ]; +this.EXPORTED_SYMBOLS = [ "gDevTools", "DevTools", "gDevToolsBrowser" ]; const { classes: Cc, interfaces: Ci, utils: Cu } = Components; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource:///modules/devtools/shared/event-emitter.js"); -Cu.import("resource://gre/modules/FileUtils.jsm"); -Cu.import("resource://gre/modules/NetUtil.jsm"); Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js"); - -XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); - -let loader = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {}).Loader; - -// Used when the tools should be loaded from the Firefox package itself (the default) - -var BuiltinProvider = { - load: function(done) { - this.loader = new loader.Loader({ - paths: { - "": "resource://gre/modules/commonjs/", - "main" : "resource:///modules/devtools/main", - "devtools": "resource:///modules/devtools", - "devtools/toolkit": "resource://gre/modules/devtools" - }, - globals: {}, - }); - this.main = loader.main(this.loader, "main"); - - return Promise.resolve(undefined); - }, - - unload: function(reason) { - loader.unload(this.loader, reason); - delete this.loader; - }, -}; - -var SrcdirProvider = { - load: function(done) { - let srcdir = Services.prefs.getComplexValue("devtools.loader.srcdir", - Ci.nsISupportsString); - srcdir = OS.Path.normalize(srcdir.data.trim()); - let devtoolsDir = OS.Path.join(srcdir, "browser/devtools"); - let toolkitDir = OS.Path.join(srcdir, "toolkit/devtools"); - - this.loader = new loader.Loader({ - paths: { - "": "resource://gre/modules/commonjs/", - "devtools/toolkit": "file://" + toolkitDir, - "devtools": "file://" + devtoolsDir, - "main": "file://" + devtoolsDir + "/main.js" - }, - globals: {} - }); - - this.main = loader.main(this.loader, "main"); - - return this._writeManifest(devtoolsDir).then((data) => { - this._writeManifest(toolkitDir); - }).then(null, Cu.reportError); - }, - - unload: function(reason) { - loader.unload(this.loader, reason); - delete this.loader; - }, - - _readFile: function(filename) { - let deferred = Promise.defer(); - let file = new FileUtils.File(filename); - NetUtil.asyncFetch(file, (inputStream, status) => { - if (!Components.isSuccessCode(status)) { - deferred.reject(new Error("Couldn't load manifest: " + filename + "\n")); - return; - } - var data = NetUtil.readInputStreamToString(inputStream, inputStream.available()); - deferred.resolve(data); - }); - return deferred.promise; - }, - - _writeFile: function(filename, data) { - let deferred = Promise.defer(); - let file = new FileUtils.File(filename); - - var ostream = FileUtils.openSafeFileOutputStream(file) - - var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. - createInstance(Ci.nsIScriptableUnicodeConverter); - converter.charset = "UTF-8"; - var istream = converter.convertToInputStream(data); - NetUtil.asyncCopy(istream, ostream, (status) => { - if (!Components.isSuccessCode(status)) { - deferred.reject(new Error("Couldn't write manifest: " + filename + "\n")); - return; - } - - deferred.resolve(null); - }); - return deferred.promise; - }, - - _writeManifest: function(dir) { - return this._readFile(dir + "/jar.mn").then((data) => { - // The file data is contained within inputStream. - // You can read it into a string with - let entries = []; - let lines = data.split(/\n/); - let preprocessed = /^\s*\*/; - let contentEntry = new RegExp("^\\s+content/(\\w+)/(\\S+)\\s+\\((\\S+)\\)"); - for (let line of lines) { - if (preprocessed.test(line)) { - dump("Unable to override preprocessed file: " + line + "\n"); - continue; - } - let match = contentEntry.exec(line); - if (match) { - let entry = "override chrome://" + match[1] + "/content/" + match[2] + "\tfile://" + dir + "/" + match[3]; - entries.push(entry); - } - } - return this._writeFile(dir + "/chrome.manifest", entries.join("\n")); - }).then(() => { - Components.manager.addBootstrappedManifestLocation(new FileUtils.File(dir)); - }); - } -}; - -this.devtools = { - _provider: null, - - get main() this._provider.main, - - // This is a gross gross hack. In one place (computed-view.js) we use - // Iterator, but the addon-sdk loader takes Iterator off the global. - // Give computed-view.js a way to get back to the Iterator until we have - // a chance to fix that crap. - _Iterator: Iterator, - - setProvider: function(provider) { - if (provider === this._provider) { - return; - } - - if (this._provider) { - delete this.require; - this._provider.unload("newprovider"); - gDevTools._teardown(); - } - this._provider = provider; - this._provider.load(); - this.require = loader.Require(this._provider.loader, { id: "devtools" }) - - let exports = this._provider.main; - // Let clients find exports on this object. - Object.getOwnPropertyNames(exports).forEach(key => { - // Lazily load here to avoid triggering lazy inits in the tools. - XPCOMUtils.defineLazyGetter(this, key, () => exports[key]); - }); - }, - - /** - * Choose a default tools provider based on the preferences. - */ - _chooseProvider: function() { - if (Services.prefs.prefHasUserValue("devtools.loader.srcdir")) { - this.setProvider(SrcdirProvider); - } else { - this.setProvider(BuiltinProvider); - } - }, - - /** - * Reload the current provider. - */ - reload: function() { - var events = devtools.require("sdk/system/events"); - events.emit("startupcache-invalidate", {}); - - this._provider.unload("reload"); - delete this._provider; - gDevTools._teardown(); - this._chooseProvider(); - }, -}; +Cu.import("resource:///modules/devtools/EventEmitter.jsm"); +Cu.import("resource:///modules/devtools/ToolDefinitions.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Toolbox", + "resource:///modules/devtools/Toolbox.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "TargetFactory", + "resource:///modules/devtools/Target.jsm"); const FORBIDDEN_IDS = new Set(["toolbox", ""]); /** * DevTools is a class that represents a set of developer tools, it holds a * set of tools and keeps track of open toolboxes in the browser. */ this.DevTools = function DevTools() { @@ -203,16 +29,21 @@ this.DevTools = function DevTools() { this._toolboxes = new Map(); // Map<target, toolbox> // destroy() is an observer's handler so we need to preserve context. this.destroy = this.destroy.bind(this); EventEmitter.decorate(this); Services.obs.addObserver(this.destroy, "quit-application", false); + + // Register the set of default tools + for (let definition of defaultTools) { + this.registerTool(definition); + } } DevTools.prototype = { /** * Register a new developer tool. * * A definition is a light object that holds different information about a * developer tool. This object is not supposed to have any operational code. @@ -353,17 +184,17 @@ DevTools.prototype = { return promise.then(function() { toolbox.raise(); return toolbox; }); } else { // No toolbox for target, create one - toolbox = new devtools.Toolbox(target, toolId, hostType); + toolbox = new Toolbox(target, toolId, hostType); this._toolboxes.set(target, toolbox); toolbox.once("destroyed", function() { this._toolboxes.delete(target); this.emit("toolbox-destroyed", target); }.bind(this)); @@ -407,25 +238,16 @@ DevTools.prototype = { let toolbox = this._toolboxes.get(target); if (toolbox == null) { return; } return toolbox.destroy(); }, /** - * Called to tear down a tools provider. - */ - _teardown: function DT_teardown() { - for (let [target, toolbox] of this._toolboxes) { - toolbox.destroy(); - } - }, - - /** * All browser windows have been closed, tidy up remaining objects. */ destroy: function() { Services.obs.removeObserver(this.destroy, "quit-application"); for (let [key, tool] of this._tools) { this.unregisterTool(key, true); } @@ -457,50 +279,42 @@ let gDevToolsBrowser = { _trackedBrowserWindows: new Set(), /** * This function is for the benefit of Tools:DevToolbox in * browser/base/content/browser-sets.inc and should not be used outside * of there */ toggleToolboxCommand: function(gBrowser) { - let target = devtools.TargetFactory.forTab(gBrowser.selectedTab); + let target = TargetFactory.forTab(gBrowser.selectedTab); let toolbox = gDevTools.getToolbox(target); toolbox ? toolbox.destroy() : gDevTools.showToolbox(target); }, - toggleBrowserToolboxCommand: function(gBrowser) { - let target = devtools.TargetFactory.forWindow(gBrowser.ownerDocument.defaultView); - let toolbox = gDevTools.getToolbox(target); - - toolbox ? toolbox.destroy() - : gDevTools.showToolbox(target, "inspector", Toolbox.HostType.WINDOW); - }, - /** * This function is for the benefit of Tools:{toolId} commands, * triggered from the WebDeveloper menu and keyboard shortcuts. * * selectToolCommand's behavior: * - if the toolbox is closed, * we open the toolbox and select the tool * - if the toolbox is open, and the targetted tool is not selected, * we select it * - if the toolbox is open, and the targetted tool is selected, * and the host is NOT a window, we close the toolbox * - if the toolbox is open, and the targetted tool is selected, * and the host is a window, we raise the toolbox window */ selectToolCommand: function(gBrowser, toolId) { - let target = devtools.TargetFactory.forTab(gBrowser.selectedTab); + let target = TargetFactory.forTab(gBrowser.selectedTab); let toolbox = gDevTools.getToolbox(target); if (toolbox && toolbox.currentToolId == toolId) { - if (toolbox.hostType == devtools.Toolbox.HostType.WINDOW) { + if (toolbox.hostType == Toolbox.HostType.WINDOW) { toolbox.raise(); } else { toolbox.destroy(); } } else { gDevTools.showToolbox(target, toolId); } }, @@ -713,18 +527,18 @@ let gDevToolsBrowser = { /** * Update the "Toggle Tools" checkbox in the developer tools menu. This is * called when a toolbox is created or destroyed. */ _updateMenuCheckbox: function DT_updateMenuCheckbox() { for (let win of gDevToolsBrowser._trackedBrowserWindows) { let hasToolbox = false; - if (devtools.TargetFactory.isKnownTab(win.gBrowser.selectedTab)) { - let target = devtools.TargetFactory.forTab(win.gBrowser.selectedTab); + if (TargetFactory.isKnownTab(win.gBrowser.selectedTab)) { + let target = TargetFactory.forTab(win.gBrowser.selectedTab); if (gDevTools._toolboxes.has(target)) { hasToolbox = true; } } let broadcaster = win.document.getElementById("devtoolsMenuBroadcaster_DevToolbox"); if (hasToolbox) { broadcaster.setAttribute("checked", "true"); @@ -805,27 +619,23 @@ let gDevToolsBrowser = { /** * All browser windows have been closed, tidy up remaining objects. */ destroy: function() { Services.obs.removeObserver(gDevToolsBrowser.destroy, "quit-application"); }, } - this.gDevToolsBrowser = gDevToolsBrowser; gDevTools.on("tool-registered", function(ev, toolId) { let toolDefinition = gDevTools._tools.get(toolId); gDevToolsBrowser._addToolToWindows(toolDefinition); }); gDevTools.on("tool-unregistered", function(ev, toolId) { gDevToolsBrowser._removeToolFromWindows(toolId); }); gDevTools.on("toolbox-ready", gDevToolsBrowser._updateMenuCheckbox); gDevTools.on("toolbox-destroyed", gDevToolsBrowser._updateMenuCheckbox); Services.obs.addObserver(gDevToolsBrowser.destroy, "quit-application", false); - -// Now, load the tools. -devtools._chooseProvider();
deleted file mode 100644 --- a/browser/devtools/framework/sidebar.js +++ /dev/null @@ -1,211 +0,0 @@ -/* -*- 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/. */ - -var Promise = require("sdk/core/promise"); -var EventEmitter = require("devtools/shared/event-emitter"); - -const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; - -/** - * ToolSidebar provides methods to register tabs in the sidebar. - * It's assumed that the sidebar contains a xul:tabbox. - * - * @param {Node} tabbox - * <tabbox> node; - * @param {ToolPanel} panel - * Related ToolPanel instance; - * @param {Boolean} showTabstripe - * Show the tabs. - */ -function ToolSidebar(tabbox, panel, showTabstripe=true) -{ - EventEmitter.decorate(this); - - this._tabbox = tabbox; - this._panelDoc = this._tabbox.ownerDocument; - this._toolPanel = panel; - - this._tabbox.tabpanels.addEventListener("select", this, true); - - this._tabs = new Map(); - - if (!showTabstripe) { - this._tabbox.setAttribute("hidetabs", "true"); - } -} - -exports.ToolSidebar = ToolSidebar; - -ToolSidebar.prototype = { - /** - * Register a tab. A tab is a document. - * The document must have a title, which will be used as the name of the tab. - * - * @param {string} tab uniq id - * @param {string} url - */ - addTab: function ToolSidebar_addTab(id, url, selected=false) { - let iframe = this._panelDoc.createElementNS(XULNS, "iframe"); - iframe.className = "iframe-" + id; - iframe.setAttribute("flex", "1"); - iframe.setAttribute("src", url); - iframe.tooltip = "aHTMLTooltip"; - - let tab = this._tabbox.tabs.appendItem(); - tab.setAttribute("label", ""); // Avoid showing "undefined" while the tab is loading - - let onIFrameLoaded = function() { - tab.setAttribute("label", iframe.contentDocument.title); - iframe.removeEventListener("load", onIFrameLoaded, true); - if ("setPanel" in iframe.contentWindow) { - iframe.contentWindow.setPanel(this._toolPanel, iframe); - } - this.emit(id + "-ready"); - }.bind(this); - - iframe.addEventListener("load", onIFrameLoaded, true); - - let tabpanel = this._panelDoc.createElementNS(XULNS, "tabpanel"); - tabpanel.setAttribute("id", "sidebar-panel-" + id); - tabpanel.appendChild(iframe); - this._tabbox.tabpanels.appendChild(tabpanel); - - this._tooltip = this._panelDoc.createElementNS(XULNS, "tooltip"); - this._tooltip.id = "aHTMLTooltip"; - tabpanel.appendChild(this._tooltip); - this._tooltip.page = true; - - tab.linkedPanel = "sidebar-panel-" + id; - - // We store the index of this tab. - this._tabs.set(id, tab); - - if (selected) { - // For some reason I don't understand, if we call this.select in this - // event loop (after inserting the tab), the tab will never get the - // the "selected" attribute set to true. - this._panelDoc.defaultView.setTimeout(function() { - this.select(id); - }.bind(this), 10); - } - - this.emit("new-tab-registered", id); - }, - - /** - * Select a specific tab. - */ - select: function ToolSidebar_select(id) { - let tab = this._tabs.get(id); - if (tab) { - this._tabbox.selectedTab = tab; - } - }, - - /** - * Return the id of the selected tab. - */ - getCurrentTabID: function ToolSidebar_getCurrentTabID() { - let currentID = null; - for (let [id, tab] of this._tabs) { - if (this._tabbox.tabs.selectedItem == tab) { - currentID = id; - break; - } - } - return currentID; - }, - - /** - * Returns the requested tab based on the id. - * - * @param String id - * unique id of the requested tab. - */ - getTab: function ToolSidebar_getTab(id) { - return this._tabbox.tabpanels.querySelector("#sidebar-panel-" + id); - }, - - /** - * Event handler. - */ - handleEvent: function ToolSidebar_eventHandler(event) { - if (event.type == "select") { - let previousTool = this._currentTool; - this._currentTool = this.getCurrentTabID(); - if (previousTool) { - this.emit(previousTool + "-unselected"); - } - - this.emit(this._currentTool + "-selected"); - this.emit("select", this._currentTool); - } - }, - - /** - * Toggle sidebar's visibility state. - */ - toggle: function ToolSidebar_toggle() { - if (this._tabbox.hasAttribute("hidden")) { - this.show(); - } else { - this.hide(); - } - }, - - /** - * Show the sidebar. - */ - show: function ToolSidebar_show() { - this._tabbox.removeAttribute("hidden"); - }, - - /** - * Show the sidebar. - */ - hide: function ToolSidebar_hide() { - this._tabbox.setAttribute("hidden", "true"); - }, - - /** - * Return the window containing the tab content. - */ - getWindowForTab: function ToolSidebar_getWindowForTab(id) { - if (!this._tabs.has(id)) { - return null; - } - - let panel = this._panelDoc.getElementById(this._tabs.get(id).linkedPanel); - return panel.firstChild.contentWindow; - }, - - /** - * Clean-up. - */ - destroy: function ToolSidebar_destroy() { - if (this._destroyed) { - return Promise.resolve(null); - } - this._destroyed = true; - - this._tabbox.tabpanels.removeEventListener("select", this, true); - - while (this._tabbox.tabpanels.hasChildNodes()) { - this._tabbox.tabpanels.removeChild(this._tabbox.tabpanels.firstChild); - } - - while (this._tabbox.tabs.hasChildNodes()) { - this._tabbox.tabs.removeChild(this._tabbox.tabs.firstChild); - } - - this._tabs = null; - this._tabbox = null; - this._panelDoc = null; - this._toolPanel = null; - - return Promise.resolve(null); - }, -}
deleted file mode 100644 --- a/browser/devtools/framework/target.js +++ /dev/null @@ -1,584 +0,0 @@ -/* 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/. */ - -"use strict"; - -const {Cc, Ci, Cu} = require("chrome"); - -var Promise = require("sdk/core/promise"); -var EventEmitter = require("devtools/shared/event-emitter"); - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer", - "resource://gre/modules/devtools/dbg-server.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "DebuggerClient", - "resource://gre/modules/devtools/dbg-client.jsm"); - -const targets = new WeakMap(); -const promiseTargets = new WeakMap(); - -/** - * Functions for creating Targets - */ -exports.TargetFactory = { - /** - * Construct a Target - * @param {XULTab} tab - * The tab to use in creating a new target. - * - * @return A target object - */ - forTab: function TF_forTab(tab) { - let target = targets.get(tab); - if (target == null) { - target = new TabTarget(tab); - targets.set(tab, target); - } - return target; - }, - - /** - * Return a promise of a Target for a remote tab. - * @param {Object} options - * The options object has the following properties: - * { - * form: the remote protocol form of a tab, - * client: a DebuggerClient instance, - * chrome: true if the remote target is the whole process - * } - * - * @return A promise of a target object - */ - forRemoteTab: function TF_forRemoteTab(options) { - let promise = promiseTargets.get(options); - if (promise == null) { - let target = new TabTarget(options); - promise = target.makeRemote().then(() => target); - promiseTargets.set(options, promise); - } - return promise; - }, - - /** - * Creating a target for a tab that is being closed is a problem because it - * allows a leak as a result of coming after the close event which normally - * clears things up. This function allows us to ask if there is a known - * target for a tab without creating a target - * @return true/false - */ - isKnownTab: function TF_isKnownTab(tab) { - return targets.has(tab); - }, - - /** - * Construct a Target - * @param {nsIDOMWindow} window - * The chromeWindow to use in creating a new target - * @return A target object - */ - forWindow: function TF_forWindow(window) { - let target = targets.get(window); - if (target == null) { - target = new WindowTarget(window); - targets.set(window, target); - } - return target; - }, - - /** - * Get all of the targets known to the local browser instance - * @return An array of target objects - */ - allTargets: function TF_allTargets() { - let windows = []; - let wm = Cc["@mozilla.org/appshell/window-mediator;1"] - .getService(Ci.nsIWindowMediator); - let en = wm.getXULWindowEnumerator(null); - while (en.hasMoreElements()) { - windows.push(en.getNext()); - } - - return windows.map(function(window) { - return TargetFactory.forWindow(window); - }); - }, -}; - -/** - * The 'version' property allows the developer tools equivalent of browser - * detection. Browser detection is evil, however while we don't know what we - * will need to detect in the future, it is an easy way to postpone work. - * We should be looking to use 'supports()' in place of version where - * possible. - */ -function getVersion() { - // FIXME: return something better - return 20; -} - -/** - * A better way to support feature detection, but we're not yet at a place - * where we have the features well enough defined for this to make lots of - * sense. - */ -function supports(feature) { - // FIXME: return something better - return false; -}; - -/** - * A Target represents something that we can debug. Targets are generally - * read-only. Any changes that you wish to make to a target should be done via - * a Tool that attaches to the target. i.e. a Target is just a pointer saying - * "the thing to debug is over there". - * - * Providing a generalized abstraction of a web-page or web-browser (available - * either locally or remotely) is beyond the scope of this class (and maybe - * also beyond the scope of this universe) However Target does attempt to - * abstract some common events and read-only properties common to many Tools. - * - * Supported read-only properties: - * - name, isRemote, url - * - * Target extends EventEmitter and provides support for the following events: - * - close: The target window has been closed. All tools attached to this - * target should close. This event is not currently cancelable. - * - navigate: The target window has navigated to a different URL - * - * Optional events: - * - will-navigate: The target window will navigate to a different URL - * - hidden: The target is not visible anymore (for TargetTab, another tab is selected) - * - visible: The target is visible (for TargetTab, tab is selected) - * - * Target also supports 2 functions to help allow 2 different versions of - * Firefox debug each other. The 'version' property is the equivalent of - * browser detection - simple and easy to implement but gets fragile when things - * are not quite what they seem. The 'supports' property is the equivalent of - * feature detection - harder to setup, but more robust long-term. - * - * Comparing Targets: 2 instances of a Target object can point at the same - * thing, so t1 !== t2 and t1 != t2 even when they represent the same object. - * To compare to targets use 't1.equals(t2)'. - */ -function Target() { - throw new Error("Use TargetFactory.newXXX or Target.getXXX to create a Target in place of 'new Target()'"); -} - -Object.defineProperty(Target.prototype, "version", { - get: getVersion, - enumerable: true -}); - - -/** - * A TabTarget represents a page living in a browser tab. Generally these will - * be web pages served over http(s), but they don't have to be. - */ -function TabTarget(tab) { - EventEmitter.decorate(this); - this.destroy = this.destroy.bind(this); - this._handleThreadState = this._handleThreadState.bind(this); - this.on("thread-resumed", this._handleThreadState); - this.on("thread-paused", this._handleThreadState); - // Only real tabs need initialization here. Placeholder objects for remote - // targets will be initialized after a makeRemote method call. - if (tab && !["client", "form", "chrome"].every(tab.hasOwnProperty, tab)) { - this._tab = tab; - this._setupListeners(); - } else { - this._form = tab.form; - this._client = tab.client; - this._chrome = tab.chrome; - } -} - -TabTarget.prototype = { - _webProgressListener: null, - - supports: supports, - get version() { return getVersion(); }, - - get tab() { - return this._tab; - }, - - get form() { - return this._form; - }, - - get client() { - return this._client; - }, - - get chrome() { - return this._chrome; - }, - - get window() { - // Be extra careful here, since this may be called by HS_getHudByWindow - // during shutdown. - if (this._tab && this._tab.linkedBrowser) { - return this._tab.linkedBrowser.contentWindow; - } - }, - - get name() { - return this._tab ? this._tab.linkedBrowser.contentDocument.title : - this._form.title; - }, - - get url() { - return this._tab ? this._tab.linkedBrowser.contentDocument.location.href : - this._form.url; - }, - - get isRemote() { - return !this.isLocalTab; - }, - - get isLocalTab() { - return !!this._tab; - }, - - get isThreadPaused() { - return !!this._isThreadPaused; - }, - - /** - * Adds remote protocol capabilities to the target, so that it can be used - * for tools that support the Remote Debugging Protocol even for local - * connections. - */ - makeRemote: function TabTarget_makeRemote() { - if (this._remote) { - return this._remote.promise; - } - - this._remote = Promise.defer(); - - if (this.isLocalTab) { - // Since a remote protocol connection will be made, let's start the - // DebuggerServer here, once and for all tools. - if (!DebuggerServer.initialized) { - DebuggerServer.init(); - DebuggerServer.addBrowserActors(); - } - - this._client = new DebuggerClient(DebuggerServer.connectPipe()); - // A local TabTarget will never perform chrome debugging. - this._chrome = false; - } - - this._setupRemoteListeners(); - - if (this.isRemote) { - // In the remote debugging case, the protocol connection will have been - // already initialized in the connection screen code. - this._remote.resolve(null); - } else { - this._client.connect((aType, aTraits) => { - this._client.listTabs(aResponse => { - this._form = aResponse.tabs[aResponse.selected]; - - this._client.attachTab(this._form.actor, (aResponse, aTabClient) => { - if (!aTabClient) { - this._remote.reject("Unable to attach to the tab"); - return; - } - this.threadActor = aResponse.threadActor; - this._remote.resolve(null); - }); - }); - }); - } - - return this._remote.promise; - }, - - /** - * Listen to the different events. - */ - _setupListeners: function TabTarget__setupListeners() { - this._webProgressListener = new TabWebProgressListener(this); - this.tab.linkedBrowser.addProgressListener(this._webProgressListener); - this.tab.addEventListener("TabClose", this); - this.tab.parentNode.addEventListener("TabSelect", this); - this.tab.ownerDocument.defaultView.addEventListener("unload", this); - }, - - /** - * Setup listeners for remote debugging, updating existing ones as necessary. - */ - _setupRemoteListeners: function TabTarget__setupRemoteListeners() { - this.client.addListener("tabDetached", this.destroy); - - this._onTabNavigated = function onRemoteTabNavigated(aType, aPacket) { - let event = Object.create(null); - event.url = aPacket.url; - event.title = aPacket.title; - // Send any stored event payload (DOMWindow or nsIRequest) for backwards - // compatibility with non-remotable tools. - event._navPayload = this._navPayload; - if (aPacket.state == "start") { - this.emit("will-navigate", event); - } else { - this.emit("navigate", event); - } - this._navPayload = null; - }.bind(this); - this.client.addListener("tabNavigated", this._onTabNavigated); - }, - - /** - * Handle tabs events. - */ - handleEvent: function (event) { - switch (event.type) { - case "TabClose": - case "unload": - this.destroy(); - break; - case "TabSelect": - if (this.tab.selected) { - this.emit("visible", event); - } else { - this.emit("hidden", event); - } - break; - } - }, - - /** - * Handle script status. - */ - _handleThreadState: function(event) { - switch (event) { - case "thread-resumed": - this._isThreadPaused = false; - break; - case "thread-paused": - this._isThreadPaused = true; - break; - } - }, - - /** - * Target is not alive anymore. - */ - destroy: function() { - // If several things call destroy then we give them all the same - // destruction promise so we're sure to destroy only once - if (this._destroyer) { - return this._destroyer.promise; - } - - this._destroyer = Promise.defer(); - - // Before taking any action, notify listeners that destruction is imminent. - this.emit("close"); - - // First of all, do cleanup tasks that pertain to both remoted and - // non-remoted targets. - this.off("thread-resumed", this._handleThreadState); - this.off("thread-paused", this._handleThreadState); - - if (this._tab) { - if (this._webProgressListener) { - this._webProgressListener.destroy(); - } - - this._tab.ownerDocument.defaultView.removeEventListener("unload", this); - this._tab.removeEventListener("TabClose", this); - this._tab.parentNode.removeEventListener("TabSelect", this); - } - - // If this target was not remoted, the promise will be resolved before the - // function returns. - if (this._tab && !this._client) { - targets.delete(this._tab); - this._tab = null; - this._client = null; - this._form = null; - this._remote = null; - - this._destroyer.resolve(null); - } else if (this._client) { - // If, on the other hand, this target was remoted, the promise will be - // resolved after the remote connection is closed. - this.client.removeListener("tabNavigated", this._onTabNavigated); - this.client.removeListener("tabDetached", this.destroy); - - this._client.close(function onClosed() { - if (this._tab) { - targets.delete(this._tab); - } else { - promiseTargets.delete(this._form); - } - this._client = null; - this._tab = null; - this._form = null; - this._remote = null; - - this._destroyer.resolve(null); - }.bind(this)); - } - - return this._destroyer.promise; - }, - - toString: function() { - return 'TabTarget:' + (this._tab ? this._tab : (this._form && this._form.actor)); - }, -}; - - -/** - * WebProgressListener for TabTarget. - * - * @param object aTarget - * The TabTarget instance to work with. - */ -function TabWebProgressListener(aTarget) { - this.target = aTarget; -} - -TabWebProgressListener.prototype = { - target: null, - - QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]), - - onStateChange: function TWPL_onStateChange(progress, request, flag, status) { - let isStart = flag & Ci.nsIWebProgressListener.STATE_START; - let isDocument = flag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT; - let isNetwork = flag & Ci.nsIWebProgressListener.STATE_IS_NETWORK; - let isRequest = flag & Ci.nsIWebProgressListener.STATE_IS_REQUEST; - - // Skip non-interesting states. - if (!isStart || !isDocument || !isRequest || !isNetwork) { - return; - } - - // emit event if the top frame is navigating - if (this.target && this.target.window == progress.DOMWindow) { - // Emit the event if the target is not remoted or store the payload for - // later emission otherwise. - if (this.target._client) { - this.target._navPayload = request; - } else { - this.target.emit("will-navigate", request); - } - } - }, - - onProgressChange: function() {}, - onSecurityChange: function() {}, - onStatusChange: function() {}, - - onLocationChange: function TWPL_onLocationChange(webProgress, request, URI, flags) { - if (this.target && - !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)) { - let window = webProgress.DOMWindow; - // Emit the event if the target is not remoted or store the payload for - // later emission otherwise. - if (this.target._client) { - this.target._navPayload = window; - } else { - this.target.emit("navigate", window); - } - } - }, - - /** - * Destroy the progress listener instance. - */ - destroy: function TWPL_destroy() { - if (this.target.tab) { - this.target.tab.linkedBrowser.removeProgressListener(this); - } - this.target._webProgressListener = null; - this.target = null; - } -}; - - -/** - * A WindowTarget represents a page living in a xul window or panel. Generally - * these will have a chrome: URL - */ -function WindowTarget(window) { - EventEmitter.decorate(this); - this._window = window; - this._setupListeners(); -} - -WindowTarget.prototype = { - supports: supports, - get version() { return getVersion(); }, - - get window() { - return this._window; - }, - - get name() { - return this._window.document.title; - }, - - get url() { - return this._window.document.location.href; - }, - - get isRemote() { - return false; - }, - - get isLocalTab() { - return false; - }, - - get isThreadPaused() { - return !!this._isThreadPaused; - }, - - /** - * Listen to the different events. - */ - _setupListeners: function() { - this._handleThreadState = this._handleThreadState.bind(this); - this.on("thread-paused", this._handleThreadState); - this.on("thread-resumed", this._handleThreadState); - }, - - _handleThreadState: function(event) { - switch (event) { - case "thread-resumed": - this._isThreadPaused = false; - break; - case "thread-paused": - this._isThreadPaused = true; - break; - } - }, - - /** - * Target is not alive anymore. - */ - destroy: function() { - if (!this._destroyed) { - this._destroyed = true; - - this.off("thread-paused", this._handleThreadState); - this.off("thread-resumed", this._handleThreadState); - this.emit("close"); - - targets.delete(this._window); - this._window = null; - } - - return Promise.resolve(null); - }, - - toString: function() { - return 'WindowTarget:' + this.window; - }, -};
--- a/browser/devtools/framework/test/browser_devtools_api.js +++ b/browser/devtools/framework/test/browser_devtools_api.js @@ -2,18 +2,20 @@ * http://creativecommons.org/publicdomain/zero/1.0/ */ // Tests devtools API const Cu = Components.utils; const toolId = "test-tool"; let tempScope = {}; -Cu.import("resource:///modules/devtools/shared/event-emitter.js", tempScope); +Cu.import("resource:///modules/devtools/EventEmitter.jsm", tempScope); let EventEmitter = tempScope.EventEmitter; +Cu.import("resource:///modules/devtools/Target.jsm", tempScope); +let TargetFactory = tempScope.TargetFactory; function test() { addTab("about:blank", function(aBrowser, aTab) { runTests(aTab); }); } function runTests(aTab) {
--- a/browser/devtools/framework/test/browser_new_activation_workflow.js +++ b/browser/devtools/framework/test/browser_new_activation_workflow.js @@ -3,16 +3,18 @@ // Tests devtools API const Cu = Components.utils; let toolbox, target; let tempScope = {}; +Cu.import("resource:///modules/devtools/Target.jsm", tempScope); +let TargetFactory = tempScope.TargetFactory; function test() { addTab("about:blank", function(aBrowser, aTab) { target = TargetFactory.forTab(gBrowser.selectedTab); loadWebConsole(aTab).then(function() { console.log('loaded'); }, console.error); });
--- a/browser/devtools/framework/test/browser_target_events.js +++ b/browser/devtools/framework/test/browser_target_events.js @@ -1,12 +1,16 @@ /* vim: set ts=2 et sw=2 tw=80: */ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ +var tempScope = {}; +Cu.import("resource:///modules/devtools/Target.jsm", tempScope); +var TargetFactory = tempScope.TargetFactory; + var target; function test() { waitForExplicitFinish(); gBrowser.selectedTab = gBrowser.addTab(); gBrowser.selectedBrowser.addEventListener("load", onLoad, true);
--- a/browser/devtools/framework/test/browser_toolbox_dynamic_registration.js +++ b/browser/devtools/framework/test/browser_toolbox_dynamic_registration.js @@ -1,14 +1,18 @@ /* vim: set ts=2 et sw=2 tw=80: */ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ let toolbox; +let temp = {}; +Cu.import("resource:///modules/devtools/Target.jsm", temp); +let TargetFactory = temp.TargetFactory; + function test() { waitForExplicitFinish(); gBrowser.selectedTab = gBrowser.addTab(); let target = TargetFactory.forTab(gBrowser.selectedTab); gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
--- a/browser/devtools/framework/test/browser_toolbox_hosts.js +++ b/browser/devtools/framework/test/browser_toolbox_hosts.js @@ -1,17 +1,21 @@ /* vim: set ts=2 et sw=2 tw=80: */ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ let temp = {} Cu.import("resource:///modules/devtools/gDevTools.jsm", temp); let DevTools = temp.DevTools; -let Toolbox = devtools.Toolbox; +Cu.import("resource:///modules/devtools/Toolbox.jsm", temp); +let Toolbox = temp.Toolbox; + +Cu.import("resource:///modules/devtools/Target.jsm", temp); +let TargetFactory = temp.TargetFactory; let toolbox, target; function test() { waitForExplicitFinish(); gBrowser.selectedTab = gBrowser.addTab();
--- a/browser/devtools/framework/test/browser_toolbox_ready.js +++ b/browser/devtools/framework/test/browser_toolbox_ready.js @@ -1,12 +1,16 @@ /* vim: set ts=2 et sw=2 tw=80: */ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ +let tempScope = {}; +Cu.import("resource:///modules/devtools/Target.jsm", tempScope); +let TargetFactory = tempScope.TargetFactory; + function test() { waitForExplicitFinish(); gBrowser.selectedTab = gBrowser.addTab(); let target = TargetFactory.forTab(gBrowser.selectedTab); gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
--- a/browser/devtools/framework/test/browser_toolbox_sidebar.js +++ b/browser/devtools/framework/test/browser_toolbox_sidebar.js @@ -1,14 +1,18 @@ /* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ function test() { const Cu = Components.utils; - let {ToolSidebar} = devtools.require("devtools/framework/sidebar"); + let tempScope = {}; + Cu.import("resource:///modules/devtools/gDevTools.jsm", tempScope); + Cu.import("resource:///modules/devtools/Target.jsm", tempScope); + Cu.import("resource:///modules/devtools/Sidebar.jsm", tempScope); + let {TargetFactory: TargetFactory, gDevTools: gDevTools, ToolSidebar: ToolSidebar} = tempScope; const toolURL = "data:text/xml;charset=utf8,<?xml version='1.0'?>" + "<?xml-stylesheet href='chrome://browser/skin/devtools/common.css' type='text/css'?>" + "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'>" + "<hbox flex='1'><description flex='1'>foo</description><splitter class='devtools-side-splitter'/>" + "<tabbox flex='1' id='sidebar' class='devtools-sidebar-tabs'><tabs/><tabpanels flex='1'/></tabbox>" + "</hbox>" + "</window>";
--- a/browser/devtools/framework/test/browser_toolbox_window_shortcuts.js +++ b/browser/devtools/framework/test/browser_toolbox_window_shortcuts.js @@ -1,12 +1,15 @@ /* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ -let Toolbox = devtools.Toolbox; +let temp = {}; +Cu.import("resource:///modules/devtools/Toolbox.jsm", temp); +let Toolbox = temp.Toolbox; +temp = null; let toolbox, toolIDs, idIndex; function test() { waitForExplicitFinish(); if (window.navigator.userAgent.indexOf("Mac OS X 10.8") != -1 || window.navigator.userAgent.indexOf("Windows NT 5.1") != -1) {
--- a/browser/devtools/framework/test/browser_toolbox_window_title_changes.js +++ b/browser/devtools/framework/test/browser_toolbox_window_title_changes.js @@ -1,13 +1,21 @@ /* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ -let Toolbox = devtools.Toolbox; let temp = {}; +Cu.import("resource:///modules/devtools/Toolbox.jsm", temp); +let Toolbox = temp.Toolbox; +temp = {}; +Cu.import("resource:///modules/devtools/Target.jsm", temp); +let TargetFactory = temp.TargetFactory; +temp = {}; +Cu.import("resource:///modules/devtools/gDevTools.jsm", temp); +let gDevTools = temp.gDevTools; +temp = {}; Cu.import("resource://gre/modules/Services.jsm", temp); let Services = temp.Services; temp = null; function test() { waitForExplicitFinish(); const URL_1 = "data:text/plain;charset=UTF-8,abcde";
--- a/browser/devtools/framework/test/head.js +++ b/browser/devtools/framework/test/head.js @@ -1,23 +1,20 @@ /* 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/. */ -let TargetFactory = gDevTools.TargetFactory; - let tempScope = {}; +Components.utils.import("resource:///modules/devtools/Target.jsm", tempScope); +let TargetFactory = tempScope.TargetFactory; Components.utils.import("resource://gre/modules/devtools/Console.jsm", tempScope); let console = tempScope.console; Components.utils.import("resource://gre/modules/commonjs/sdk/core/promise.js", tempScope); let Promise = tempScope.Promise; -let {devtools} = Components.utils.import("resource:///modules/devtools/gDevTools.jsm", {}); -let TargetFactory = devtools.TargetFactory; - /** * Open a new tab at a URL and call a callback on load */ function addTab(aURL, aCallback) { waitForExplicitFinish(); gBrowser.selectedTab = gBrowser.addTab();
deleted file mode 100644 --- a/browser/devtools/framework/toolbox-hosts.js +++ /dev/null @@ -1,282 +0,0 @@ -/* 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/. */ - -"use strict"; - -const {Cu} = require("chrome"); - -let Promise = require("sdk/core/promise"); -let EventEmitter = require("devtools/shared/event-emitter"); - -Cu.import("resource://gre/modules/Services.jsm"); - -/** - * A toolbox host represents an object that contains a toolbox (e.g. the - * sidebar or a separate window). Any host object should implement the - * following functions: - * - * create() - create the UI and emit a 'ready' event when the UI is ready to use - * destroy() - destroy the host's UI - */ - -exports.Hosts = { - "bottom": BottomHost, - "side": SidebarHost, - "window": WindowHost -} - -/** - * Host object for the dock on the bottom of the browser - */ -function BottomHost(hostTab) { - this.hostTab = hostTab; - - EventEmitter.decorate(this); -} - -BottomHost.prototype = { - type: "bottom", - - heightPref: "devtools.toolbox.footer.height", - - /** - * Create a box at the bottom of the host tab. - */ - create: function BH_create() { - let deferred = Promise.defer(); - - let gBrowser = this.hostTab.ownerDocument.defaultView.gBrowser; - let ownerDocument = gBrowser.ownerDocument; - - this._splitter = ownerDocument.createElement("splitter"); - this._splitter.setAttribute("class", "devtools-horizontal-splitter"); - - this.frame = ownerDocument.createElement("iframe"); - this.frame.className = "devtools-toolbox-bottom-iframe"; - this.frame.height = Services.prefs.getIntPref(this.heightPref); - - this._nbox = gBrowser.getNotificationBox(this.hostTab.linkedBrowser); - this._nbox.appendChild(this._splitter); - this._nbox.appendChild(this.frame); - - let frameLoad = function() { - this.frame.removeEventListener("DOMContentLoaded", frameLoad, true); - this.emit("ready", this.frame); - - deferred.resolve(this.frame); - }.bind(this); - - this.frame.tooltip = "aHTMLTooltip"; - this.frame.addEventListener("DOMContentLoaded", frameLoad, true); - - // we have to load something so we can switch documents if we have to - this.frame.setAttribute("src", "about:blank"); - - focusTab(this.hostTab); - - return deferred.promise; - }, - - /** - * Raise the host. - */ - raise: function BH_raise() { - focusTab(this.hostTab); - }, - - /** - * Set the toolbox title. - */ - setTitle: function BH_setTitle(title) { - // Nothing to do for this host type. - }, - - /** - * Destroy the bottom dock. - */ - destroy: function BH_destroy() { - if (!this._destroyed) { - this._destroyed = true; - - Services.prefs.setIntPref(this.heightPref, this.frame.height); - this._nbox.removeChild(this._splitter); - this._nbox.removeChild(this.frame); - } - - return Promise.resolve(null); - } -} - - -/** - * Host object for the in-browser sidebar - */ -function SidebarHost(hostTab) { - this.hostTab = hostTab; - - EventEmitter.decorate(this); -} - -SidebarHost.prototype = { - type: "side", - - widthPref: "devtools.toolbox.sidebar.width", - - /** - * Create a box in the sidebar of the host tab. - */ - create: function SH_create() { - let deferred = Promise.defer(); - - let gBrowser = this.hostTab.ownerDocument.defaultView.gBrowser; - let ownerDocument = gBrowser.ownerDocument; - - this._splitter = ownerDocument.createElement("splitter"); - this._splitter.setAttribute("class", "devtools-side-splitter"); - - this.frame = ownerDocument.createElement("iframe"); - this.frame.className = "devtools-toolbox-side-iframe"; - this.frame.width = Services.prefs.getIntPref(this.widthPref); - - this._sidebar = gBrowser.getSidebarContainer(this.hostTab.linkedBrowser); - this._sidebar.appendChild(this._splitter); - this._sidebar.appendChild(this.frame); - - let frameLoad = function() { - this.frame.removeEventListener("DOMContentLoaded", frameLoad, true); - this.emit("ready", this.frame); - - deferred.resolve(this.frame); - }.bind(this); - - this.frame.addEventListener("DOMContentLoaded", frameLoad, true); - this.frame.tooltip = "aHTMLTooltip"; - this.frame.setAttribute("src", "about:blank"); - - focusTab(this.hostTab); - - return deferred.promise; - }, - - /** - * Raise the host. - */ - raise: function SH_raise() { - focusTab(this.hostTab); - }, - - /** - * Set the toolbox title. - */ - setTitle: function SH_setTitle(title) { - // Nothing to do for this host type. - }, - - /** - * Destroy the sidebar. - */ - destroy: function SH_destroy() { - if (!this._destroyed) { - this._destroyed = true; - - Services.prefs.setIntPref(this.widthPref, this.frame.width); - this._sidebar.removeChild(this._splitter); - this._sidebar.removeChild(this.frame); - } - - return Promise.resolve(null); - } -} - -/** - * Host object for the toolbox in a separate window - */ -function WindowHost() { - this._boundUnload = this._boundUnload.bind(this); - - EventEmitter.decorate(this); -} - -WindowHost.prototype = { - type: "window", - - WINDOW_URL: "chrome://browser/content/devtools/framework/toolbox-window.xul", - - /** - * Create a new xul window to contain the toolbox. - */ - create: function WH_create() { - let deferred = Promise.defer(); - - let flags = "chrome,centerscreen,resizable,dialog=no"; - let win = Services.ww.openWindow(null, this.WINDOW_URL, "_blank", - flags, null); - - let frameLoad = function(event) { - win.removeEventListener("load", frameLoad, true); - this.frame = win.document.getElementById("toolbox-iframe"); - this.emit("ready", this.frame); - - deferred.resolve(this.frame); - }.bind(this); - - win.addEventListener("load", frameLoad, true); - win.addEventListener("unload", this._boundUnload); - - win.focus(); - - this._window = win; - - return deferred.promise; - }, - - /** - * Catch the user closing the window. - */ - _boundUnload: function(event) { - if (event.target.location != this.WINDOW_URL) { - return; - } - this._window.removeEventListener("unload", this._boundUnload); - - this.emit("window-closed"); - }, - - /** - * Raise the host. - */ - raise: function RH_raise() { - this._window.focus(); - }, - - /** - * Set the toolbox title. - */ - setTitle: function WH_setTitle(title) { - this._window.document.title = title; - }, - - /** - * Destroy the window. - */ - destroy: function WH_destroy() { - if (!this._destroyed) { - this._destroyed = true; - - this._window.removeEventListener("unload", this._boundUnload); - this._window.close(); - } - - return Promise.resolve(null); - } -} - -/** - * Switch to the given tab in a browser and focus the browser window - */ -function focusTab(tab) { - let browserWindow = tab.ownerDocument.defaultView; - browserWindow.focus(); - browserWindow.gBrowser.selectedTab = tab; -}
deleted file mode 100644 --- a/browser/devtools/framework/toolbox.js +++ /dev/null @@ -1,680 +0,0 @@ -/* 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/. */ - -"use strict"; - -const {Cc, Ci, Cu} = require("chrome"); - -let Promise = require("sdk/core/promise"); -let EventEmitter = require("devtools/shared/event-emitter"); - -Cu.import('resource://gre/modules/XPCOMUtils.jsm'); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource:///modules/devtools/gDevTools.jsm"); - -loader.lazyGetter(this, "Hosts", () => require("devtools/framework/toolbox-hosts").Hosts); - -XPCOMUtils.defineLazyModuleGetter(this, "CommandUtils", - "resource:///modules/devtools/DeveloperToolbar.jsm"); - -XPCOMUtils.defineLazyGetter(this, "toolboxStrings", function() { - let bundle = Services.strings.createBundle("chrome://browser/locale/devtools/toolbox.properties"); - let l10n = function(aName, ...aArgs) { - try { - if (aArgs.length == 0) { - return bundle.GetStringFromName(aName); - } else { - return bundle.formatStringFromName(aName, aArgs, aArgs.length); - } - } catch (ex) { - Services.console.logStringMessage("Error reading '" + aName + "'"); - } - }; - return l10n; -}); - -XPCOMUtils.defineLazyGetter(this, "Requisition", function() { - let scope = {}; - Cu.import("resource://gre/modules/devtools/Require.jsm", scope); - Cu.import("resource:///modules/devtools/gcli.jsm", scope); - - let req = scope.require; - return req('gcli/cli').Requisition; -}); - -/** - * A "Toolbox" is the component that holds all the tools for one specific - * target. Visually, it's a document that includes the tools tabs and all - * the iframes where the tool panels will be living in. - * - * @param {object} target - * The object the toolbox is debugging. - * @param {string} selectedTool - * Tool to select initially - * @param {Toolbox.HostType} hostType - * Type of host that will host the toolbox (e.g. sidebar, window) - */ -function Toolbox(target, selectedTool, hostType) { - this._target = target; - this._toolPanels = new Map(); - - this._toolRegistered = this._toolRegistered.bind(this); - this._toolUnregistered = this._toolUnregistered.bind(this); - this.destroy = this.destroy.bind(this); - - this._target.on("close", this.destroy); - - if (!hostType) { - hostType = Services.prefs.getCharPref(this._prefs.LAST_HOST); - } - if (!selectedTool) { - selectedTool = Services.prefs.getCharPref(this._prefs.LAST_TOOL); - } - let definitions = gDevTools.getToolDefinitionMap(); - if (!definitions.get(selectedTool)) { - selectedTool = "webconsole"; - } - this._defaultToolId = selectedTool; - - this._host = this._createHost(hostType); - - EventEmitter.decorate(this); - - this._refreshHostTitle = this._refreshHostTitle.bind(this); - this._target.on("navigate", this._refreshHostTitle); - this.on("host-changed", this._refreshHostTitle); - this.on("select", this._refreshHostTitle); - - gDevTools.on("tool-registered", this._toolRegistered); - gDevTools.on("tool-unregistered", this._toolUnregistered); -} -exports.Toolbox = Toolbox; - -/** - * The toolbox can be 'hosted' either embedded in a browser window - * or in a separate window. - */ -Toolbox.HostType = { - BOTTOM: "bottom", - SIDE: "side", - WINDOW: "window" -} - -Toolbox.prototype = { - _URL: "chrome://browser/content/devtools/framework/toolbox.xul", - - _prefs: { - LAST_HOST: "devtools.toolbox.host", - LAST_TOOL: "devtools.toolbox.selectedTool", - SIDE_ENABLED: "devtools.toolbox.sideEnabled" - }, - - HostType: Toolbox.HostType, - - /** - * Returns a *copy* of the _toolPanels collection. - * - * @return {Map} panels - * All the running panels in the toolbox - */ - getToolPanels: function TB_getToolPanels() { - let panels = new Map(); - - for (let [key, value] of this._toolPanels) { - panels.set(key, value); - } - return panels; - }, - - /** - * Access the panel for a given tool - */ - getPanel: function TBOX_getPanel(id) { - return this.getToolPanels().get(id); - }, - - /** - * This is a shortcut for getPanel(currentToolId) because it is much more - * likely that we're going to want to get the panel that we've just made - * visible - */ - getCurrentPanel: function TBOX_getCurrentPanel() { - return this.getToolPanels().get(this.currentToolId); - }, - - /** - * Get/alter the target of a Toolbox so we're debugging something different. - * See Target.jsm for more details. - * TODO: Do we allow |toolbox.target = null;| ? - */ - get target() { - return this._target; - }, - - /** - * Get/alter the host of a Toolbox, i.e. is it in browser or in a separate - * tab. See HostType for more details. - */ - get hostType() { - return this._host.type; - }, - - /** - * Get/alter the currently displayed tool. - */ - get currentToolId() { - return this._currentToolId; - }, - - set currentToolId(value) { - this._currentToolId = value; - }, - - /** - * Get the iframe containing the toolbox UI. - */ - get frame() { - return this._host.frame; - }, - - /** - * Shortcut to the document containing the toolbox UI - */ - get doc() { - return this.frame.contentDocument; - }, - - /** - * Open the toolbox - */ - open: function TBOX_open() { - let deferred = Promise.defer(); - - this._host.create().then(function(iframe) { - let domReady = function() { - iframe.removeEventListener("DOMContentLoaded", domReady, true); - - this.isReady = true; - - let closeButton = this.doc.getElementById("toolbox-close"); - closeButton.addEventListener("command", this.destroy, true); - - this._buildDockButtons(); - this._buildTabs(); - this._buildButtons(); - this._addKeysToWindow(); - - this.selectTool(this._defaultToolId).then(function(panel) { - this.emit("ready"); - deferred.resolve(); - }.bind(this)); - }.bind(this); - - iframe.addEventListener("DOMContentLoaded", domReady, true); - iframe.setAttribute("src", this._URL); - }.bind(this)); - - return deferred.promise; - }, - - /** - * Adds the keys and commands to the Toolbox Window in window mode. - */ - _addKeysToWindow: function TBOX__addKeysToWindow() { - if (this.hostType != Toolbox.HostType.WINDOW) { - return; - } - let doc = this.doc.defaultView.parent.document; - for (let [id, toolDefinition] of gDevTools._tools) { - if (toolDefinition.key) { - // Prevent multiple entries for the same tool. - if (doc.getElementById("key_" + id)) { - continue; - } - let key = doc.createElement("key"); - key.id = "key_" + id; - - if (toolDefinition.key.startsWith("VK_")) { - key.setAttribute("keycode", toolDefinition.key); - } else { - key.setAttribute("key", toolDefinition.key); - } - - key.setAttribute("modifiers", toolDefinition.modifiers); - key.setAttribute("oncommand", "void(0);"); // needed. See bug 371900 - key.addEventListener("command", function(toolId) { - this.selectTool(toolId); - }.bind(this, id), true); - doc.getElementById("toolbox-keyset").appendChild(key); - } - } - }, - - /** - * Build the buttons for changing hosts. Called every time - * the host changes. - */ - _buildDockButtons: function TBOX_createDockButtons() { - let dockBox = this.doc.getElementById("toolbox-dock-buttons"); - - while (dockBox.firstChild) { - dockBox.removeChild(dockBox.firstChild); - } - - if (!this._target.isLocalTab) { - return; - } - - let closeButton = this.doc.getElementById("toolbox-close"); - if (this.hostType === this.HostType.WINDOW) { - closeButton.setAttribute("hidden", "true"); - } else { - closeButton.removeAttribute("hidden"); - } - - let sideEnabled = Services.prefs.getBoolPref(this._prefs.SIDE_ENABLED); - - for each (let position in this.HostType) { - if (position == this.hostType || - (!sideEnabled && position == this.HostType.SIDE)) { - continue; - } - - let button = this.doc.createElement("toolbarbutton"); - button.id = "toolbox-dock-" + position; - button.className = "toolbox-dock-button"; - button.setAttribute("tooltiptext", toolboxStrings("toolboxDockButtons." + - position + ".tooltip")); - button.addEventListener("command", function(position) { - this.switchHost(position); - }.bind(this, position)); - - dockBox.appendChild(button); - } - }, - - /** - * Add tabs to the toolbox UI for registered tools - */ - _buildTabs: function TBOX_buildTabs() { - for (let definition of gDevTools.getToolDefinitionArray()) { - this._buildTabForTool(definition); - } - }, - - /** - * Add buttons to the UI as specified in the devtools.window.toolbarSpec pref - */ - _buildButtons: function TBOX_buildButtons() { - if (!this.target.isLocalTab) { - return; - } - - let toolbarSpec = CommandUtils.getCommandbarSpec("devtools.toolbox.toolbarSpec"); - let environment = { chromeDocument: this.target.tab.ownerDocument }; - let requisition = new Requisition(environment); - - let buttons = CommandUtils.createButtons(toolbarSpec, this._target, this.doc, requisition); - - let container = this.doc.getElementById("toolbox-buttons"); - buttons.forEach(function(button) { - container.appendChild(button); - }.bind(this)); - }, - - /** - * Build a tab for one tool definition and add to the toolbox - * - * @param {string} toolDefinition - * Tool definition of the tool to build a tab for. - */ - _buildTabForTool: function TBOX_buildTabForTool(toolDefinition) { - if (!toolDefinition.isTargetSupported(this._target)) { - return; - } - - let tabs = this.doc.getElementById("toolbox-tabs"); - let deck = this.doc.getElementById("toolbox-deck"); - - let id = toolDefinition.id; - - let radio = this.doc.createElement("radio"); - radio.className = "toolbox-tab devtools-tab"; - radio.id = "toolbox-tab-" + id; - radio.setAttribute("flex", "1"); - radio.setAttribute("toolid", id); - radio.setAttribute("tooltiptext", toolDefinition.tooltip); - - radio.addEventListener("command", function(id) { - this.selectTool(id); - }.bind(this, id)); - - if (toolDefinition.icon) { - let image = this.doc.createElement("image"); - image.setAttribute("src", toolDefinition.icon); - radio.appendChild(image); - } - - let label = this.doc.createElement("label"); - label.setAttribute("value", toolDefinition.label) - label.setAttribute("crop", "end"); - label.setAttribute("flex", "1"); - - let vbox = this.doc.createElement("vbox"); - vbox.className = "toolbox-panel"; - vbox.id = "toolbox-panel-" + id; - - radio.appendChild(label); - tabs.appendChild(radio); - deck.appendChild(vbox); - - this._addKeysToWindow(); - }, - - /** - * Switch to the tool with the given id - * - * @param {string} id - * The id of the tool to switch to - */ - selectTool: function TBOX_selectTool(id) { - let deferred = Promise.defer(); - - let selected = this.doc.querySelector(".devtools-tab[selected]"); - if (selected) { - selected.removeAttribute("selected"); - } - let tab = this.doc.getElementById("toolbox-tab-" + id); - tab.setAttribute("selected", "true"); - - if (this._currentToolId == id) { - // Return the existing panel in order to have a consistent return value. - return Promise.resolve(this._toolPanels.get(id)); - } - - if (!this.isReady) { - throw new Error("Can't select tool, wait for toolbox 'ready' event"); - } - let tab = this.doc.getElementById("toolbox-tab-" + id); - - if (!tab) { - throw new Error("No tool found"); - } - - let tabstrip = this.doc.getElementById("toolbox-tabs"); - - // select the right tab - let index = -1; - let tabs = tabstrip.childNodes; - for (let i = 0; i < tabs.length; i++) { - if (tabs[i] === tab) { - index = i; - break; - } - } - tabstrip.selectedIndex = index; - - // and select the right iframe - let deck = this.doc.getElementById("toolbox-deck"); - deck.selectedIndex = index; - - let definition = gDevTools.getToolDefinitionMap().get(id); - - this._currentToolId = id; - - let iframe = this.doc.getElementById("toolbox-panel-iframe-" + id); - if (!iframe) { - iframe = this.doc.createElement("iframe"); - iframe.className = "toolbox-panel-iframe"; - iframe.id = "toolbox-panel-iframe-" + id; - iframe.setAttribute("flex", 1); - iframe.setAttribute("forceOwnRefreshDriver", ""); - iframe.tooltip = "aHTMLTooltip"; - - let vbox = this.doc.getElementById("toolbox-panel-" + id); - vbox.appendChild(iframe); - - let boundLoad = function() { - iframe.removeEventListener("DOMContentLoaded", boundLoad, true); - - let built = definition.build(iframe.contentWindow, this); - Promise.resolve(built).then(function(panel) { - this._toolPanels.set(id, panel); - - this.emit(id + "-ready", panel); - this.emit("select", id); - this.emit(id + "-selected", panel); - gDevTools.emit(id + "-ready", this, panel); - - deferred.resolve(panel); - }.bind(this)); - }.bind(this); - - iframe.addEventListener("DOMContentLoaded", boundLoad, true); - iframe.setAttribute("src", definition.url); - } else { - let panel = this._toolPanels.get(id); - // only emit 'select' event if the iframe has been loaded - if (panel) { - this.emit("select", id); - this.emit(id + "-selected", panel); - deferred.resolve(panel); - } - } - - Services.prefs.setCharPref(this._prefs.LAST_TOOL, id); - - return deferred.promise; - }, - - /** - * Raise the toolbox host. - */ - raise: function TBOX_raise() { - this._host.raise(); - }, - - /** - * Refresh the host's title. - */ - _refreshHostTitle: function TBOX_refreshHostTitle() { - let toolName; - let toolId = this.currentToolId; - if (toolId) { - let toolDef = gDevTools.getToolDefinitionMap().get(toolId); - toolName = toolDef.label; - } else { - // no tool is selected - toolName = toolboxStrings("toolbox.defaultTitle"); - } - let title = toolboxStrings("toolbox.titleTemplate", - toolName, this.target.url); - this._host.setTitle(title); - }, - - /** - * Create a host object based on the given host type. - * - * Warning: some hosts require that the toolbox target provides a reference to - * the attached tab. Not all Targets have a tab property - make sure you correctly - * mix and match hosts and targets. - * - * @param {string} hostType - * The host type of the new host object - * - * @return {Host} host - * The created host object - */ - _createHost: function TBOX_createHost(hostType) { - if (!Hosts[hostType]) { - throw new Error('Unknown hostType: '+ hostType); - } - let newHost = new Hosts[hostType](this.target.tab); - - // clean up the toolbox if its window is closed - newHost.on("window-closed", this.destroy); - - return newHost; - }, - - /** - * Switch to a new host for the toolbox UI. E.g. - * bottom, sidebar, separate window. - * - * @param {string} hostType - * The host type of the new host object - */ - switchHost: function TBOX_switchHost(hostType) { - if (hostType == this._host.type) { - return; - } - - if (!this._target.isLocalTab) { - return; - } - - let newHost = this._createHost(hostType); - return newHost.create().then(function(iframe) { - // change toolbox document's parent to the new host - iframe.QueryInterface(Ci.nsIFrameLoaderOwner); - iframe.swapFrameLoaders(this.frame); - - this._host.off("window-closed", this.destroy); - this._host.destroy(); - - this._host = newHost; - - Services.prefs.setCharPref(this._prefs.LAST_HOST, this._host.type); - - this._buildDockButtons(); - this._addKeysToWindow(); - - this.emit("host-changed"); - }.bind(this)); - }, - - /** - * Handler for the tool-registered event. - * @param {string} event - * Name of the event ("tool-registered") - * @param {string} toolId - * Id of the tool that was registered - */ - _toolRegistered: function TBOX_toolRegistered(event, toolId) { - let defs = gDevTools.getToolDefinitionMap(); - let tool = defs.get(toolId); - - this._buildTabForTool(tool); - }, - - /** - * Handler for the tool-unregistered event. - * @param {string} event - * Name of the event ("tool-unregistered") - * @param {string} toolId - * Id of the tool that was unregistered - */ - _toolUnregistered: function TBOX_toolUnregistered(event, toolId) { - let radio = this.doc.getElementById("toolbox-tab-" + toolId); - let panel = this.doc.getElementById("toolbox-panel-" + toolId); - - if (radio) { - if (this._currentToolId == toolId) { - let nextToolName = null; - if (radio.nextSibling) { - nextToolName = radio.nextSibling.getAttribute("toolid"); - } - if (radio.previousSibling) { - nextToolName = radio.previousSibling.getAttribute("toolid"); - } - if (nextToolName) { - this.selectTool(nextToolName); - } - } - radio.parentNode.removeChild(radio); - } - - if (panel) { - panel.parentNode.removeChild(panel); - } - - if (this.hostType == Toolbox.HostType.WINDOW) { - let doc = this.doc.defaultView.parent.document; - let key = doc.getElementById("key_" + id); - if (key) { - key.parentNode.removeChild(key); - } - } - - if (this._toolPanels.has(toolId)) { - let instance = this._toolPanels.get(toolId); - instance.destroy(); - this._toolPanels.delete(toolId); - } - }, - - - /** - * Get the toolbox's notification box - * - * @return The notification box element. - */ - getNotificationBox: function TBOX_getNotificationBox() { - return this.doc.getElementById("toolbox-notificationbox"); - }, - - /** - * Remove all UI elements, detach from target and clear up - */ - destroy: function TBOX_destroy() { - // If several things call destroy then we give them all the same - // destruction promise so we're sure to destroy only once - if (this._destroyer) { - return this._destroyer; - } - // Assign the "_destroyer" property before calling the other - // destroyer methods to guarantee that the Toolbox's destroy - // method is only executed once. - let deferred = Promise.defer(); - this._destroyer = deferred.promise; - - this._target.off("navigate", this._refreshHostTitle); - this.off("select", this._refreshHostTitle); - this.off("host-changed", this._refreshHostTitle); - - gDevTools.off("tool-registered", this._toolRegistered); - gDevTools.off("tool-unregistered", this._toolUnregistered); - - let outstanding = []; - - for (let [id, panel] of this._toolPanels) { - outstanding.push(panel.destroy()); - } - - let container = this.doc.getElementById("toolbox-buttons"); - while(container.firstChild) { - container.removeChild(container.firstChild); - } - - outstanding.push(this._host.destroy()); - - // Targets need to be notified that the toolbox is being torn down, so that - // remote protocol connections can be gracefully terminated. - if (this._target) { - this._target.off("close", this.destroy); - outstanding.push(this._target.destroy()); - } - this._target = null; - - Promise.all(outstanding).then(function() { - this.emit("destroyed"); - // Free _host after the call to destroyed in order to let a chance - // to destroyed listeners to still query toolbox attributes - this._host = null; - deferred.resolve(); - }.bind(this)); - - return this._destroyer; - } -};
new file mode 100644 --- /dev/null +++ b/browser/devtools/inspector/Breadcrumbs.jsm @@ -0,0 +1,599 @@ +/* -*- 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 Cc = Components.classes; +const Cu = Components.utils; +const Ci = Components.interfaces; + +const PSEUDO_CLASSES = [":hover", ":active", ":focus"]; +const ENSURE_SELECTION_VISIBLE_DELAY = 50; // ms + +this.EXPORTED_SYMBOLS = ["HTMLBreadcrumbs"]; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource:///modules/devtools/DOMHelpers.jsm"); +Cu.import("resource:///modules/devtools/LayoutHelpers.jsm"); + +const LOW_PRIORITY_ELEMENTS = { + "HEAD": true, + "BASE": true, + "BASEFONT": true, + "ISINDEX": true, + "LINK": true, + "META": true, + "SCRIPT": true, + "STYLE": true, + "TITLE": true, +}; + +/////////////////////////////////////////////////////////////////////////// +//// HTML Breadcrumbs + +/** + * Display the ancestors of the current node and its children. + * Only one "branch" of children are displayed (only one line). + * + * FIXME: Bug 822388 - Use the BreadcrumbsWidget in the Inspector. + * + * Mechanism: + * . If no nodes displayed yet: + * then display the ancestor of the selected node and the selected node; + * else select the node; + * . If the selected node is the last node displayed, append its first (if any). + */ +this.HTMLBreadcrumbs = function HTMLBreadcrumbs(aInspector) +{ + this.inspector = aInspector; + this.selection = this.inspector.selection; + this.chromeWin = this.inspector.panelWin; + this.chromeDoc = this.inspector.panelDoc; + this.DOMHelpers = new DOMHelpers(this.chromeWin); + this._init(); +} + +HTMLBreadcrumbs.prototype = { + _init: function BC__init() + { + this.container = this.chromeDoc.getElementById("inspector-breadcrumbs"); + this.container.addEventListener("mousedown", this, true); + this.container.addEventListener("keypress", this, true); + + // We will save a list of already displayed nodes in this array. + this.nodeHierarchy = []; + + // Last selected node in nodeHierarchy. + this.currentIndex = -1; + + // By default, hide the arrows. We let the <scrollbox> show them + // in case of overflow. + this.container.removeAttribute("overflows"); + this.container._scrollButtonUp.collapsed = true; + this.container._scrollButtonDown.collapsed = true; + + this.onscrollboxreflow = function() { + if (this.container._scrollButtonDown.collapsed) + this.container.removeAttribute("overflows"); + else + this.container.setAttribute("overflows", true); + }.bind(this); + + this.container.addEventListener("underflow", this.onscrollboxreflow, false); + this.container.addEventListener("overflow", this.onscrollboxreflow, false); + + this.update = this.update.bind(this); + this.updateSelectors = this.updateSelectors.bind(this); + this.selection.on("new-node", this.update); + this.selection.on("pseudoclass", this.updateSelectors); + this.selection.on("attribute-changed", this.updateSelectors); + this.update(); + }, + + /** + * Build a string that represents the node: tagName#id.class1.class2. + * + * @param aNode The node to pretty-print + * @returns a string + */ + prettyPrintNodeAsText: function BC_prettyPrintNodeText(aNode) + { + let text = aNode.tagName.toLowerCase(); + if (aNode.id) { + text += "#" + aNode.id; + } + for (let i = 0; i < aNode.classList.length; i++) { + text += "." + aNode.classList[i]; + } + for (let i = 0; i < PSEUDO_CLASSES.length; i++) { + let pseudo = PSEUDO_CLASSES[i]; + if (DOMUtils.hasPseudoClassLock(aNode, pseudo)) { + text += pseudo; + } + } + + return text; + }, + + + /** + * Build <label>s that represent the node: + * <label class="breadcrumbs-widget-item-tag">tagName</label> + * <label class="breadcrumbs-widget-item-id">#id</label> + * <label class="breadcrumbs-widget-item-classes">.class1.class2</label> + * + * @param aNode The node to pretty-print + * @returns a document fragment. + */ + prettyPrintNodeAsXUL: function BC_prettyPrintNodeXUL(aNode) + { + let fragment = this.chromeDoc.createDocumentFragment(); + + let tagLabel = this.chromeDoc.createElement("label"); + tagLabel.className = "breadcrumbs-widget-item-tag plain"; + + let idLabel = this.chromeDoc.createElement("label"); + idLabel.className = "breadcrumbs-widget-item-id plain"; + + let classesLabel = this.chromeDoc.createElement("label"); + classesLabel.className = "breadcrumbs-widget-item-classes plain"; + + let pseudosLabel = this.chromeDoc.createElement("label"); + pseudosLabel.className = "breadcrumbs-widget-item-pseudo-classes plain"; + + tagLabel.textContent = aNode.tagName.toLowerCase(); + idLabel.textContent = aNode.id ? ("#" + aNode.id) : ""; + + let classesText = ""; + for (let i = 0; i < aNode.classList.length; i++) { + classesText += "." + aNode.classList[i]; + } + classesLabel.textContent = classesText; + + let pseudos = PSEUDO_CLASSES.filter(function(pseudo) { + return DOMUtils.hasPseudoClassLock(aNode, pseudo); + }, this); + pseudosLabel.textContent = pseudos.join(""); + + fragment.appendChild(tagLabel); + fragment.appendChild(idLabel); + fragment.appendChild(classesLabel); + fragment.appendChild(pseudosLabel); + + return fragment; + }, + + /** + * Open the sibling menu. + * + * @param aButton the button representing the node. + * @param aNode the node we want the siblings from. + */ + openSiblingMenu: function BC_openSiblingMenu(aButton, aNode) + { + // We make sure that the targeted node is selected + // because we want to use the nodemenu that only works + // for inspector.selection + this.selection.setNode(aNode, "breadcrumbs"); + + let title = this.chromeDoc.createElement("menuitem"); + title.setAttribute("label", this.inspector.strings.GetStringFromName("breadcrumbs.siblings")); + title.setAttribute("disabled", "true"); + + let separator = this.chromeDoc.createElement("menuseparator"); + + let items = [title, separator]; + + let nodes = aNode.parentNode.childNodes; + for (let i = 0; i < nodes.length; i++) { + if (nodes[i].nodeType == aNode.ELEMENT_NODE) { + let item = this.chromeDoc.createElement("menuitem"); + if (nodes[i] === aNode) { + item.setAttribute("disabled", "true"); + item.setAttribute("checked", "true"); + } + + item.setAttribute("type", "radio"); + item.setAttribute("label", this.prettyPrintNodeAsText(nodes[i])); + + let selection = this.selection; + item.onmouseup = (function(aNode) { + return function() { + selection.setNode(aNode, "breadcrumbs"); + } + })(nodes[i]); + + items.push(item); + } + } + this.inspector.showNodeMenu(aButton, "before_start", items); + }, + + /** + * Generic event handler. + * + * @param nsIDOMEvent event + * The DOM event object. + */ + handleEvent: function BC_handleEvent(event) + { + if (event.type == "mousedown" && event.button == 0) { + // on Click and Hold, open the Siblings menu + + let timer; + let container = this.container; + + function openMenu(event) { + cancelHold(); + let target = event.originalTarget; + if (target.tagName == "button") { + target.onBreadcrumbsHold(); + } + } + + function handleClick(event) { + cancelHold(); + let target = event.originalTarget; + if (target.tagName == "button") { + target.onBreadcrumbsClick(); + } + } + + let window = this.chromeWin; + function cancelHold(event) { + window.clearTimeout(timer); + container.removeEventListener("mouseout", cancelHold, false); + container.removeEventListener("mouseup", handleClick, false); + } + + container.addEventListener("mouseout", cancelHold, false); + container.addEventListener("mouseup", handleClick, false); + timer = window.setTimeout(openMenu, 500, event); + } + + if (event.type == "keypress" && this.selection.isElementNode()) { + let node = null; + switch (event.keyCode) { + case this.chromeWin.KeyEvent.DOM_VK_LEFT: + if (this.currentIndex != 0) { + node = this.nodeHierarchy[this.currentIndex - 1].node; + } + break; + case this.chromeWin.KeyEvent.DOM_VK_RIGHT: + if (this.currentIndex < this.nodeHierarchy.length - 1) { + node = this.nodeHierarchy[this.currentIndex + 1].node; + } + break; + case this.chromeWin.KeyEvent.DOM_VK_UP: + node = this.selection.node.previousSibling; + while (node && (node.nodeType != node.ELEMENT_NODE)) { + node = node.previousSibling; + } + break; + case this.chromeWin.KeyEvent.DOM_VK_DOWN: + node = this.selection.node.nextSibling; + while (node && (node.nodeType != node.ELEMENT_NODE)) { + node = node.nextSibling; + } + break; + } + if (node) { + this.selection.setNode(node, "breadcrumbs"); + } + event.preventDefault(); + event.stopPropagation(); + } + }, + + /** + * Remove nodes and delete properties. + */ + destroy: function BC_destroy() + { + this.nodeHierarchy.forEach(function(crumb) { + if (LayoutHelpers.isNodeConnected(crumb.node)) { + DOMUtils.clearPseudoClassLocks(crumb.node); + } + }); + + this.selection.off("new-node", this.update); + this.selection.off("pseudoclass", this.updateSelectors); + this.selection.off("attribute-changed", this.updateSelectors); + + this.container.removeEventListener("underflow", this.onscrollboxreflow, false); + this.container.removeEventListener("overflow", this.onscrollboxreflow, false); + this.onscrollboxreflow = null; + + this.empty(); + this.container.removeEventListener("mousedown", this, true); + this.container.removeEventListener("keypress", this, true); + this.container = null; + this.nodeHierarchy = null; + }, + + /** + * Empty the breadcrumbs container. + */ + empty: function BC_empty() + { + while (this.container.hasChildNodes()) { + this.container.removeChild(this.container.firstChild); + } + }, + + /** + * Re-init the cache and remove all the buttons. + */ + invalidateHierarchy: function BC_invalidateHierarchy() + { + this.inspector.hideNodeMenu(); + this.nodeHierarchy = []; + this.empty(); + }, + + /** + * Set which button represent the selected node. + * + * @param aIdx Index of the displayed-button to select + */ + setCursor: function BC_setCursor(aIdx) + { + // Unselect the previously selected button + if (this.currentIndex > -1 && this.currentIndex < this.nodeHierarchy.length) { + this.nodeHierarchy[this.currentIndex].button.removeAttribute("checked"); + } + if (aIdx > -1) { + this.nodeHierarchy[aIdx].button.setAttribute("checked", "true"); + if (this.hadFocus) + this.nodeHierarchy[aIdx].button.focus(); + } + this.currentIndex = aIdx; + }, + + /** + * Get the index of the node in the cache. + * + * @param aNode + * @returns integer the index, -1 if not found + */ + indexOf: function BC_indexOf(aNode) + { + let i = this.nodeHierarchy.length - 1; + for (let i = this.nodeHierarchy.length - 1; i >= 0; i--) { + if (this.nodeHierarchy[i].node === aNode) { + return i; + } + } + return -1; + }, + + /** + * Remove all the buttons and their references in the cache + * after a given index. + * + * @param aIdx + */ + cutAfter: function BC_cutAfter(aIdx) + { + while (this.nodeHierarchy.length > (aIdx + 1)) { + let toRemove = this.nodeHierarchy.pop(); + this.container.removeChild(toRemove.button); + } + }, + + /** + * Build a button representing the node. + * + * @param aNode The node from the page. + * @returns aNode The <button>. + */ + buildButton: function BC_buildButton(aNode) + { + let button = this.chromeDoc.createElement("button"); + button.appendChild(this.prettyPrintNodeAsXUL(aNode)); + button.className = "breadcrumbs-widget-item"; + + button.setAttribute("tooltiptext", this.prettyPrintNodeAsText(aNode)); + + button.onkeypress = function onBreadcrumbsKeypress(e) { + if (e.charCode == Ci.nsIDOMKeyEvent.DOM_VK_SPACE || + e.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RETURN) + button.click(); + } + + button.onBreadcrumbsClick = function onBreadcrumbsClick() { + this.selection.setNode(aNode, "breadcrumbs"); + }.bind(this); + + button.onclick = (function _onBreadcrumbsRightClick(event) { + button.focus(); + if (event.button == 2) { + this.openSiblingMenu(button, aNode); + } + }).bind(this); + + button.onBreadcrumbsHold = (function _onBreadcrumbsHold() { + this.openSiblingMenu(button, aNode); + }).bind(this); + return button; + }, + + /** + * Connecting the end of the breadcrumbs to a node. + * + * @param aNode The node to reach. + */ + expand: function BC_expand(aNode) + { + let fragment = this.chromeDoc.createDocumentFragment(); + let toAppend = aNode; + let lastButtonInserted = null; + let originalLength = this.nodeHierarchy.length; + let stopNode = null; + if (originalLength > 0) { + stopNode = this.nodeHierarchy[originalLength - 1].node; + } + while (toAppend && toAppend.tagName && toAppend != stopNode) { + let button = this.buildButton(toAppend); + fragment.insertBefore(button, lastButtonInserted); + lastButtonInserted = button; + this.nodeHierarchy.splice(originalLength, 0, {node: toAppend, button: button}); + toAppend = this.DOMHelpers.getParentObject(toAppend); + } + this.container.appendChild(fragment, this.container.firstChild); + }, + + /** + * Get a child of a node that can be displayed in the breadcrumbs + * and that is probably visible. See LOW_PRIORITY_ELEMENTS. + * + * @param aNode The parent node. + * @returns nsIDOMNode|null + */ + getInterestingFirstNode: function BC_getInterestingFirstNode(aNode) + { + let nextChild = this.DOMHelpers.getChildObject(aNode, 0); + let fallback = null; + + while (nextChild) { + if (nextChild.nodeType == aNode.ELEMENT_NODE) { + if (!(nextChild.tagName in LOW_PRIORITY_ELEMENTS)) { + return nextChild; + } + if (!fallback) { + fallback = nextChild; + } + } + nextChild = this.DOMHelpers.getNextSibling(nextChild); + } + return fallback; + }, + + + /** + * Find the "youngest" ancestor of a node which is already in the breadcrumbs. + * + * @param aNode + * @returns Index of the ancestor in the cache + */ + getCommonAncestor: function BC_getCommonAncestor(aNode) + { + let node = aNode; + while (node) { + let idx = this.indexOf(node); + if (idx > -1) { + return idx; + } else { + node = this.DOMHelpers.getParentObject(node); + } + } + return -1; + }, + + /** + * Make sure that the latest node in the breadcrumbs is not the selected node + * if the selected node still has children. + */ + ensureFirstChild: function BC_ensureFirstChild() + { + // If the last displayed node is the selected node + if (this.currentIndex == this.nodeHierarchy.length - 1) { + let node = this.nodeHierarchy[this.currentIndex].node; + let child = this.getInterestingFirstNode(node); + // If the node has a child + if (child) { + // Show this child + this.expand(child); + } + } + }, + + /** + * Ensure the selected node is visible. + */ + scroll: function BC_scroll() + { + // FIXME bug 684352: make sure its immediate neighbors are visible too. + + let scrollbox = this.container; + let element = this.nodeHierarchy[this.currentIndex].button; + + // Repeated calls to ensureElementIsVisible would interfere with each other + // and may sometimes result in incorrect scroll positions. + this.chromeWin.clearTimeout(this._ensureVisibleTimeout); + this._ensureVisibleTimeout = this.chromeWin.setTimeout(function() { + scrollbox.ensureElementIsVisible(element); + }, ENSURE_SELECTION_VISIBLE_DELAY); + }, + + updateSelectors: function BC_updateSelectors() + { + for (let i = this.nodeHierarchy.length - 1; i >= 0; i--) { + let crumb = this.nodeHierarchy[i]; + let button = crumb.button; + + while(button.hasChildNodes()) { + button.removeChild(button.firstChild); + } + button.appendChild(this.prettyPrintNodeAsXUL(crumb.node)); + button.setAttribute("tooltiptext", this.prettyPrintNodeAsText(crumb.node)); + } + }, + + /** + * Update the breadcrumbs display when a new node is selected. + */ + update: function BC_update() + { + this.inspector.hideNodeMenu(); + + let cmdDispatcher = this.chromeDoc.commandDispatcher; + this.hadFocus = (cmdDispatcher.focusedElement && + cmdDispatcher.focusedElement.parentNode == this.container); + + if (!this.selection.isConnected()) { + this.cutAfter(-1); // remove all the crumbs + return; + } + + if (!this.selection.isElementNode()) { + this.setCursor(-1); // no selection + return; + } + + let idx = this.indexOf(this.selection.node); + + // Is the node already displayed in the breadcrumbs? + if (idx > -1) { + // Yes. We select it. + this.setCursor(idx); + } else { + // No. Is the breadcrumbs display empty? + if (this.nodeHierarchy.length > 0) { + // No. We drop all the element that are not direct ancestors + // of the selection + let parent = this.DOMHelpers.getParentObject(this.selection.node); + let idx = this.getCommonAncestor(parent); + this.cutAfter(idx); + } + // we append the missing button between the end of the breadcrumbs display + // and the current node. + this.expand(this.selection.node); + + // we select the current node button + idx = this.indexOf(this.selection.node); + this.setCursor(idx); + } + // Add the first child of the very last node of the breadcrumbs if possible. + this.ensureFirstChild(); + this.updateSelectors(); + + // Make sure the selected node and its neighbours are visible. + this.scroll(); + }, +} + +XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () { + return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); +});
--- a/browser/devtools/inspector/CmdInspect.jsm +++ b/browser/devtools/inspector/CmdInspect.jsm @@ -5,18 +5,18 @@ const { classes: Cc, interfaces: Ci, utils: Cu } = Components; this.EXPORTED_SYMBOLS = [ ]; Cu.import("resource:///modules/devtools/gcli.jsm"); Cu.import('resource://gre/modules/XPCOMUtils.jsm'); XPCOMUtils.defineLazyModuleGetter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "devtools", - "resource:///modules/devtools/gDevTools.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "TargetFactory", + "resource:///modules/devtools/Target.jsm"); /** * 'inspect' command */ gcli.addCommand({ name: "inspect", description: gcli.lookup("inspectDesc"), manual: gcli.lookup("inspectManual"), @@ -25,15 +25,15 @@ gcli.addCommand({ name: "selector", type: "node", description: gcli.lookup("inspectNodeDesc"), manual: gcli.lookup("inspectNodeManual") } ], exec: function Command_inspect(args, context) { let gBrowser = context.environment.chromeDocument.defaultView.gBrowser; - let target = devtools.TargetFactory.forTab(gBrowser.selectedTab); + let target = TargetFactory.forTab(gBrowser.selectedTab); return gDevTools.showToolbox(target, "inspector").then(function(toolbox) { toolbox.getCurrentPanel().selection.setNode(args.selector, "gcli"); }.bind(this)); } });
new file mode 100644 --- /dev/null +++ b/browser/devtools/inspector/Highlighter.jsm @@ -0,0 +1,799 @@ +/* -*- 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 Cu = Components.utils; +const Cc = Components.classes; +const Ci = Components.interfaces; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource:///modules/devtools/LayoutHelpers.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource:///modules/devtools/EventEmitter.jsm"); + +this.EXPORTED_SYMBOLS = ["Highlighter"]; + +const PSEUDO_CLASSES = [":hover", ":active", ":focus"]; + // add ":visited" and ":link" after bug 713106 is fixed + +/** + * A highlighter mechanism. + * + * The highlighter is built dynamically into the browser element. + * The caller is in charge of destroying the highlighter (ie, the highlighter + * won't be destroyed if a new tab is selected for example). + * + * API: + * + * // Constructor and destructor. + * Highlighter(aTab, aInspector) + * void destroy(); + * + * // Show and hide the highlighter + * void show(); + * void hide(); + * boolean isHidden(); + * + * // Redraw the highlighter if the visible portion of the node has changed. + * void invalidateSize(aScroll); + * + * Events: + * + * "closed" - Highlighter is closing + * "highlighting" - Highlighter is highlighting + * "locked" - The selected node has been locked + * "unlocked" - The selected ndoe has been unlocked + * + * Structure: + * <stack class="highlighter-container"> + * <box class="highlighter-outline-container"> + * <box class="highlighter-outline" locked="true/false"/> + * </box> + * <box class="highlighter-controls"> + * <box class="highlighter-nodeinfobar-container" position="top/bottom" locked="true/false"> + * <box class="highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-top"/> + * <hbox class="highlighter-nodeinfobar"> + * <toolbarbutton class="highlighter-nodeinfobar-inspectbutton highlighter-nodeinfobar-button"/> + * <hbox class="highlighter-nodeinfobar-text">tagname#id.class1.class2</hbox> + * <toolbarbutton class="highlighter-nodeinfobar-menu highlighter-nodeinfobar-button">…</toolbarbutton> + * </hbox> + * <box class="highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-bottom"/> + * </box> + * </box> + * </stack> + * + */ + + +/** + * Constructor. + * + * @param aTarget The inspection target. + * @param aInspector Inspector panel. + * @param aToolbox The toolbox holding the inspector. + */ +this.Highlighter = function Highlighter(aTarget, aInspector, aToolbox) +{ + this.target = aTarget; + this.tab = aTarget.tab; + this.toolbox = aToolbox; + this.browser = this.tab.linkedBrowser; + this.chromeDoc = this.tab.ownerDocument; + this.chromeWin = this.chromeDoc.defaultView; + this.inspector = aInspector + + EventEmitter.decorate(this); + + this._init(); +} + +Highlighter.prototype = { + get selection() { + return this.inspector.selection; + }, + + _init: function Highlighter__init() + { + this.unlockAndFocus = this.unlockAndFocus.bind(this); + this.updateInfobar = this.updateInfobar.bind(this); + this.highlight = this.highlight.bind(this); + + let stack = this.browser.parentNode; + this.win = this.browser.contentWindow; + this._highlighting = false; + + this.highlighterContainer = this.chromeDoc.createElement("stack"); + this.highlighterContainer.className = "highlighter-container"; + + this.outline = this.chromeDoc.createElement("box"); + this.outline.className = "highlighter-outline"; + + let outlineContainer = this.chromeDoc.createElement("box"); + outlineContainer.appendChild(this.outline); + outlineContainer.className = "highlighter-outline-container"; + + // The controlsBox will host the different interactive + // elements of the highlighter (buttons, toolbars, ...). + let controlsBox = this.chromeDoc.createElement("box"); + controlsBox.className = "highlighter-controls"; + this.highlighterContainer.appendChild(outlineContainer); + this.highlighterContainer.appendChild(controlsBox); + + // Insert the highlighter right after the browser + stack.insertBefore(this.highlighterContainer, stack.childNodes[1]); + + this.buildInfobar(controlsBox); + + this.transitionDisabler = null; + this.pageEventsMuter = null; + + this.unlockAndFocus(); + + this.selection.on("new-node", this.highlight); + this.selection.on("new-node", this.updateInfobar); + this.selection.on("pseudoclass", this.updateInfobar); + this.selection.on("attribute-changed", this.updateInfobar); + + this.onToolSelected = function(event, id) { + if (id != "inspector") { + this.chromeWin.clearTimeout(this.pageEventsMuter); + this.detachMouseListeners(); + this.disabled = true; + this.hide(); + } else { + if (!this.locked) { + this.attachMouseListeners(); + } + this.disabled = false; + this.show(); + } + }.bind(this); + this.toolbox.on("select", this.onToolSelected); + + this.hidden = true; + this.highlight(); + }, + + /** + * Destroy the nodes. Remove listeners. + */ + destroy: function Highlighter_destroy() + { + this.inspectButton.removeEventListener("command", this.unlockAndFocus); + this.inspectButton = null; + + this.toolbox.off("select", this.onToolSelected); + this.toolbox = null; + + this.selection.off("new-node", this.highlight); + this.selection.off("new-node", this.updateInfobar); + this.selection.off("pseudoclass", this.updateInfobar); + this.selection.off("attribute-changed", this.updateInfobar); + + this.detachMouseListeners(); + this.detachPageListeners(); + + this.chromeWin.clearTimeout(this.transitionDisabler); + this.chromeWin.clearTimeout(this.pageEventsMuter); + this.boundCloseEventHandler = null; + this._contentRect = null; + this._highlightRect = null; + this._highlighting = false; + this.outline = null; + this.nodeInfo = null; + this.highlighterContainer.parentNode.removeChild(this.highlighterContainer); + this.highlighterContainer = null; + this.win = null + this.browser = null; + this.chromeDoc = null; + this.chromeWin = null; + this.tabbrowser = null; + + this.emit("closed"); + }, + + /** + * Show the outline, and select a node. + */ + highlight: function Highlighter_highlight() + { + if (this.selection.reason != "highlighter") { + this.lock(); + } + + let canHighlightNode = this.selection.isNode() && + this.selection.isConnected() && + this.selection.isElementNode(); + + if (canHighlightNode) { + if (this.selection.reason != "navigateaway") { + this.disabled = false; + } + this.show(); + this.updateInfobar(); + this.invalidateSize(); + if (!this._highlighting && + this.selection.reason != "highlighter") { + LayoutHelpers.scrollIntoViewIfNeeded(this.selection.node); + } + } else { + this.disabled = true; + this.hide(); + } + }, + + /** + * Update the highlighter size and position. + */ + invalidateSize: function Highlighter_invalidateSize() + { + let canHiglightNode = this.selection.isNode() && + this.selection.isConnected() && + this.selection.isElementNode(); + + if (!canHiglightNode) + return; + + let clientRect = this.selection.node.getBoundingClientRect(); + let rect = LayoutHelpers.getDirtyRect(this.selection.node); + this.highlightRectangle(rect); + + this.moveInfobar(); + + if (this._highlighting) { + this.showOutline(); + this.emit("highlighting"); + } + }, + + /** + * Show the highlighter if it has been hidden. + */ + show: function() { + if (!this.hidden || this.disabled) return; + this.showOutline(); + this.showInfobar(); + this.computeZoomFactor(); + this.attachPageListeners(); + this.invalidateSize(); + this.hidden = false; + }, + + /** + * Hide the highlighter, the outline and the infobar. + */ + hide: function() { + if (this.hidden) return; + this.hideOutline(); + this.hideInfobar(); + this.detachPageListeners(); + this.hidden = true; + }, + + /** + * Is the highlighter visible? + * + * @return boolean + */ + isHidden: function() { + return this.hidden; + }, + + /** + * Lock a node. Stops the inspection. + */ + lock: function() { + if (this.locked === true) return; + this.outline.setAttribute("locked", "true"); + this.nodeInfo.container.setAttribute("locked", "true"); + this.detachMouseListeners(); + this.locked = true; + this.emit("locked"); + }, + + /** + * Start inspecting. + * Unlock the current node (if any), and select any node being hovered. + */ + unlock: function() { + if (this.locked === false) return; + this.outline.removeAttribute("locked"); + this.nodeInfo.container.removeAttribute("locked"); + this.attachMouseListeners(); + this.locked = false; + if (this.selection.isElementNode() && + this.selection.isConnected()) { + this.showOutline(); + } + this.emit("unlocked"); + }, + + /** + * Focus the browser before unlocking. + */ + unlockAndFocus: function Highlighter_unlockAndFocus() { + if (this.locked === false) return; + this.chromeWin.focus(); + this.unlock(); + }, + + /** + * Hide the infobar + */ + hideInfobar: function Highlighter_hideInfobar() { + this.nodeInfo.container.setAttribute("force-transitions", "true"); + this.nodeInfo.container.setAttribute("hidden", "true"); + }, + + /** + * Show the infobar + */ + showInfobar: function Highlighter_showInfobar() { + this.nodeInfo.container.removeAttribute("hidden"); + this.moveInfobar(); + this.nodeInfo.container.removeAttribute("force-transitions"); + }, + + /** + * Hide the outline + */ + hideOutline: function Highlighter_hideOutline() { + this.outline.setAttribute("hidden", "true"); + }, + + /** + * Show the outline + */ + showOutline: function Highlighter_showOutline() { + if (this._highlighting) + this.outline.removeAttribute("hidden"); + }, + + /** + * Build the node Infobar. + * + * <box class="highlighter-nodeinfobar-container"> + * <box class="Highlighter-nodeinfobar-arrow-top"/> + * <hbox class="highlighter-nodeinfobar"> + * <toolbarbutton class="highlighter-nodeinfobar-button highlighter-nodeinfobar-inspectbutton"/> + * <hbox class="highlighter-nodeinfobar-text"> + * <xhtml:span class="highlighter-nodeinfobar-tagname"/> + * <xhtml:span class="highlighter-nodeinfobar-id"/> + * <xhtml:span class="highlighter-nodeinfobar-classes"/> + * <xhtml:span class="highlighter-nodeinfobar-pseudo-classes"/> + * </hbox> + * <toolbarbutton class="highlighter-nodeinfobar-button highlighter-nodeinfobar-menu"/> + * </hbox> + * <box class="Highlighter-nodeinfobar-arrow-bottom"/> + * </box> + * + * @param nsIDOMElement aParent + * The container of the infobar. + */ + buildInfobar: function Highlighter_buildInfobar(aParent) + { + let container = this.chromeDoc.createElement("box"); + container.className = "highlighter-nodeinfobar-container"; + container.setAttribute("position", "top"); + container.setAttribute("disabled", "true"); + + let nodeInfobar = this.chromeDoc.createElement("hbox"); + nodeInfobar.className = "highlighter-nodeinfobar"; + + let arrowBoxTop = this.chromeDoc.createElement("box"); + arrowBoxTop.className = "highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-top"; + + let arrowBoxBottom = this.chromeDoc.createElement("box"); + arrowBoxBottom.className = "highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-bottom"; + + let tagNameLabel = this.chromeDoc.createElementNS("http://www.w3.org/1999/xhtml", "span"); + tagNameLabel.className = "highlighter-nodeinfobar-tagname"; + + let idLabel = this.chromeDoc.createElementNS("http://www.w3.org/1999/xhtml", "span"); + idLabel.className = "highlighter-nodeinfobar-id"; + + let classesBox = this.chromeDoc.createElementNS("http://www.w3.org/1999/xhtml", "span"); + classesBox.className = "highlighter-nodeinfobar-classes"; + + let pseudoClassesBox = this.chromeDoc.createElementNS("http://www.w3.org/1999/xhtml", "span"); + pseudoClassesBox.className = "highlighter-nodeinfobar-pseudo-classes"; + + // Add some content to force a better boundingClientRect down below. + pseudoClassesBox.textContent = " "; + + // Create buttons + + this.inspectButton = this.chromeDoc.createElement("toolbarbutton"); + this.inspectButton.className = "highlighter-nodeinfobar-button highlighter-nodeinfobar-inspectbutton" + let toolbarInspectButton = this.inspector.panelDoc.getElementById("inspector-inspect-toolbutton"); + this.inspectButton.setAttribute("tooltiptext", toolbarInspectButton.getAttribute("tooltiptext")); + this.inspectButton.addEventListener("command", this.unlockAndFocus); + + let nodemenu = this.chromeDoc.createElement("toolbarbutton"); + nodemenu.setAttribute("type", "menu"); + nodemenu.className = "highlighter-nodeinfobar-button highlighter-nodeinfobar-menu" + nodemenu.setAttribute("tooltiptext", + this.strings.GetStringFromName("nodeMenu.tooltiptext")); + + nodemenu.onclick = function() { + this.inspector.showNodeMenu(nodemenu, "after_start"); + }.bind(this); + + // <hbox class="highlighter-nodeinfobar-text"/> + let texthbox = this.chromeDoc.createElement("hbox"); + texthbox.className = "highlighter-nodeinfobar-text"; + texthbox.setAttribute("align", "center"); + texthbox.setAttribute("flex", "1"); + + texthbox.addEventListener("mousedown", function(aEvent) { + // On click, show the node: + if (this.selection.isElementNode()) { + LayoutHelpers.scrollIntoViewIfNeeded(this.selection.node); + } + }.bind(this), true); + + texthbox.appendChild(tagNameLabel); + texthbox.appendChild(idLabel); + texthbox.appendChild(classesBox); + texthbox.appendChild(pseudoClassesBox); + + nodeInfobar.appendChild(this.inspectButton); + nodeInfobar.appendChild(texthbox); + nodeInfobar.appendChild(nodemenu); + + container.appendChild(arrowBoxTop); + container.appendChild(nodeInfobar); + container.appendChild(arrowBoxBottom); + + aParent.appendChild(container); + + let barHeight = container.getBoundingClientRect().height; + + this.nodeInfo = { + tagNameLabel: tagNameLabel, + idLabel: idLabel, + classesBox: classesBox, + pseudoClassesBox: pseudoClassesBox, + container: container, + barHeight: barHeight, + }; + }, + + /** + * Highlight a rectangular region. + * + * @param object aRect + * The rectangle region to highlight. + * @returns boolean + * True if the rectangle was highlighted, false otherwise. + */ + highlightRectangle: function Highlighter_highlightRectangle(aRect) + { + if (!aRect) { + this.unhighlight(); + return; + } + + let oldRect = this._contentRect; + + if (oldRect && aRect.top == oldRect.top && aRect.left == oldRect.left && + aRect.width == oldRect.width && aRect.height == oldRect.height) { + return; // same rectangle + } + + let aRectScaled = LayoutHelpers.getZoomedRect(this.win, aRect); + + if (aRectScaled.left >= 0 && aRectScaled.top >= 0 && + aRectScaled.width > 0 && aRectScaled.height > 0) { + + this.showOutline(); + + // The bottom div and the right div are flexibles (flex=1). + // We don't need to resize them. + let top = "top:" + aRectScaled.top + "px;"; + let left = "left:" + aRectScaled.left + "px;"; + let width = "width:" + aRectScaled.width + "px;"; + let height = "height:" + aRectScaled.height + "px;"; + this.outline.setAttribute("style", top + left + width + height); + + this._highlighting = true; + } else { + this.unhighlight(); + } + + this._contentRect = aRect; // save orig (non-scaled) rect + this._highlightRect = aRectScaled; // and save the scaled rect. + + return; + }, + + /** + * Clear the highlighter surface. + */ + unhighlight: function Highlighter_unhighlight() + { + this._highlighting = false; + this.hideOutline(); + }, + + /** + * Update node information (tagName#id.class) + */ + updateInfobar: function Highlighter_updateInfobar() + { + if (!this.selection.isElementNode()) { + this.nodeInfo.tagNameLabel.textContent = ""; + this.nodeInfo.idLabel.textContent = ""; + this.nodeInfo.classesBox.textContent = ""; + this.nodeInfo.pseudoClassesBox.textContent = ""; + return; + } + + let node = this.selection.node; + + // Tag name + this.nodeInfo.tagNameLabel.textContent = node.tagName; + + // ID + this.nodeInfo.idLabel.textContent = node.id ? "#" + node.id : ""; + + // Classes + let classes = this.nodeInfo.classesBox; + + classes.textContent = node.classList.length ? + "." + Array.join(node.classList, ".") : ""; + + // Pseudo-classes + let pseudos = PSEUDO_CLASSES.filter(function(pseudo) { + return DOMUtils.hasPseudoClassLock(node, pseudo); + }, this); + + let pseudoBox = this.nodeInfo.pseudoClassesBox; + pseudoBox.textContent = pseudos.join(""); + }, + + /** + * Move the Infobar to the right place in the highlighter. + */ + moveInfobar: function Highlighter_moveInfobar() + { + if (this._highlightRect) { + let winHeight = this.win.innerHeight * this.zoom; + let winWidth = this.win.innerWidth * this.zoom; + + let rect = {top: this._highlightRect.top, + left: this._highlightRect.left, + width: this._highlightRect.width, + height: this._highlightRect.height}; + + rect.top = Math.max(rect.top, 0); + rect.left = Math.max(rect.left, 0); + rect.width = Math.max(rect.width, 0); + rect.height = Math.max(rect.height, 0); + + rect.top = Math.min(rect.top, winHeight); + rect.left = Math.min(rect.left, winWidth); + + this.nodeInfo.container.removeAttribute("disabled"); + // Can the bar be above the node? + if (rect.top < this.nodeInfo.barHeight) { + // No. Can we move the toolbar under the node? + if (rect.top + rect.height + + this.nodeInfo.barHeight > winHeight) { + // No. Let's move it inside. + this.nodeInfo.container.style.top = rect.top + "px"; + this.nodeInfo.container.setAttribute("position", "overlap"); + } else { + // Yes. Let's move it under the node. + this.nodeInfo.container.style.top = rect.top + rect.height + "px"; + this.nodeInfo.container.setAttribute("position", "bottom"); + } + } else { + // Yes. Let's move it on top of the node. + this.nodeInfo.container.style.top = + rect.top - this.nodeInfo.barHeight + "px"; + this.nodeInfo.container.setAttribute("position", "top"); + } + + let barWidth = this.nodeInfo.container.getBoundingClientRect().width; + let left = rect.left + rect.width / 2 - barWidth / 2; + + // Make sure the whole infobar is visible + if (left < 0) { + left = 0; + this.nodeInfo.container.setAttribute("hide-arrow", "true"); + } else { + if (left + barWidth > winWidth) { + left = winWidth - barWidth; + this.nodeInfo.container.setAttribute("hide-arrow", "true"); + } else { + this.nodeInfo.container.removeAttribute("hide-arrow"); + } + } + this.nodeInfo.container.style.left = left + "px"; + } else { + this.nodeInfo.container.style.left = "0"; + this.nodeInfo.container.style.top = "0"; + this.nodeInfo.container.setAttribute("position", "top"); + this.nodeInfo.container.setAttribute("hide-arrow", "true"); + } + }, + + /** + * Store page zoom factor. + */ + computeZoomFactor: function Highlighter_computeZoomFactor() { + this.zoom = + this.win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .fullZoom; + }, + + ///////////////////////////////////////////////////////////////////////// + //// Event Handling + + attachMouseListeners: function Highlighter_attachMouseListeners() + { + this.browser.addEventListener("mousemove", this, true); + this.browser.addEventListener("click", this, true); + this.browser.addEventListener("dblclick", this, true); + this.browser.addEventListener("mousedown", this, true); + this.browser.addEventListener("mouseup", this, true); + }, + + detachMouseListeners: function Highlighter_detachMouseListeners() + { + this.browser.removeEventListener("mousemove", this, true); + this.browser.removeEventListener("click", this, true); + this.browser.removeEventListener("dblclick", this, true); + this.browser.removeEventListener("mousedown", this, true); + this.browser.removeEventListener("mouseup", this, true); + }, + + attachPageListeners: function Highlighter_attachPageListeners() + { + this.browser.addEventListener("resize", this, true); + this.browser.addEventListener("scroll", this, true); + this.browser.addEventListener("MozAfterPaint", this, true); + }, + + detachPageListeners: function Highlighter_detachPageListeners() + { + this.browser.removeEventListener("resize", this, true); + this.browser.removeEventListener("scroll", this, true); + this.browser.removeEventListener("MozAfterPaint", this, true); + }, + + /** + * Generic event handler. + * + * @param nsIDOMEvent aEvent + * The DOM event object. + */ + handleEvent: function Highlighter_handleEvent(aEvent) + { + switch (aEvent.type) { + case "click": + this.handleClick(aEvent); + break; + case "mousemove": + this.brieflyIgnorePageEvents(); + this.handleMouseMove(aEvent); + break; + case "resize": + this.computeZoomFactor(); + break; + case "MozAfterPaint": + case "scroll": + this.brieflyDisableTransitions(); + this.invalidateSize(); + break; + case "dblclick": + case "mousedown": + case "mouseup": + aEvent.stopPropagation(); + aEvent.preventDefault(); + break; + } + }, + + /** + * Disable the CSS transitions for a short time to avoid laggy animations + * during scrolling or resizing. + */ + brieflyDisableTransitions: function Highlighter_brieflyDisableTransitions() + { + if (this.transitionDisabler) { + this.chromeWin.clearTimeout(this.transitionDisabler); + } else { + this.outline.setAttribute("disable-transitions", "true"); + this.nodeInfo.container.setAttribute("disable-transitions", "true"); + } + this.transitionDisabler = + this.chromeWin.setTimeout(function() { + this.outline.removeAttribute("disable-transitions"); + this.nodeInfo.container.removeAttribute("disable-transitions"); + this.transitionDisabler = null; + }.bind(this), 500); + }, + + /** + * Don't listen to page events while inspecting with the mouse. + */ + brieflyIgnorePageEvents: function Highlighter_brieflyIgnorePageEvents() + { + // The goal is to keep smooth animations while inspecting. + // CSS Transitions might be interrupted because of a MozAfterPaint + // event that would triger an invalidateSize() call. + // So we don't listen to events that would trigger an invalidateSize() + // call. + // + // Side effect, zoom levels are not updated during this short period. + // It's very unlikely this would happen, but just in case, we call + // computeZoomFactor() when reattaching the events. + if (this.pageEventsMuter) { + this.chromeWin.clearTimeout(this.pageEventsMuter); + } else { + this.detachPageListeners(); + } + this.pageEventsMuter = + this.chromeWin.setTimeout(function() { + this.attachPageListeners(); + // Just in case the zoom level changed while ignoring the paint events + this.computeZoomFactor(); + this.pageEventsMuter = null; + }.bind(this), 500); + }, + + /** + * Handle clicks. + * + * @param nsIDOMEvent aEvent + * The DOM event. + */ + handleClick: function Highlighter_handleClick(aEvent) + { + // Stop inspection when the user clicks on a node. + if (aEvent.button == 0) { + this.lock(); + let node = this.selection.node; + this.selection.setNode(node, "highlighter-lock"); + aEvent.preventDefault(); + aEvent.stopPropagation(); + } + }, + + /** + * Handle mousemoves in panel. + * + * @param nsiDOMEvent aEvent + * The MouseEvent triggering the method. + */ + handleMouseMove: function Highlighter_handleMouseMove(aEvent) + { + let doc = aEvent.target.ownerDocument; + + // This should never happen, but just in case, we don't let the + // highlighter highlight browser nodes. + if (doc && doc != this.chromeDoc) { + let element = LayoutHelpers.getElementFromPoint(aEvent.target.ownerDocument, + aEvent.clientX, aEvent.clientY); + if (element && element != this.selection.node) { + this.selection.setNode(element, "highlighter"); + } + } + }, +}; + +/////////////////////////////////////////////////////////////////////////// + +XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () { + return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils) +}); + +XPCOMUtils.defineLazyGetter(Highlighter.prototype, "strings", function () { + return Services.strings.createBundle( + "chrome://browser/locale/devtools/inspector.properties"); +});
new file mode 100644 --- /dev/null +++ b/browser/devtools/inspector/InspectorPanel.jsm @@ -0,0 +1,640 @@ +/* -*- 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"); +Cu.import("resource:///modules/devtools/CssLogic.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"); +XPCOMUtils.defineLazyModuleGetter(this, "SelectorSearch", + "resource:///modules/devtools/SelectorSearch.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.onNavigatedAway = this.onNavigatedAway.bind(this); + 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); + + // 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.setupSearchBox(); + 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; + }, + + /** + * Hooks the searchbar to show result and auto completion suggestions. + */ + setupSearchBox: function InspectorPanel_setupSearchBox() { + // Initiate the selectors search object. + let setNodeFunction = function(node) { + this.selection.setNode(node, "selectorsearch"); + }.bind(this); + if (this.searchSuggestions) { + this.searchSuggestions.destroy(); + this.searchSuggestions = null; + } + this.searchBox = this.panelDoc.getElementById("inspector-searchbox"); + this.searchSuggestions = new SelectorSearch(this.browser.contentDocument, + this.searchBox, + setNodeFunction); + }, + + /** + * 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.xhtml", + "ruleview" == defaultTab); + + this.sidebar.addTab("computedview", + "chrome://browser/content/devtools/computedview.xhtml", + "computedview" == defaultTab); + + if (Services.prefs.getBoolPref("devtools.fontinspector.enabled")) { + this.sidebar.addTab("fontinspector", + "chrome://browser/content/devtools/fontinspector/font-inspector.xhtml", + "fontinspector" == 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, payload) { + let newWindow = payload._navPayload || payload; + this.selection.setNode(null); + this._destroyMarkup(); + this.isDirty = false; + let self = this; + + function onDOMReady() { + newWindow.removeEventListener("DOMContentLoaded", onDOMReady, true); + + if (self._destroyed) { + return; + } + + if (!self.selection.node) { + self.selection.setNode(newWindow.document.documentElement, "navigateaway"); + } + self._initMarkup(); + self.setupSearchBox(); + } + + if (newWindow.document.readyState == "loading") { + newWindow.addEventListener("DOMContentLoaded", onDOMReady, true); + } else { + onDOMReady(); + } + }, + + /** + * 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("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.breadcrumbs.destroy(); + this.searchSuggestions.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.searchSuggestions = null; + this.lastNodemenuItem = null; + this.nodemenu = null; + this.highlighter = null; + + return Promise.resolve(null); + }, + + /** + * 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); + + if (this.selection.isElementNode()) { + let checked = DOMUtils.hasPseudoClassLock(this.selection.node, ":" + name); + menu.setAttribute("checked", checked); + menu.removeAttribute("disabled"); + } else { + menu.setAttribute("disabled", "true"); + } + } + + // Disable delete item if needed + let deleteNode = this.panelDoc.getElementById("node-menu-delete"); + if (this.selection.isRoot() || this.selection.isDocumentTypeNode()) { + deleteNode.setAttribute("disabled", "true"); + } else { + deleteNode.removeAttribute("disabled"); + } + + // Disable / enable "Copy Unique Selector", "Copy inner HTML" & + // "Copy outer HTML" as appropriate + let unique = this.panelDoc.getElementById("node-menu-copyuniqueselector"); + let copyInnerHTML = this.panelDoc.getElementById("node-menu-copyinner"); + let copyOuterHTML = this.panelDoc.getElementById("node-menu-copyouter"); + if (this.selection.isElementNode()) { + unique.removeAttribute("disabled"); + copyInnerHTML.removeAttribute("disabled"); + copyOuterHTML.removeAttribute("disabled"); + } else { + unique.setAttribute("disabled", "true"); + copyInnerHTML.setAttribute("disabled", "true"); + copyOuterHTML.setAttribute("disabled", "true"); + } + }, + + _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"); + this.breadcrumbs.scroll(); + }, + + /** + * Clear any pseudo-class locks applied to the current hierarchy. + */ + clearPseudoClasses: function InspectorPanel_clearPseudoClasses() { + this.breadcrumbs.nodeHierarchy.forEach(function(crumb) { + try { + DOMUtils.clearPseudoClassLocks(crumb.node); + } catch(e) { + // Ignore dead nodes after navigation. + } + }); + }, + + /** + * 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); + } + }, + + /** + * Copy a unique selector of the selected Node to the clipboard. + */ + copyUniqueSelector: function InspectorPanel_copyUniqueSelector() + { + if (!this.selection.isNode()) { + return; + } + + let toCopy = CssLogic.findCssSelector(this.selection.node); + 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); +});
--- a/browser/devtools/inspector/Makefile.in +++ b/browser/devtools/inspector/Makefile.in @@ -8,9 +8,8 @@ srcdir = @srcdir@ VPATH = @srcdir@ include $(DEPTH)/config/autoconf.mk include $(topsrcdir)/config/rules.mk libs:: $(NSINSTALL) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools/ - $(NSINSTALL) $(srcdir)/*.js $(FINAL_TARGET)/modules/devtools/inspector
new file mode 100644 --- /dev/null +++ b/browser/devtools/inspector/Selection.jsm @@ -0,0 +1,235 @@ +/* -*- 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 Cu = Components.utils; +Cu.import("resource:///modules/devtools/EventEmitter.jsm"); + +this.EXPORTED_SYMBOLS = ["Selection"]; + +/** + * API + * + * new Selection(node=null, track={attributes,detached}); + * destroy() + * node (readonly) + * setNode(node, origin="unknown") + * + * Helpers: + * + * window + * document + * isRoot() + * isNode() + * isHTMLNode() + * + * Check the nature of the node: + * + * isElementNode() + * isAttributeNode() + * isTextNode() + * isCDATANode() + * isEntityRefNode() + * isEntityNode() + * isProcessingInstructionNode() + * isCommentNode() + * isDocumentNode() + * isDocumentTypeNode() + * isDocumentFragmentNode() + * isNotationNode() + * + * Events: + * "new-node" when the inner node changed + * "before-new-node" when the inner node is set to change + * "attribute-changed" when an attribute is changed (only if tracked) + * "detached" when the node (or one of its parents) is removed from the document (only if tracked) + * "reparented" when the node (or one of its parents) is moved under a different node (only if tracked) + */ + +/** + * A Selection object. Hold a reference to a node. + * Includes some helpers, fire some helpful events. + * + * @param node Inner node. + * Can be null. Can be (un)set in the future via the "node" property; + * @param trackAttribute Tell if events should be fired when the attributes of + * the ndoe change. + * + */ +this.Selection = function Selection(node=null, track={attributes:true,detached:true}) { + EventEmitter.decorate(this); + this._onMutations = this._onMutations.bind(this); + this.track = track; + this.setNode(node); +} + +Selection.prototype = { + _node: null, + + _onMutations: function(mutations) { + let attributeChange = false; + let detached = false; + let parentNode = null; + for (let m of mutations) { + if (!attributeChange && m.type == "attributes") { + attributeChange = true; + } + if (m.type == "childList") { + if (!detached && !this.isConnected()) { + parentNode = m.target; + detached = true; + } + } + } + + if (attributeChange) + this.emit("attribute-changed"); + if (detached) + this.emit("detached", parentNode); + }, + + _attachEvents: function SN__attachEvents() { + if (!this.window || !this.isNode() || !this.track) { + return; + } + + if (this.track.attributes) { + this._nodeObserver = new this.window.MutationObserver(this._onMutations); + this._nodeObserver.observe(this.node, {attributes: true}); + } + + if (this.track.detached) { + this._docObserver = new this.window.MutationObserver(this._onMutations); + this._docObserver.observe(this.document.documentElement, {childList: true, subtree: true}); + } + }, + + _detachEvents: function SN__detachEvents() { + // `disconnect` fail if node's document has + // been deleted. + try { + if (this._nodeObserver) + this._nodeObserver.disconnect(); + } catch(e) {} + try { + if (this._docObserver) + this._docObserver.disconnect(); + } catch(e) {} + }, + + destroy: function SN_destroy() { + this._detachEvents(); + this.setNode(null); + }, + + setNode: function SN_setNode(value, reason="unknown") { + this.reason = reason; + if (value !== this._node) { + this.emit("before-new-node", value, reason); + let previousNode = this._node; + this._detachEvents(); + this._node = value; + this._attachEvents(); + this.emit("new-node", previousNode, this.reason); + } + }, + + get node() { + return this._node; + }, + + get window() { + if (this.isNode()) { + return this.node.ownerDocument.defaultView; + } + return null; + }, + + get document() { + if (this.isNode()) { + return this.node.ownerDocument; + } + return null; + }, + + isRoot: function SN_isRootNode() { + return this.isNode() && + this.isConnected() && + this.node.ownerDocument.documentElement === this.node; + }, + + isNode: function SN_isNode() { + return (this.node && + !Components.utils.isDeadWrapper(this.node) && + this.node.ownerDocument && + this.node.ownerDocument.defaultView && + this.node instanceof this.node.ownerDocument.defaultView.Node); + }, + + isConnected: function SN_isConnected() { + try { + let doc = this.document; + return doc && doc.defaultView && doc.documentElement.contains(this.node); + } catch (e) { + // "can't access dead object" error + return false; + } + }, + + isHTMLNode: function SN_isHTMLNode() { + let xhtml_ns = "http://www.w3.org/1999/xhtml"; + return this.isNode() && this.node.namespaceURI == xhtml_ns; + }, + + // Node type + + isElementNode: function SN_isElementNode() { + return this.isNode() && this.node.nodeType == this.window.Node.ELEMENT_NODE; + }, + + isAttributeNode: function SN_isAttributeNode() { + return this.isNode() && this.node.nodeType == this.window.Node.ATTRIBUTE_NODE; + }, + + isTextNode: function SN_isTextNode() { + return this.isNode() && this.node.nodeType == this.window.Node.TEXT_NODE; + }, + + isCDATANode: function SN_isCDATANode() { + return this.isNode() && this.node.nodeType == this.window.Node.CDATA_SECTION_NODE; + }, + + isEntityRefNode: function SN_isEntityRefNode() { + return this.isNode() && this.node.nodeType == this.window.Node.ENTITY_REFERENCE_NODE; + }, + + isEntityNode: function SN_isEntityNode() { + return this.isNode() && this.node.nodeType == this.window.Node.ENTITY_NODE; + }, + + isProcessingInstructionNode: function SN_isProcessingInstructionNode() { + return this.isNode() && this.node.nodeType == this.window.Node.PROCESSING_INSTRUCTION_NODE; + }, + + isCommentNode: function SN_isCommentNode() { + return this.isNode() && this.node.nodeType == this.window.Node.PROCESSING_INSTRUCTION_NODE; + }, + + isDocumentNode: function SN_isDocumentNode() { + return this.isNode() && this.node.nodeType == this.window.Node.DOCUMENT_NODE; + }, + + isDocumentTypeNode: function SN_isDocumentTypeNode() { + return this.isNode() && this.node.nodeType ==this.window. Node.DOCUMENT_TYPE_NODE; + }, + + isDocumentFragmentNode: function SN_isDocumentFragmentNode() { + return this.isNode() && this.node.nodeType == this.window.Node.DOCUMENT_FRAGMENT_NODE; + }, + + isNotationNode: function SN_isNotationNode() { + return this.isNode() && this.node.nodeType == this.window.Node.NOTATION_NODE; + }, +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/inspector/SelectorSearch.jsm @@ -0,0 +1,549 @@ +/* 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/. */ + +"use strict"; + +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "AutocompletePopup", + "resource:///modules/devtools/AutocompletePopup.jsm"); +this.EXPORTED_SYMBOLS = ["SelectorSearch"]; + +// Maximum number of selector suggestions shown in the panel. +const MAX_SUGGESTIONS = 15; + +/** + * Converts any input box on a page to a CSS selector search and suggestion box. + * + * @constructor + * @param nsIDOMDocument aContentDocument + * The content document which inspector is attached to. + * @param nsiInputElement aInputNode + * The input element to which the panel will be attached and from where + * search input will be taken. + * @param Function aCallback + * The method to callback when a search is available. + * This method is called with the matched node as the first argument. + */ +this.SelectorSearch = function(aContentDocument, aInputNode, aCallback) { + this.doc = aContentDocument; + this.callback = aCallback; + this.searchBox = aInputNode; + this.panelDoc = this.searchBox.ownerDocument; + + // initialize variables. + this._lastSearched = null; + this._lastValidSearch = ""; + this._lastToLastValidSearch = null; + this._searchResults = null; + this._searchSuggestions = {}; + this._searchIndex = 0; + + // bind! + this._showPopup = this._showPopup.bind(this); + this._onHTMLSearch = this._onHTMLSearch.bind(this); + this._onSearchKeypress = this._onSearchKeypress.bind(this); + this._onListBoxKeypress = this._onListBoxKeypress.bind(this); + + // Options for the AutocompletePopup. + let options = { + panelId: "inspector-searchbox-panel", + listBoxId: "searchbox-panel-listbox", + fixedWidth: true, + autoSelect: true, + position: "before_start", + direction: "ltr", + onClick: this._onListBoxKeypress, + onKeypress: this._onListBoxKeypress, + }; + this.searchPopup = new AutocompletePopup(this.panelDoc, options); + + // event listeners. + this.searchBox.addEventListener("command", this._onHTMLSearch, true); + this.searchBox.addEventListener("keypress", this._onSearchKeypress, true); +} + +this.SelectorSearch.prototype = { + + // The possible states of the query. + States: { + CLASS: "class", + ID: "id", + TAG: "tag", + }, + + // The current state of the query. + _state: null, + + // The query corresponding to last state computation. + _lastStateCheckAt: null, + + /** + * Computes the state of the query. State refers to whether the query + * currently requires a class suggestion, or a tag, or an Id suggestion. + * This getter will effectively compute the state by traversing the query + * character by character each time the query changes. + * + * @example + * '#f' requires an Id suggestion, so the state is States.ID + * 'div > .foo' requires class suggestion, so state is States.CLASS + */ + get state() { + if (!this.searchBox || !this.searchBox.value) { + return null; + } + + let query = this.searchBox.value; + if (this._lastStateCheckAt == query) { + // If query is the same, return early. + return this._state; + } + this._lastStateCheckAt = query; + + this._state = null; + let subQuery = ""; + // Now we iterate over the query and decide the state character by character. + // The logic here is that while iterating, the state can go from one to + // another with some restrictions. Like, if the state is Class, then it can + // never go to Tag state without a space or '>' character; Or like, a Class + // state with only '.' cannot go to an Id state without any [a-zA-Z] after + // the '.' which means that '.#' is a selector matching a class name '#'. + // Similarily for '#.' which means a selctor matching an id '.'. + for (let i = 1; i <= query.length; i++) { + // Calculate the state. + subQuery = query.slice(0, i); + let [secondLastChar, lastChar] = subQuery.slice(-2); + switch (this._state) { + case null: + // This will happen only in the first iteration of the for loop. + lastChar = secondLastChar; + case this.States.TAG: + this._state = lastChar == "." + ? this.States.CLASS + : lastChar == "#" + ? this.States.ID + : this.States.TAG; + break; + + case this.States.CLASS: + if (subQuery.match(/[\.]+[^\.]*$/)[0].length > 2) { + // Checks whether the subQuery has atleast one [a-zA-Z] after the '.'. + this._state = (lastChar == " " || lastChar == ">") + ? this.States.TAG + : lastChar == "#" + ? this.States.ID + : this.States.CLASS; + } + break; + + case this.States.ID: + if (subQuery.match(/[#]+[^#]*$/)[0].length > 2) { + // Checks whether the subQuery has atleast one [a-zA-Z] after the '#'. + this._state = (lastChar == " " || lastChar == ">") + ? this.States.TAG + : lastChar == "." + ? this.States.CLASS + : this.States.ID; + } + break; + } + } + return this._state; + }, + + /** + * Removes event listeners and cleans up references. + */ + destroy: function SelectorSearch_destroy() { + // event listeners. + this.searchBox.removeEventListener("command", this._onHTMLSearch, true); + this.searchBox.removeEventListener("keypress", this._onSearchKeypress, true); + this.searchPopup.destroy(); + this.searchPopup = null; + this.searchBox = null; + this.doc = null; + this.panelDoc = null; + this._searchResults = null; + this._searchSuggestions = null; + this.callback = null; + }, + + /** + * The command callback for the input box. This function is automatically + * invoked as the user is typing if the input box type is search. + */ + _onHTMLSearch: function SelectorSearch__onHTMLSearch() { + let query = this.searchBox.value; + if (query == this._lastSearched) { + return; + } + this._lastSearched = query; + this._searchIndex = 0; + + if (query.length == 0) { + this._lastValidSearch = ""; + this.searchBox.removeAttribute("filled"); + this.searchBox.classList.remove("devtools-no-search-result"); + if (this.searchPopup.isOpen) { + this.searchPopup.hidePopup(); + } + return; + } + + this.searchBox.setAttribute("filled", true); + try { + this._searchResults = this.doc.querySelectorAll(query); + } + catch (ex) { + this._searchResults = []; + } + if (this._searchResults.length > 0) { + this._lastValidSearch = query; + // Even though the selector matched atleast one node, there is still + // possibility of suggestions. + if (query.match(/[\s>+]$/)) { + // If the query has a space or '>' at the end, create a selector to match + // the children of the selector inside the search box by adding a '*'. + this._lastValidSearch += "*"; + } + else if (query.match(/[\s>+][\.#a-zA-Z][\.#>\s+]*$/)) { + // If the query is a partial descendant selector which does not matches + // any node, remove the last incomplete part and add a '*' to match + // everything. For ex, convert 'foo > b' to 'foo > *' . + let lastPart = query.match(/[\s>+][\.#a-zA-Z][^>\s+]*$/)[0]; + this._lastValidSearch = query.slice(0, -1 * lastPart.length + 1) + "*"; + } + + if (!query.slice(-1).match(/[\.#\s>+]/)) { + // Hide the popup if we have some matching nodes and the query is not + // ending with [.# >] which means that the selector is not at the + // beginning of a new class, tag or id. + if (this.searchPopup.isOpen) { + this.searchPopup.hidePopup(); + } + } + else { + this.showSuggestions(); + } + this.searchBox.classList.remove("devtools-no-search-result"); + this.callback(this._searchResults[0]); + } + else { + if (query.match(/[\s>+]$/)) { + this._lastValidSearch = query + "*"; + } + else if (query.match(/[\s>+][\.#a-zA-Z][\.#>\s+]*$/)) { + let lastPart = query.match(/[\s+>][\.#a-zA-Z][^>\s+]*$/)[0]; + this._lastValidSearch = query.slice(0, -1 * lastPart.length + 1) + "*"; + } + this.searchBox.classList.add("devtools-no-search-result"); + this.showSuggestions(); + } + }, + + /** + * Handles keypresses inside the input box. + */ + _onSearchKeypress: function SelectorSearch__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.searchPopup.isOpen && this.searchPopup.itemCount > 0) { + this.searchPopup.focus(); + if (this.searchPopup.selectedIndex == this.searchPopup.itemCount - 1) { + this.searchPopup.selectedIndex = + Math.max(0, this.searchPopup.itemCount - 2); + } + else { + this.searchPopup.selectedIndex = this.searchPopup.itemCount - 1; + } + this.searchBox.value = this.searchPopup.selectedItem.label; + } + else if (--this._searchIndex < 0) { + this._searchIndex = this._searchResults.length - 1; + } + break; + + case aEvent.DOM_VK_DOWN: + if (this.searchPopup.isOpen && this.searchPopup.itemCount > 0) { + this.searchPopup.focus(); + this.searchPopup.selectedIndex = 0; + this.searchBox.value = this.searchPopup.selectedItem.label; + } + this._searchIndex = (this._searchIndex + 1) % this._searchResults.length; + break; + + case aEvent.DOM_VK_TAB: + if (this.searchPopup.isOpen && + this.searchPopup.getItemAtIndex(this.searchPopup.itemCount - 1) + .preLabel == query) { + this.searchPopup.selectedIndex = this.searchPopup.itemCount - 1; + this.searchBox.value = this.searchPopup.selectedItem.label; + this._onHTMLSearch(); + } + break; + + case aEvent.DOM_VK_BACK_SPACE: + case aEvent.DOM_VK_DELETE: + // need to throw away the lastValidSearch. + this._lastToLastValidSearch = null; + // This gets the most complete selector from the query. For ex. + // '.foo.ba' returns '.foo' , '#foo > .bar.baz' returns '#foo > .bar' + // '.foo +bar' returns '.foo +' and likewise. + this._lastValidSearch = (query.match(/(.*)[\.#][^\.# ]{0,}$/) || + query.match(/(.*[\s>+])[a-zA-Z][^\.# ]{0,}$/) || + ["",""])[1]; + return; + + default: + return; + } + + aEvent.preventDefault(); + aEvent.stopPropagation(); + if (this._searchResults.length > 0) { + this.callback(this._searchResults[this._searchIndex]); + } + }, + + /** + * Handles keypress and mouse click on the suggestions richlistbox. + */ + _onListBoxKeypress: function SelectorSearch__onListBoxKeypress(aEvent) { + switch(aEvent.keyCode || aEvent.button) { + case aEvent.DOM_VK_ENTER: + case aEvent.DOM_VK_RETURN: + case aEvent.DOM_VK_TAB: + case 0: // left mouse button + aEvent.stopPropagation(); + aEvent.preventDefault(); + this.searchBox.value = this.searchPopup.selectedItem.label; + this.searchBox.focus(); + this._onHTMLSearch(); + break; + + case aEvent.DOM_VK_UP: + if (this.searchPopup.selectedIndex == 0) { + this.searchPopup.selectedIndex = -1; + aEvent.stopPropagation(); + aEvent.preventDefault(); + this.searchBox.focus(); + } + else { + let index = this.searchPopup.selectedIndex; + this.searchBox.value = this.searchPopup.getItemAtIndex(index - 1).label; + } + break; + + case aEvent.DOM_VK_DOWN: + if (this.searchPopup.selectedIndex == this.searchPopup.itemCount - 1) { + this.searchPopup.selectedIndex = -1; + aEvent.stopPropagation(); + aEvent.preventDefault(); + this.searchBox.focus(); + } + else { + let index = this.searchPopup.selectedIndex; + this.searchBox.value = this.searchPopup.getItemAtIndex(index + 1).label; + } + break; + + case aEvent.DOM_VK_BACK_SPACE: + aEvent.stopPropagation(); + aEvent.preventDefault(); + this.searchBox.focus(); + if (this.searchBox.selectionStart > 0) { + this.searchBox.value = + this.searchBox.value.substring(0, this.searchBox.selectionStart - 1); + } + this._lastToLastValidSearch = null; + let query = this.searchBox.value; + this._lastValidSearch = (query.match(/(.*)[\.#][^\.# ]{0,}$/) || + query.match(/(.*[\s>+])[a-zA-Z][^\.# ]{0,}$/) || + ["",""])[1]; + this._onHTMLSearch(); + break; + } + }, + + + /** + * Populates the suggestions list and show the suggestion popup. + */ + _showPopup: function SelectorSearch__showPopup(aList, aFirstPart) { + // Sort alphabetically in increaseing order. + aList = aList.sort(); + // Sort based on count= in decreasing order. + aList = aList.sort(function([a1,a2], [b1,b2]) { + return a2 < b2; + }); + + let total = 0; + let query = this.searchBox.value; + let toLowerCase = false; + let items = []; + // In case of tagNames, change the case to small. + if (query.match(/.*[\.#][^\.#]{0,}$/) == null) { + toLowerCase = true; + } + for (let [value, count] of aList) { + // for cases like 'div ' or 'div >' or 'div+' + if (query.match(/[\s>+]$/)) { + value = query + value; + } + // for cases like 'div #a' or 'div .a' or 'div > d' and likewise + else if (query.match(/[\s>+][\.#a-zA-Z][^\s>+\.#]*$/)) { + let lastPart = query.match(/[\s>+][\.#a-zA-Z][^>\s+\.#]*$/)[0]; + value = query.slice(0, -1 * lastPart.length + 1) + value; + } + // for cases like 'div.class' or '#foo.bar' and likewise + else if (query.match(/[a-zA-Z][#\.][^#\.\s+>]*$/)) { + let lastPart = query.match(/[a-zA-Z][#\.][^#\.\s>+]*$/)[0]; + value = query.slice(0, -1 * lastPart.length + 1) + value; + } + let item = { + preLabel: query, + label: value, + count: count + }; + if (toLowerCase) { + item.label = value.toLowerCase(); + } + items.unshift(item); + if (++total > MAX_SUGGESTIONS - 1) { + break; + } + } + if (total > 0) { + this.searchPopup.setItems(items); + this.searchPopup.openPopup(this.searchBox); + } + else { + this.searchPopup.hidePopup(); + } + }, + + /** + * Suggests classes,ids and tags based on the user input as user types in the + * searchbox. + */ + showSuggestions: function SelectorSearch_showSuggestions() { + let query = this.searchBox.value; + if (this._lastValidSearch != "" && + this._lastToLastValidSearch != this._lastValidSearch) { + this._searchSuggestions = { + ids: new Map(), + classes: new Map(), + tags: new Map(), + }; + + let nodes = []; + try { + nodes = this.doc.querySelectorAll(this._lastValidSearch); + } catch (ex) {} + for (let node of nodes) { + this._searchSuggestions.ids.set(node.id, 1); + this._searchSuggestions.tags + .set(node.tagName, + (this._searchSuggestions.tags.get(node.tagName) || 0) + 1); + for (let className of node.classList) { + this._searchSuggestions.classes + .set(className, + (this._searchSuggestions.classes.get(className) || 0) + 1); + } + } + this._lastToLastValidSearch = this._lastValidSearch; + } + else if (this._lastToLastValidSearch != this._lastValidSearch) { + this._searchSuggestions = { + ids: new Map(), + classes: new Map(), + tags: new Map(), + }; + + if (query.length == 0) { + return; + } + + let nodes = null; + if (this.state == this.States.CLASS) { + nodes = this.doc.querySelectorAll("[class]"); + for (let node of nodes) { + for (let className of node.classList) { + this._searchSuggestions.classes + .set(className, + (this._searchSuggestions.classes.get(className) || 0) + 1); + } + } + } + else if (this.state == this.States.ID) { + nodes = this.doc.querySelectorAll("[id]"); + for (let node of nodes) { + this._searchSuggestions.ids.set(node.id, 1); + } + } + else if (this.state == this.States.TAG) { + nodes = this.doc.getElementsByTagName("*"); + for (let node of nodes) { + this._searchSuggestions.tags + .set(node.tagName, + (this._searchSuggestions.tags.get(node.tagName) || 0) + 1); + } + } + else { + return; + } + this._lastToLastValidSearch = this._lastValidSearch; + } + + // Filter the suggestions based on search box value. + let result = []; + let firstPart = ""; + if (this.state == this.States.TAG) { + // gets the tag that is being completed. For ex. 'div.foo > s' returns 's', + // 'di' returns 'di' and likewise. + firstPart = (query.match(/[\s>+]?([a-zA-Z]*)$/) || ["",query])[1]; + for (let [tag, count] of this._searchSuggestions.tags) { + if (tag.toLowerCase().startsWith(firstPart.toLowerCase())) { + result.push([tag, count]); + } + } + } + else if (this.state == this.States.CLASS) { + // gets the class that is being completed. For ex. '.foo.b' returns 'b' + firstPart = query.match(/\.([^\.]*)$/)[1]; + for (let [className, count] of this._searchSuggestions.classes) { + if (className.startsWith(firstPart)) { + result.push(["." + className, count]); + } + } + firstPart = "." + firstPart; + } + else if (this.state == this.States.ID) { + // gets the id that is being completed. For ex. '.foo#b' returns 'b' + firstPart = query.match(/#([^#]*)$/)[1]; + for (let [id, count] of this._searchSuggestions.ids) { + if (id.startsWith(firstPart)) { + result.push(["#" + id, 1]); + } + } + firstPart = "#" + firstPart; + } + + this._showPopup(result, firstPart); + }, +};
deleted file mode 100644 --- a/browser/devtools/inspector/breadcrumbs.js +++ /dev/null @@ -1,597 +0,0 @@ -/* -*- 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 {Cc, Cu, Ci} = require("chrome"); - -const PSEUDO_CLASSES = [":hover", ":active", ":focus"]; -const ENSURE_SELECTION_VISIBLE_DELAY = 50; // ms - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource:///modules/devtools/DOMHelpers.jsm"); -Cu.import("resource:///modules/devtools/LayoutHelpers.jsm"); - -const LOW_PRIORITY_ELEMENTS = { - "HEAD": true, - "BASE": true, - "BASEFONT": true, - "ISINDEX": true, - "LINK": true, - "META": true, - "SCRIPT": true, - "STYLE": true, - "TITLE": true, -}; - -/////////////////////////////////////////////////////////////////////////// -//// HTML Breadcrumbs - -/** - * Display the ancestors of the current node and its children. - * Only one "branch" of children are displayed (only one line). - * - * FIXME: Bug 822388 - Use the BreadcrumbsWidget in the Inspector. - * - * Mechanism: - * . If no nodes displayed yet: - * then display the ancestor of the selected node and the selected node; - * else select the node; - * . If the selected node is the last node displayed, append its first (if any). - */ -function HTMLBreadcrumbs(aInspector) -{ - this.inspector = aInspector; - this.selection = this.inspector.selection; - this.chromeWin = this.inspector.panelWin; - this.chromeDoc = this.inspector.panelDoc; - this.DOMHelpers = new DOMHelpers(this.chromeWin); - this._init(); -} - -exports.HTMLBreadcrumbs = HTMLBreadcrumbs; - -HTMLBreadcrumbs.prototype = { - _init: function BC__init() - { - this.container = this.chromeDoc.getElementById("inspector-breadcrumbs"); - this.container.addEventListener("mousedown", this, true); - this.container.addEventListener("keypress", this, true); - - // We will save a list of already displayed nodes in this array. - this.nodeHierarchy = []; - - // Last selected node in nodeHierarchy. - this.currentIndex = -1; - - // By default, hide the arrows. We let the <scrollbox> show them - // in case of overflow. - this.container.removeAttribute("overflows"); - this.container._scrollButtonUp.collapsed = true; - this.container._scrollButtonDown.collapsed = true; - - this.onscrollboxreflow = function() { - if (this.container._scrollButtonDown.collapsed) - this.container.removeAttribute("overflows"); - else - this.container.setAttribute("overflows", true); - }.bind(this); - - this.container.addEventListener("underflow", this.onscrollboxreflow, false); - this.container.addEventListener("overflow", this.onscrollboxreflow, false); - - this.update = this.update.bind(this); - this.updateSelectors = this.updateSelectors.bind(this); - this.selection.on("new-node", this.update); - this.selection.on("pseudoclass", this.updateSelectors); - this.selection.on("attribute-changed", this.updateSelectors); - this.update(); - }, - - /** - * Build a string that represents the node: tagName#id.class1.class2. - * - * @param aNode The node to pretty-print - * @returns a string - */ - prettyPrintNodeAsText: function BC_prettyPrintNodeText(aNode) - { - let text = aNode.tagName.toLowerCase(); - if (aNode.id) { - text += "#" + aNode.id; - } - for (let i = 0; i < aNode.classList.length; i++) { - text += "." + aNode.classList[i]; - } - for (let i = 0; i < PSEUDO_CLASSES.length; i++) { - let pseudo = PSEUDO_CLASSES[i]; - if (DOMUtils.hasPseudoClassLock(aNode, pseudo)) { - text += pseudo; - } - } - - return text; - }, - - - /** - * Build <label>s that represent the node: - * <label class="breadcrumbs-widget-item-tag">tagName</label> - * <label class="breadcrumbs-widget-item-id">#id</label> - * <label class="breadcrumbs-widget-item-classes">.class1.class2</label> - * - * @param aNode The node to pretty-print - * @returns a document fragment. - */ - prettyPrintNodeAsXUL: function BC_prettyPrintNodeXUL(aNode) - { - let fragment = this.chromeDoc.createDocumentFragment(); - - let tagLabel = this.chromeDoc.createElement("label"); - tagLabel.className = "breadcrumbs-widget-item-tag plain"; - - let idLabel = this.chromeDoc.createElement("label"); - idLabel.className = "breadcrumbs-widget-item-id plain"; - - let classesLabel = this.chromeDoc.createElement("label"); - classesLabel.className = "breadcrumbs-widget-item-classes plain"; - - let pseudosLabel = this.chromeDoc.createElement("label"); - pseudosLabel.className = "breadcrumbs-widget-item-pseudo-classes plain"; - - tagLabel.textContent = aNode.tagName.toLowerCase(); - idLabel.textContent = aNode.id ? ("#" + aNode.id) : ""; - - let classesText = ""; - for (let i = 0; i < aNode.classList.length; i++) { - classesText += "." + aNode.classList[i]; - } - classesLabel.textContent = classesText; - - let pseudos = PSEUDO_CLASSES.filter(function(pseudo) { - return DOMUtils.hasPseudoClassLock(aNode, pseudo); - }, this); - pseudosLabel.textContent = pseudos.join(""); - - fragment.appendChild(tagLabel); - fragment.appendChild(idLabel); - fragment.appendChild(classesLabel); - fragment.appendChild(pseudosLabel); - - return fragment; - }, - - /** - * Open the sibling menu. - * - * @param aButton the button representing the node. - * @param aNode the node we want the siblings from. - */ - openSiblingMenu: function BC_openSiblingMenu(aButton, aNode) - { - // We make sure that the targeted node is selected - // because we want to use the nodemenu that only works - // for inspector.selection - this.selection.setNode(aNode, "breadcrumbs"); - - let title = this.chromeDoc.createElement("menuitem"); - title.setAttribute("label", this.inspector.strings.GetStringFromName("breadcrumbs.siblings")); - title.setAttribute("disabled", "true"); - - let separator = this.chromeDoc.createElement("menuseparator"); - - let items = [title, separator]; - - let nodes = aNode.parentNode.childNodes; - for (let i = 0; i < nodes.length; i++) { - if (nodes[i].nodeType == aNode.ELEMENT_NODE) { - let item = this.chromeDoc.createElement("menuitem"); - if (nodes[i] === aNode) { - item.setAttribute("disabled", "true"); - item.setAttribute("checked", "true"); - } - - item.setAttribute("type", "radio"); - item.setAttribute("label", this.prettyPrintNodeAsText(nodes[i])); - - let selection = this.selection; - item.onmouseup = (function(aNode) { - return function() { - selection.setNode(aNode, "breadcrumbs"); - } - })(nodes[i]); - - items.push(item); - } - } - this.inspector.showNodeMenu(aButton, "before_start", items); - }, - - /** - * Generic event handler. - * - * @param nsIDOMEvent event - * The DOM event object. - */ - handleEvent: function BC_handleEvent(event) - { - if (event.type == "mousedown" && event.button == 0) { - // on Click and Hold, open the Siblings menu - - let timer; - let container = this.container; - - function openMenu(event) { - cancelHold(); - let target = event.originalTarget; - if (target.tagName == "button") { - target.onBreadcrumbsHold(); - } - } - - function handleClick(event) { - cancelHold(); - let target = event.originalTarget; - if (target.tagName == "button") { - target.onBreadcrumbsClick(); - } - } - - let window = this.chromeWin; - function cancelHold(event) { - window.clearTimeout(timer); - container.removeEventListener("mouseout", cancelHold, false); - container.removeEventListener("mouseup", handleClick, false); - } - - container.addEventListener("mouseout", cancelHold, false); - container.addEventListener("mouseup", handleClick, false); - timer = window.setTimeout(openMenu, 500, event); - } - - if (event.type == "keypress" && this.selection.isElementNode()) { - let node = null; - switch (event.keyCode) { - case this.chromeWin.KeyEvent.DOM_VK_LEFT: - if (this.currentIndex != 0) { - node = this.nodeHierarchy[this.currentIndex - 1].node; - } - break; - case this.chromeWin.KeyEvent.DOM_VK_RIGHT: - if (this.currentIndex < this.nodeHierarchy.length - 1) { - node = this.nodeHierarchy[this.currentIndex + 1].node; - } - break; - case this.chromeWin.KeyEvent.DOM_VK_UP: - node = this.selection.node.previousSibling; - while (node && (node.nodeType != node.ELEMENT_NODE)) { - node = node.previousSibling; - } - break; - case this.chromeWin.KeyEvent.DOM_VK_DOWN: - node = this.selection.node.nextSibling; - while (node && (node.nodeType != node.ELEMENT_NODE)) { - node = node.nextSibling; - } - break; - } - if (node) { - this.selection.setNode(node, "breadcrumbs"); - } - event.preventDefault(); - event.stopPropagation(); - } - }, - - /** - * Remove nodes and delete properties. - */ - destroy: function BC_destroy() - { - this.nodeHierarchy.forEach(function(crumb) { - if (LayoutHelpers.isNodeConnected(crumb.node)) { - DOMUtils.clearPseudoClassLocks(crumb.node); - } - }); - - this.selection.off("new-node", this.update); - this.selection.off("pseudoclass", this.updateSelectors); - this.selection.off("attribute-changed", this.updateSelectors); - - this.container.removeEventListener("underflow", this.onscrollboxreflow, false); - this.container.removeEventListener("overflow", this.onscrollboxreflow, false); - this.onscrollboxreflow = null; - - this.empty(); - this.container.removeEventListener("mousedown", this, true); - this.container.removeEventListener("keypress", this, true); - this.container = null; - this.nodeHierarchy = null; - }, - - /** - * Empty the breadcrumbs container. - */ - empty: function BC_empty() - { - while (this.container.hasChildNodes()) { - this.container.removeChild(this.container.firstChild); - } - }, - - /** - * Re-init the cache and remove all the buttons. - */ - invalidateHierarchy: function BC_invalidateHierarchy() - { - this.inspector.hideNodeMenu(); - this.nodeHierarchy = []; - this.empty(); - }, - - /** - * Set which button represent the selected node. - * - * @param aIdx Index of the displayed-button to select - */ - setCursor: function BC_setCursor(aIdx) - { - // Unselect the previously selected button - if (this.currentIndex > -1 && this.currentIndex < this.nodeHierarchy.length) { - this.nodeHierarchy[this.currentIndex].button.removeAttribute("checked"); - } - if (aIdx > -1) { - this.nodeHierarchy[aIdx].button.setAttribute("checked", "true"); - if (this.hadFocus) - this.nodeHierarchy[aIdx].button.focus(); - } - this.currentIndex = aIdx; - }, - - /** - * Get the index of the node in the cache. - * - * @param aNode - * @returns integer the index, -1 if not found - */ - indexOf: function BC_indexOf(aNode) - { - let i = this.nodeHierarchy.length - 1; - for (let i = this.nodeHierarchy.length - 1; i >= 0; i--) { - if (this.nodeHierarchy[i].node === aNode) { - return i; - } - } - return -1; - }, - - /** - * Remove all the buttons and their references in the cache - * after a given index. - * - * @param aIdx - */ - cutAfter: function BC_cutAfter(aIdx) - { - while (this.nodeHierarchy.length > (aIdx + 1)) { - let toRemove = this.nodeHierarchy.pop(); - this.container.removeChild(toRemove.button); - } - }, - - /** - * Build a button representing the node. - * - * @param aNode The node from the page. - * @returns aNode The <button>. - */ - buildButton: function BC_buildButton(aNode) - { - let button = this.chromeDoc.createElement("button"); - button.appendChild(this.prettyPrintNodeAsXUL(aNode)); - button.className = "breadcrumbs-widget-item"; - - button.setAttribute("tooltiptext", this.prettyPrintNodeAsText(aNode)); - - button.onkeypress = function onBreadcrumbsKeypress(e) { - if (e.charCode == Ci.nsIDOMKeyEvent.DOM_VK_SPACE || - e.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RETURN) - button.click(); - } - - button.onBreadcrumbsClick = function onBreadcrumbsClick() { - this.selection.setNode(aNode, "breadcrumbs"); - }.bind(this); - - button.onclick = (function _onBreadcrumbsRightClick(event) { - button.focus(); - if (event.button == 2) { - this.openSiblingMenu(button, aNode); - } - }).bind(this); - - button.onBreadcrumbsHold = (function _onBreadcrumbsHold() { - this.openSiblingMenu(button, aNode); - }).bind(this); - return button; - }, - - /** - * Connecting the end of the breadcrumbs to a node. - * - * @param aNode The node to reach. - */ - expand: function BC_expand(aNode) - { - let fragment = this.chromeDoc.createDocumentFragment(); - let toAppend = aNode; - let lastButtonInserted = null; - let originalLength = this.nodeHierarchy.length; - let stopNode = null; - if (originalLength > 0) { - stopNode = this.nodeHierarchy[originalLength - 1].node; - } - while (toAppend && toAppend.tagName && toAppend != stopNode) { - let button = this.buildButton(toAppend); - fragment.insertBefore(button, lastButtonInserted); - lastButtonInserted = button; - this.nodeHierarchy.splice(originalLength, 0, {node: toAppend, button: button}); - toAppend = this.DOMHelpers.getParentObject(toAppend); - } - this.container.appendChild(fragment, this.container.firstChild); - }, - - /** - * Get a child of a node that can be displayed in the breadcrumbs - * and that is probably visible. See LOW_PRIORITY_ELEMENTS. - * - * @param aNode The parent node. - * @returns nsIDOMNode|null - */ - getInterestingFirstNode: function BC_getInterestingFirstNode(aNode) - { - let nextChild = this.DOMHelpers.getChildObject(aNode, 0); - let fallback = null; - - while (nextChild) { - if (nextChild.nodeType == aNode.ELEMENT_NODE) { - if (!(nextChild.tagName in LOW_PRIORITY_ELEMENTS)) { - return nextChild; - } - if (!fallback) { - fallback = nextChild; - } - } - nextChild = this.DOMHelpers.getNextSibling(nextChild); - } - return fallback; - }, - - - /** - * Find the "youngest" ancestor of a node which is already in the breadcrumbs. - * - * @param aNode - * @returns Index of the ancestor in the cache - */ - getCommonAncestor: function BC_getCommonAncestor(aNode) - { - let node = aNode; - while (node) { - let idx = this.indexOf(node); - if (idx > -1) { - return idx; - } else { - node = this.DOMHelpers.getParentObject(node); - } - } - return -1; - }, - - /** - * Make sure that the latest node in the breadcrumbs is not the selected node - * if the selected node still has children. - */ - ensureFirstChild: function BC_ensureFirstChild() - { - // If the last displayed node is the selected node - if (this.currentIndex == this.nodeHierarchy.length - 1) { - let node = this.nodeHierarchy[this.currentIndex].node; - let child = this.getInterestingFirstNode(node); - // If the node has a child - if (child) { - // Show this child - this.expand(child); - } - } - }, - - /** - * Ensure the selected node is visible. - */ - scroll: function BC_scroll() - { - // FIXME bug 684352: make sure its immediate neighbors are visible too. - - let scrollbox = this.container; - let element = this.nodeHierarchy[this.currentIndex].button; - - // Repeated calls to ensureElementIsVisible would interfere with each other - // and may sometimes result in incorrect scroll positions. - this.chromeWin.clearTimeout(this._ensureVisibleTimeout); - this._ensureVisibleTimeout = this.chromeWin.setTimeout(function() { - scrollbox.ensureElementIsVisible(element); - }, ENSURE_SELECTION_VISIBLE_DELAY); - }, - - updateSelectors: function BC_updateSelectors() - { - for (let i = this.nodeHierarchy.length - 1; i >= 0; i--) { - let crumb = this.nodeHierarchy[i]; - let button = crumb.button; - - while(button.hasChildNodes()) { - button.removeChild(button.firstChild); - } - button.appendChild(this.prettyPrintNodeAsXUL(crumb.node)); - button.setAttribute("tooltiptext", this.prettyPrintNodeAsText(crumb.node)); - } - }, - - /** - * Update the breadcrumbs display when a new node is selected. - */ - update: function BC_update() - { - this.inspector.hideNodeMenu(); - - let cmdDispatcher = this.chromeDoc.commandDispatcher; - this.hadFocus = (cmdDispatcher.focusedElement && - cmdDispatcher.focusedElement.parentNode == this.container); - - if (!this.selection.isConnected()) { - this.cutAfter(-1); // remove all the crumbs - return; - } - - if (!this.selection.isElementNode()) { - this.setCursor(-1); // no selection - return; - } - - let idx = this.indexOf(this.selection.node); - - // Is the node already displayed in the breadcrumbs? - if (idx > -1) { - // Yes. We select it. - this.setCursor(idx); - } else { - // No. Is the breadcrumbs display empty? - if (this.nodeHierarchy.length > 0) { - // No. We drop all the element that are not direct ancestors - // of the selection - let parent = this.DOMHelpers.getParentObject(this.selection.node); - let idx = this.getCommonAncestor(parent); - this.cutAfter(idx); - } - // we append the missing button between the end of the breadcrumbs display - // and the current node. - this.expand(this.selection.node); - - // we select the current node button - idx = this.indexOf(this.selection.node); - this.setCursor(idx); - } - // Add the first child of the very last node of the breadcrumbs if possible. - this.ensureFirstChild(); - this.updateSelectors(); - - // Make sure the selected node and its neighbours are visible. - this.scroll(); - }, -} - -XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () { - return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); -});
deleted file mode 100644 --- a/browser/devtools/inspector/highlighter.js +++ /dev/null @@ -1,798 +0,0 @@ -/* -*- 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 {Cu, Cc, Ci} = require("chrome"); - -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource:///modules/devtools/LayoutHelpers.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -let EventEmitter = require("devtools/shared/event-emitter"); - -const PSEUDO_CLASSES = [":hover", ":active", ":focus"]; - // add ":visited" and ":link" after bug 713106 is fixed - -/** - * A highlighter mechanism. - * - * The highlighter is built dynamically into the browser element. - * The caller is in charge of destroying the highlighter (ie, the highlighter - * won't be destroyed if a new tab is selected for example). - * - * API: - * - * // Constructor and destructor. - * Highlighter(aTab, aInspector) - * void destroy(); - * - * // Show and hide the highlighter - * void show(); - * void hide(); - * boolean isHidden(); - * - * // Redraw the highlighter if the visible portion of the node has changed. - * void invalidateSize(aScroll); - * - * Events: - * - * "closed" - Highlighter is closing - * "highlighting" - Highlighter is highlighting - * "locked" - The selected node has been locked - * "unlocked" - The selected ndoe has been unlocked - * - * Structure: - * <stack class="highlighter-container"> - * <box class="highlighter-outline-container"> - * <box class="highlighter-outline" locked="true/false"/> - * </box> - * <box class="highlighter-controls"> - * <box class="highlighter-nodeinfobar-container" position="top/bottom" locked="true/false"> - * <box class="highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-top"/> - * <hbox class="highlighter-nodeinfobar"> - * <toolbarbutton class="highlighter-nodeinfobar-inspectbutton highlighter-nodeinfobar-button"/> - * <hbox class="highlighter-nodeinfobar-text">tagname#id.class1.class2</hbox> - * <toolbarbutton class="highlighter-nodeinfobar-menu highlighter-nodeinfobar-button">…</toolbarbutton> - * </hbox> - * <box class="highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-bottom"/> - * </box> - * </box> - * </stack> - * - */ - - -/** - * Constructor. - * - * @param aTarget The inspection target. - * @param aInspector Inspector panel. - * @param aToolbox The toolbox holding the inspector. - */ -function Highlighter(aTarget, aInspector, aToolbox) -{ - this.target = aTarget; - this.tab = aTarget.tab; - this.toolbox = aToolbox; - this.browser = this.tab.linkedBrowser; - this.chromeDoc = this.tab.ownerDocument; - this.chromeWin = this.chromeDoc.defaultView; - this.inspector = aInspector - - EventEmitter.decorate(this); - - this._init(); -} - -exports.Highlighter = Highlighter; - -Highlighter.prototype = { - get selection() { - return this.inspector.selection; - }, - - _init: function Highlighter__init() - { - this.unlockAndFocus = this.unlockAndFocus.bind(this); - this.updateInfobar = this.updateInfobar.bind(this); - this.highlight = this.highlight.bind(this); - - let stack = this.browser.parentNode; - this.win = this.browser.contentWindow; - this._highlighting = false; - - this.highlighterContainer = this.chromeDoc.createElement("stack"); - this.highlighterContainer.className = "highlighter-container"; - - this.outline = this.chromeDoc.createElement("box"); - this.outline.className = "highlighter-outline"; - - let outlineContainer = this.chromeDoc.createElement("box"); - outlineContainer.appendChild(this.outline); - outlineContainer.className = "highlighter-outline-container"; - - // The controlsBox will host the different interactive - // elements of the highlighter (buttons, toolbars, ...). - let controlsBox = this.chromeDoc.createElement("box"); - controlsBox.className = "highlighter-controls"; - this.highlighterContainer.appendChild(outlineContainer); - this.highlighterContainer.appendChild(controlsBox); - - // Insert the highlighter right after the browser - stack.insertBefore(this.highlighterContainer, stack.childNodes[1]); - - this.buildInfobar(controlsBox); - - this.transitionDisabler = null; - this.pageEventsMuter = null; - - this.unlockAndFocus(); - - this.selection.on("new-node", this.highlight); - this.selection.on("new-node", this.updateInfobar); - this.selection.on("pseudoclass", this.updateInfobar); - this.selection.on("attribute-changed", this.updateInfobar); - - this.onToolSelected = function(event, id) { - if (id != "inspector") { - this.chromeWin.clearTimeout(this.pageEventsMuter); - this.detachMouseListeners(); - this.disabled = true; - this.hide(); - } else { - if (!this.locked) { - this.attachMouseListeners(); - } - this.disabled = false; - this.show(); - } - }.bind(this); - this.toolbox.on("select", this.onToolSelected); - - this.hidden = true; - this.highlight(); - }, - - /** - * Destroy the nodes. Remove listeners. - */ - destroy: function Highlighter_destroy() - { - this.inspectButton.removeEventListener("command", this.unlockAndFocus); - this.inspectButton = null; - - this.toolbox.off("select", this.onToolSelected); - this.toolbox = null; - - this.selection.off("new-node", this.highlight); - this.selection.off("new-node", this.updateInfobar); - this.selection.off("pseudoclass", this.updateInfobar); - this.selection.off("attribute-changed", this.updateInfobar); - - this.detachMouseListeners(); - this.detachPageListeners(); - - this.chromeWin.clearTimeout(this.transitionDisabler); - this.chromeWin.clearTimeout(this.pageEventsMuter); - this.boundCloseEventHandler = null; - this._contentRect = null; - this._highlightRect = null; - this._highlighting = false; - this.outline = null; - this.nodeInfo = null; - this.highlighterContainer.parentNode.removeChild(this.highlighterContainer); - this.highlighterContainer = null; - this.win = null - this.browser = null; - this.chromeDoc = null; - this.chromeWin = null; - this.tabbrowser = null; - - this.emit("closed"); - }, - - /** - * Show the outline, and select a node. - */ - highlight: function Highlighter_highlight() - { - if (this.selection.reason != "highlighter") { - this.lock(); - } - - let canHighlightNode = this.selection.isNode() && - this.selection.isConnected() && - this.selection.isElementNode(); - - if (canHighlightNode) { - if (this.selection.reason != "navigateaway") { - this.disabled = false; - } - this.show(); - this.updateInfobar(); - this.invalidateSize(); - if (!this._highlighting && - this.selection.reason != "highlighter") { - LayoutHelpers.scrollIntoViewIfNeeded(this.selection.node); - } - } else { - this.disabled = true; - this.hide(); - } - }, - - /** - * Update the highlighter size and position. - */ - invalidateSize: function Highlighter_invalidateSize() - { - let canHiglightNode = this.selection.isNode() && - this.selection.isConnected() && - this.selection.isElementNode(); - - if (!canHiglightNode) - return; - - let clientRect = this.selection.node.getBoundingClientRect(); - let rect = LayoutHelpers.getDirtyRect(this.selection.node); - this.highlightRectangle(rect); - - this.moveInfobar(); - - if (this._highlighting) { - this.showOutline(); - this.emit("highlighting"); - } - }, - - /** - * Show the highlighter if it has been hidden. - */ - show: function() { - if (!this.hidden || this.disabled) return; - this.showOutline(); - this.showInfobar(); - this.computeZoomFactor(); - this.attachPageListeners(); - this.invalidateSize(); - this.hidden = false; - }, - - /** - * Hide the highlighter, the outline and the infobar. - */ - hide: function() { - if (this.hidden) return; - this.hideOutline(); - this.hideInfobar(); - this.detachPageListeners(); - this.hidden = true; - }, - - /** - * Is the highlighter visible? - * - * @return boolean - */ - isHidden: function() { - return this.hidden; - }, - - /** - * Lock a node. Stops the inspection. - */ - lock: function() { - if (this.locked === true) return; - this.outline.setAttribute("locked", "true"); - this.nodeInfo.container.setAttribute("locked", "true"); - this.detachMouseListeners(); - this.locked = true; - this.emit("locked"); - }, - - /** - * Start inspecting. - * Unlock the current node (if any), and select any node being hovered. - */ - unlock: function() { - if (this.locked === false) return; - this.outline.removeAttribute("locked"); - this.nodeInfo.container.removeAttribute("locked"); - this.attachMouseListeners(); - this.locked = false; - if (this.selection.isElementNode() && - this.selection.isConnected()) { - this.showOutline(); - } - this.emit("unlocked"); - }, - - /** - * Focus the browser before unlocking. - */ - unlockAndFocus: function Highlighter_unlockAndFocus() { - if (this.locked === false) return; - this.chromeWin.focus(); - this.unlock(); - }, - - /** - * Hide the infobar - */ - hideInfobar: function Highlighter_hideInfobar() { - this.nodeInfo.container.setAttribute("force-transitions", "true"); - this.nodeInfo.container.setAttribute("hidden", "true"); - }, - - /** - * Show the infobar - */ - showInfobar: function Highlighter_showInfobar() { - this.nodeInfo.container.removeAttribute("hidden"); - this.moveInfobar(); - this.nodeInfo.container.removeAttribute("force-transitions"); - }, - - /** - * Hide the outline - */ - hideOutline: function Highlighter_hideOutline() { - this.outline.setAttribute("hidden", "true"); - }, - - /** - * Show the outline - */ - showOutline: function Highlighter_showOutline() { - if (this._highlighting) - this.outline.removeAttribute("hidden"); - }, - - /** - * Build the node Infobar. - * - * <box class="highlighter-nodeinfobar-container"> - * <box class="Highlighter-nodeinfobar-arrow-top"/> - * <hbox class="highlighter-nodeinfobar"> - * <toolbarbutton class="highlighter-nodeinfobar-button highlighter-nodeinfobar-inspectbutton"/> - * <hbox class="highlighter-nodeinfobar-text"> - * <xhtml:span class="highlighter-nodeinfobar-tagname"/> - * <xhtml:span class="highlighter-nodeinfobar-id"/> - * <xhtml:span class="highlighter-nodeinfobar-classes"/> - * <xhtml:span class="highlighter-nodeinfobar-pseudo-classes"/> - * </hbox> - * <toolbarbutton class="highlighter-nodeinfobar-button highlighter-nodeinfobar-menu"/> - * </hbox> - * <box class="Highlighter-nodeinfobar-arrow-bottom"/> - * </box> - * - * @param nsIDOMElement aParent - * The container of the infobar. - */ - buildInfobar: function Highlighter_buildInfobar(aParent) - { - let container = this.chromeDoc.createElement("box"); - container.className = "highlighter-nodeinfobar-container"; - container.setAttribute("position", "top"); - container.setAttribute("disabled", "true"); - - let nodeInfobar = this.chromeDoc.createElement("hbox"); - nodeInfobar.className = "highlighter-nodeinfobar"; - - let arrowBoxTop = this.chromeDoc.createElement("box"); - arrowBoxTop.className = "highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-top"; - - let arrowBoxBottom = this.chromeDoc.createElement("box"); - arrowBoxBottom.className = "highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-bottom"; - - let tagNameLabel = this.chromeDoc.createElementNS("http://www.w3.org/1999/xhtml", "span"); - tagNameLabel.className = "highlighter-nodeinfobar-tagname"; - - let idLabel = this.chromeDoc.createElementNS("http://www.w3.org/1999/xhtml", "span"); - idLabel.className = "highlighter-nodeinfobar-id"; - - let classesBox = this.chromeDoc.createElementNS("http://www.w3.org/1999/xhtml", "span"); - classesBox.className = "highlighter-nodeinfobar-classes"; - - let pseudoClassesBox = this.chromeDoc.createElementNS("http://www.w3.org/1999/xhtml", "span"); - pseudoClassesBox.className = "highlighter-nodeinfobar-pseudo-classes"; - - // Add some content to force a better boundingClientRect down below. - pseudoClassesBox.textContent = " "; - - // Create buttons - - this.inspectButton = this.chromeDoc.createElement("toolbarbutton"); - this.inspectButton.className = "highlighter-nodeinfobar-button highlighter-nodeinfobar-inspectbutton" - let toolbarInspectButton = this.inspector.panelDoc.getElementById("inspector-inspect-toolbutton"); - this.inspectButton.setAttribute("tooltiptext", toolbarInspectButton.getAttribute("tooltiptext")); - this.inspectButton.addEventListener("command", this.unlockAndFocus); - - let nodemenu = this.chromeDoc.createElement("toolbarbutton"); - nodemenu.setAttribute("type", "menu"); - nodemenu.className = "highlighter-nodeinfobar-button highlighter-nodeinfobar-menu" - nodemenu.setAttribute("tooltiptext", - this.strings.GetStringFromName("nodeMenu.tooltiptext")); - - nodemenu.onclick = function() { - this.inspector.showNodeMenu(nodemenu, "after_start"); - }.bind(this); - - // <hbox class="highlighter-nodeinfobar-text"/> - let texthbox = this.chromeDoc.createElement("hbox"); - texthbox.className = "highlighter-nodeinfobar-text"; - texthbox.setAttribute("align", "center"); - texthbox.setAttribute("flex", "1"); - - texthbox.addEventListener("mousedown", function(aEvent) { - // On click, show the node: - if (this.selection.isElementNode()) { - LayoutHelpers.scrollIntoViewIfNeeded(this.selection.node); - } - }.bind(this), true); - - texthbox.appendChild(tagNameLabel); - texthbox.appendChild(idLabel); - texthbox.appendChild(classesBox); - texthbox.appendChild(pseudoClassesBox); - - nodeInfobar.appendChild(this.inspectButton); - nodeInfobar.appendChild(texthbox); - nodeInfobar.appendChild(nodemenu); - - container.appendChild(arrowBoxTop); - container.appendChild(nodeInfobar); - container.appendChild(arrowBoxBottom); - - aParent.appendChild(container); - - let barHeight = container.getBoundingClientRect().height; - - this.nodeInfo = { - tagNameLabel: tagNameLabel, - idLabel: idLabel, - classesBox: classesBox, - pseudoClassesBox: pseudoClassesBox, - container: container, - barHeight: barHeight, - }; - }, - - /** - * Highlight a rectangular region. - * - * @param object aRect - * The rectangle region to highlight. - * @returns boolean - * True if the rectangle was highlighted, false otherwise. - */ - highlightRectangle: function Highlighter_highlightRectangle(aRect) - { - if (!aRect) { - this.unhighlight(); - return; - } - - let oldRect = this._contentRect; - - if (oldRect && aRect.top == oldRect.top && aRect.left == oldRect.left && - aRect.width == oldRect.width && aRect.height == oldRect.height) { - return; // same rectangle - } - - let aRectScaled = LayoutHelpers.getZoomedRect(this.win, aRect); - - if (aRectScaled.left >= 0 && aRectScaled.top >= 0 && - aRectScaled.width > 0 && aRectScaled.height > 0) { - - this.showOutline(); - - // The bottom div and the right div are flexibles (flex=1). - // We don't need to resize them. - let top = "top:" + aRectScaled.top + "px;"; - let left = "left:" + aRectScaled.left + "px;"; - let width = "width:" + aRectScaled.width + "px;"; - let height = "height:" + aRectScaled.height + "px;"; - this.outline.setAttribute("style", top + left + width + height); - - this._highlighting = true; - } else { - this.unhighlight(); - } - - this._contentRect = aRect; // save orig (non-scaled) rect - this._highlightRect = aRectScaled; // and save the scaled rect. - - return; - }, - - /** - * Clear the highlighter surface. - */ - unhighlight: function Highlighter_unhighlight() - { - this._highlighting = false; - this.hideOutline(); - }, - - /** - * Update node information (tagName#id.class) - */ - updateInfobar: function Highlighter_updateInfobar() - { - if (!this.selection.isElementNode()) { - this.nodeInfo.tagNameLabel.textContent = ""; - this.nodeInfo.idLabel.textContent = ""; - this.nodeInfo.classesBox.textContent = ""; - this.nodeInfo.pseudoClassesBox.textContent = ""; - return; - } - - let node = this.selection.node; - - // Tag name - this.nodeInfo.tagNameLabel.textContent = node.tagName; - - // ID - this.nodeInfo.idLabel.textContent = node.id ? "#" + node.id : ""; - - // Classes - let classes = this.nodeInfo.classesBox; - - classes.textContent = node.classList.length ? - "." + Array.join(node.classList, ".") : ""; - - // Pseudo-classes - let pseudos = PSEUDO_CLASSES.filter(function(pseudo) { - return DOMUtils.hasPseudoClassLock(node, pseudo); - }, this); - - let pseudoBox = this.nodeInfo.pseudoClassesBox; - pseudoBox.textContent = pseudos.join(""); - }, - - /** - * Move the Infobar to the right place in the highlighter. - */ - moveInfobar: function Highlighter_moveInfobar() - { - if (this._highlightRect) { - let winHeight = this.win.innerHeight * this.zoom; - let winWidth = this.win.innerWidth * this.zoom; - - let rect = {top: this._highlightRect.top, - left: this._highlightRect.left, - width: this._highlightRect.width, - height: this._highlightRect.height}; - - rect.top = Math.max(rect.top, 0); - rect.left = Math.max(rect.left, 0); - rect.width = Math.max(rect.width, 0); - rect.height = Math.max(rect.height, 0); - - rect.top = Math.min(rect.top, winHeight); - rect.left = Math.min(rect.left, winWidth); - - this.nodeInfo.container.removeAttribute("disabled"); - // Can the bar be above the node? - if (rect.top < this.nodeInfo.barHeight) { - // No. Can we move the toolbar under the node? - if (rect.top + rect.height + - this.nodeInfo.barHeight > winHeight) { - // No. Let's move it inside. - this.nodeInfo.container.style.top = rect.top + "px"; - this.nodeInfo.container.setAttribute("position", "overlap"); - } else { - // Yes. Let's move it under the node. - this.nodeInfo.container.style.top = rect.top + rect.height + "px"; - this.nodeInfo.container.setAttribute("position", "bottom"); - } - } else { - // Yes. Let's move it on top of the node. - this.nodeInfo.container.style.top = - rect.top - this.nodeInfo.barHeight + "px"; - this.nodeInfo.container.setAttribute("position", "top"); - } - - let barWidth = this.nodeInfo.container.getBoundingClientRect().width; - let left = rect.left + rect.width / 2 - barWidth / 2; - - // Make sure the whole infobar is visible - if (left < 0) { - left = 0; - this.nodeInfo.container.setAttribute("hide-arrow", "true"); - } else { - if (left + barWidth > winWidth) { - left = winWidth - barWidth; - this.nodeInfo.container.setAttribute("hide-arrow", "true"); - } else { - this.nodeInfo.container.removeAttribute("hide-arrow"); - } - } - this.nodeInfo.container.style.left = left + "px"; - } else { - this.nodeInfo.container.style.left = "0"; - this.nodeInfo.container.style.top = "0"; - this.nodeInfo.container.setAttribute("position", "top"); - this.nodeInfo.container.setAttribute("hide-arrow", "true"); - } - }, - - /** - * Store page zoom factor. - */ - computeZoomFactor: function Highlighter_computeZoomFactor() { - this.zoom = - this.win.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils) - .fullZoom; - }, - - ///////////////////////////////////////////////////////////////////////// - //// Event Handling - - attachMouseListeners: function Highlighter_attachMouseListeners() - { - this.browser.addEventListener("mousemove", this, true); - this.browser.addEventListener("click", this, true); - this.browser.addEventListener("dblclick", this, true); - this.browser.addEventListener("mousedown", this, true); - this.browser.addEventListener("mouseup", this, true); - }, - - detachMouseListeners: function Highlighter_detachMouseListeners() - { - this.browser.removeEventListener("mousemove", this, true); - this.browser.removeEventListener("click", this, true); - this.browser.removeEventListener("dblclick", this, true); - this.browser.removeEventListener("mousedown", this, true); - this.browser.removeEventListener("mouseup", this, true); - }, - - attachPageListeners: function Highlighter_attachPageListeners() - { - this.browser.addEventListener("resize", this, true); - this.browser.addEventListener("scroll", this, true); - this.browser.addEventListener("MozAfterPaint", this, true); - }, - - detachPageListeners: function Highlighter_detachPageListeners() - { - this.browser.removeEventListener("resize", this, true); - this.browser.removeEventListener("scroll", this, true); - this.browser.removeEventListener("MozAfterPaint", this, true); - }, - - /** - * Generic event handler. - * - * @param nsIDOMEvent aEvent - * The DOM event object. - */ - handleEvent: function Highlighter_handleEvent(aEvent) - { - switch (aEvent.type) { - case "click": - this.handleClick(aEvent); - break; - case "mousemove":