Bug 912891 - [app manager] Implement a CUSTOM host. r=harth

This commit is contained in:
Paul Rouget 2013-11-15 21:47:00 -05:00
parent ed0f7fc2bc
commit ca7e3fb1a1
6 changed files with 190 additions and 18 deletions

View File

@ -206,11 +206,13 @@ DevTools.prototype = {
* The id of the tool to show
* @param {Toolbox.HostType} hostType
* The type of host (bottom, window, side)
* @param {object} hostOptions
* Options for host specifically
*
* @return {Toolbox} toolbox
* The toolbox that was opened
*/
showToolbox: function(target, toolId, hostType) {
showToolbox: function(target, toolId, hostType, hostOptions) {
let deferred = promise.defer();
let toolbox = this._toolboxes.get(target);
@ -233,7 +235,7 @@ DevTools.prototype = {
}
else {
// No toolbox for target, create one
toolbox = new devtools.Toolbox(target, toolId, hostType);
toolbox = new devtools.Toolbox(target, toolId, hostType, hostOptions);
this._toolboxes.set(target, toolbox);

View File

@ -23,3 +23,4 @@ skip-if = os == "win"
[browser_toolbox_window_shortcuts.js]
[browser_toolbox_window_title_changes.js]
[browser_toolbox_zoom.js]
[browser_toolbox_custom_host.js]

View File

@ -0,0 +1,60 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
function test() {
Cu.import("resource://gre/modules/Services.jsm");
let temp = {}
Cu.import("resource:///modules/devtools/gDevTools.jsm", temp);
let DevTools = temp.DevTools;
Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm", temp);
let LayoutHelpers = temp.LayoutHelpers;
Cu.import("resource://gre/modules/devtools/Loader.jsm", temp);
let devtools = temp.devtools;
let Toolbox = devtools.Toolbox;
let toolbox, iframe, target, tab;
waitForExplicitFinish();
gBrowser.selectedTab = gBrowser.addTab();
target = TargetFactory.forTab(gBrowser.selectedTab);
window.addEventListener("message", onMessage);
iframe = document.createElement("iframe");
document.documentElement.appendChild(iframe);
gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
let options = {customIframe: iframe};
gDevTools.showToolbox(target, null, Toolbox.HostType.CUSTOM, options)
.then(testCustomHost, console.error)
.then(null, console.error);
}, true);
content.location = "data:text/html,test custom host";
function onMessage(event) {
info("onMessage: " + event.data);
let json = JSON.parse(event.data);
if (json.name == "toolbox-close") {
ok("Got the `toolbox-close` message");
cleanup();
}
}
function testCustomHost(toolbox) {
is(toolbox.doc.defaultView.top, window, "Toolbox is included in browser.xul");
is(toolbox.doc, iframe.contentDocument, "Toolbox is in the custom iframe");
executeSoon(() => gBrowser.removeCurrentTab());
}
function cleanup() {
window.removeEventListener("message", onMessage);
iframe.remove();
finish();
}
}

View File

@ -10,6 +10,7 @@ let promise = require("sdk/core/promise");
let EventEmitter = require("devtools/shared/event-emitter");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource:///modules/devtools/DOMHelpers.jsm");
/**
* A toolbox host represents an object that contains a toolbox (e.g. the
@ -23,7 +24,8 @@ Cu.import("resource://gre/modules/Services.jsm");
exports.Hosts = {
"bottom": BottomHost,
"side": SidebarHost,
"window": WindowHost
"window": WindowHost,
"custom": CustomHost
}
/**
@ -61,18 +63,18 @@ BottomHost.prototype = {
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");
let domHelper = new DOMHelpers(this.frame.contentWindow);
domHelper.onceDOMReady(frameLoad);
focusTab(this.hostTab);
return deferred.promise;
@ -272,6 +274,59 @@ WindowHost.prototype = {
}
}
/**
* Host object for the toolbox in its own tab
*/
function CustomHost(hostTab, options) {
this.frame = options.customIframe;
this.uid = options.uid;
EventEmitter.decorate(this);
}
CustomHost.prototype = {
type: "custom",
_sendMessageToTopWindow: function CH__sendMessageToTopWindow(msg) {
// It's up to the custom frame owner (parent window) to honor
// "close" or "raise" instructions.
let topWindow = this.frame.ownerDocument.defaultView;
let json = {name:"toolbox-" + msg, uid: this.uid}
topWindow.postMessage(JSON.stringify(json), "*");
},
/**
* Create a new xul window to contain the toolbox.
*/
create: function CH_create() {
return promise.resolve(this.frame);
},
/**
* Raise the host.
*/
raise: function CH_raise() {
this._sendMessageToTopWindow("raise");
},
/**
* Set the toolbox title.
*/
setTitle: function CH_setTitle(title) {
// Not supported
},
/**
* Destroy the window.
*/
destroy: function WH_destroy() {
if (!this._destroyed) {
this._destroyed = true;
this._sendMessageToTopWindow("close");
}
return promise.resolve(null);
}
}
/**
* Switch to the given tab in a browser and focus the browser window
*/

View File

@ -19,6 +19,7 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource:///modules/devtools/gDevTools.jsm");
Cu.import("resource:///modules/devtools/scratchpad-manager.jsm");
Cu.import("resource:///modules/devtools/DOMHelpers.jsm");
loader.lazyGetter(this, "Hosts", () => require("devtools/framework/toolbox-hosts").Hosts);
@ -55,8 +56,10 @@ loader.lazyGetter(this, "Requisition", () => {
* Tool to select initially
* @param {Toolbox.HostType} hostType
* Type of host that will host the toolbox (e.g. sidebar, window)
* @param {object} hostOptions
* Options for host specifically
*/
function Toolbox(target, selectedTool, hostType) {
function Toolbox(target, selectedTool, hostType, hostOptions) {
this._target = target;
this._toolPanels = new Map();
this._telemetry = new Telemetry();
@ -79,7 +82,7 @@ function Toolbox(target, selectedTool, hostType) {
}
this._defaultToolId = selectedTool;
this._host = this._createHost(hostType);
this._host = this._createHost(hostType, hostOptions);
EventEmitter.decorate(this);
@ -99,7 +102,8 @@ exports.Toolbox = Toolbox;
Toolbox.HostType = {
BOTTOM: "bottom",
SIDE: "side",
WINDOW: "window"
WINDOW: "window",
CUSTOM: "custom"
};
Toolbox.prototype = {
@ -187,8 +191,6 @@ Toolbox.prototype = {
let deferred = promise.defer();
let domReady = () => {
iframe.removeEventListener("DOMContentLoaded", domReady, true);
this.isReady = true;
let closeButton = this.doc.getElementById("toolbox-close");
@ -211,9 +213,11 @@ Toolbox.prototype = {
});
};
iframe.addEventListener("DOMContentLoaded", domReady, true);
iframe.setAttribute("src", this._URL);
let domHelper = new DOMHelpers(iframe.contentWindow);
domHelper.onceDOMReady(domReady);
return deferred.promise;
});
},
@ -387,6 +391,7 @@ Toolbox.prototype = {
for (let type in Toolbox.HostType) {
let position = Toolbox.HostType[type];
if (position == this.hostType ||
position == Toolbox.HostType.CUSTOM ||
(!sideEnabled && position == Toolbox.HostType.SIDE)) {
continue;
}
@ -555,8 +560,6 @@ Toolbox.prototype = {
vbox.appendChild(iframe);
let onLoad = () => {
iframe.removeEventListener("DOMContentLoaded", onLoad, true);
let built = definition.build(iframe.contentWindow, this);
promise.resolve(built).then((panel) => {
this._toolPanels.set(id, panel);
@ -566,8 +569,25 @@ Toolbox.prototype = {
});
};
iframe.addEventListener("DOMContentLoaded", onLoad, true);
iframe.setAttribute("src", definition.url);
// Depending on the host, iframe.contentWindow is not always
// defined at this moment. If it is not defined, we use an
// event listener on the iframe DOM node. If it's defined,
// we use the chromeEventHandler. We can't use a listener
// on the DOM node every time because this won't work
// if the (xul chrome) iframe is loaded in a content docshell.
if (iframe.contentWindow) {
let domHelper = new DOMHelpers(iframe.contentWindow);
domHelper.onceDOMReady(onLoad);
} else {
let callback = () => {
iframe.removeEventListener("DOMContentLoaded", callback);
onLoad();
}
iframe.addEventListener("DOMContentLoaded", callback);
}
return deferred.promise;
},
@ -732,13 +752,13 @@ Toolbox.prototype = {
* @return {Host} host
* The created host object
*/
_createHost: function(hostType) {
_createHost: function(hostType, options) {
if (!Hosts[hostType]) {
throw new Error("Unknown hostType: " + hostType);
}
// clean up the toolbox if its window is closed
let newHost = new Hosts[hostType](this.target.tab);
let newHost = new Hosts[hostType](this.target.tab, options);
newHost.on("window-closed", this.destroy);
return newHost;
},
@ -766,7 +786,9 @@ Toolbox.prototype = {
this._host = newHost;
Services.prefs.setCharPref(this._prefs.LAST_HOST, this._host.type);
if (this.hostType != Toolbox.HostType.CUSTOM) {
Services.prefs.setCharPref(this._prefs.LAST_HOST, this._host.type);
}
this._buildDockButtons();
this._addKeysToWindow();

View File

@ -2,6 +2,10 @@
* 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 Ci = Components.interfaces;
const Cu = Components.utils;
Cu.import("resource://gre/modules/Services.jsm");
this.EXPORTED_SYMBOLS = ["DOMHelpers"];
/**
@ -13,6 +17,9 @@ this.EXPORTED_SYMBOLS = ["DOMHelpers"];
* The content window, owning the document to traverse.
*/
this.DOMHelpers = function DOMHelpers(aWindow) {
if (!aWindow) {
throw new Error("window can't be null or undefined");
}
this.window = aWindow;
};
@ -120,5 +127,30 @@ DOMHelpers.prototype = {
{
delete this.window;
delete this.treeWalker;
},
/**
* A simple way to be notified (once) when a window becomes
* interactive (DOMContentLoaded).
*
* It is based on the chromeEventHandler. This is useful when
* chrome iframes are loaded in content docshells (in Firefox
* tabs for example).
*/
onceDOMReady: function Helpers_onLocationChange(callback) {
let window = this.window;
let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell);
let onReady = function(event) {
if (event.target == window.document) {
docShell.chromeEventHandler.removeEventListener("DOMContentLoaded", onReady, false);
// If in `callback` the URL of the window is changed and a listener to DOMContentLoaded
// is attached, the event we just received will be also be caught by the new listener.
// We want to avoid that so we execute the callback in the next queue.
Services.tm.mainThread.dispatch(callback, 0);
}
}
docShell.chromeEventHandler.addEventListener("DOMContentLoaded", onReady, false);
}
};