mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-05 00:25:27 +00:00
462 lines
14 KiB
JavaScript
462 lines
14 KiB
JavaScript
/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
|
|
/* 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/. */
|
|
|
|
"use strict";
|
|
|
|
const Cc = Components.classes;
|
|
const Ci = Components.interfaces;
|
|
const Cu = Components.utils;
|
|
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils",
|
|
"resource:///modules/WebConsoleUtils.jsm");
|
|
|
|
var EXPORTED_SYMBOLS = ["PropertyPanel", "PropertyTreeView"];
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
//// PropertyTreeView.
|
|
|
|
/**
|
|
* This is an implementation of the nsITreeView interface. For comments on the
|
|
* interface properties, see the documentation:
|
|
* https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsITreeView
|
|
*/
|
|
var PropertyTreeView = function() {
|
|
this._rows = [];
|
|
this._objectCache = {};
|
|
};
|
|
|
|
PropertyTreeView.prototype = {
|
|
/**
|
|
* Stores the visible rows of the tree.
|
|
* @private
|
|
*/
|
|
_rows: null,
|
|
|
|
/**
|
|
* Stores the nsITreeBoxObject for this tree.
|
|
* @private
|
|
*/
|
|
_treeBox: null,
|
|
|
|
/**
|
|
* Stores cached information about local objects being inspected.
|
|
* @private
|
|
*/
|
|
_objectCache: null,
|
|
|
|
/**
|
|
* Use this setter to update the content of the tree.
|
|
*
|
|
* @param object aData
|
|
* A meta object that holds information about the object you want to
|
|
* display in the property panel. Object properties:
|
|
* - object:
|
|
* This is the raw object you want to display. You can only provide
|
|
* this object if you want the property panel to work in sync mode.
|
|
* - remoteObject:
|
|
* An array that holds information on the remote object being
|
|
* inspected. Each element in this array describes each property in the
|
|
* remote object. See WebConsoleUtils.namesAndValuesOf() for details.
|
|
* - rootCacheId:
|
|
* The cache ID where the objects referenced in remoteObject are found.
|
|
* - panelCacheId:
|
|
* The cache ID where any object retrieved by this property panel
|
|
* instance should be stored into.
|
|
* - remoteObjectProvider:
|
|
* A function that is invoked when a new object is needed. This is
|
|
* called when the user tries to expand an inspectable property. The
|
|
* callback must take four arguments:
|
|
* - fromCacheId:
|
|
* Tells from where to retrieve the object the user picked (from
|
|
* which cache ID).
|
|
* - objectId:
|
|
* The object ID the user wants.
|
|
* - panelCacheId:
|
|
* Tells in which cache ID to store the objects referenced by
|
|
* objectId so they can be retrieved later.
|
|
* - callback:
|
|
* The callback function to be invoked when the remote object is
|
|
* received. This function takes one argument: the raw message
|
|
* received from the Web Console content script.
|
|
*/
|
|
set data(aData) {
|
|
let oldLen = this._rows.length;
|
|
|
|
this._cleanup();
|
|
|
|
if (!aData) {
|
|
return;
|
|
}
|
|
|
|
if (aData.remoteObject) {
|
|
this._rootCacheId = aData.rootCacheId;
|
|
this._panelCacheId = aData.panelCacheId;
|
|
this._remoteObjectProvider = aData.remoteObjectProvider;
|
|
this._rows = [].concat(aData.remoteObject);
|
|
this._updateRemoteObject(this._rows, 0);
|
|
}
|
|
else if (aData.object) {
|
|
this._rows = this._inspectObject(aData.object);
|
|
}
|
|
else {
|
|
throw new Error("First argument must have a .remoteObject or " +
|
|
"an .object property!");
|
|
}
|
|
|
|
if (this._treeBox) {
|
|
this._treeBox.beginUpdateBatch();
|
|
if (oldLen) {
|
|
this._treeBox.rowCountChanged(0, -oldLen);
|
|
}
|
|
this._treeBox.rowCountChanged(0, this._rows.length);
|
|
this._treeBox.endUpdateBatch();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Update a remote object so it can be used with the tree view. This method
|
|
* adds properties to each array element.
|
|
*
|
|
* @private
|
|
* @param array aObject
|
|
* The remote object you want prepared for use with the tree view.
|
|
* @param number aLevel
|
|
* The level you want to give to each property in the remote object.
|
|
*/
|
|
_updateRemoteObject: function PTV__updateRemoteObject(aObject, aLevel)
|
|
{
|
|
aObject.forEach(function(aElement) {
|
|
aElement.level = aLevel;
|
|
aElement.isOpened = false;
|
|
aElement.children = null;
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Inspect a local object.
|
|
*
|
|
* @private
|
|
* @param object aObject
|
|
* The object you want to inspect.
|
|
*/
|
|
_inspectObject: function PTV__inspectObject(aObject)
|
|
{
|
|
this._objectCache = {};
|
|
this._remoteObjectProvider = this._localObjectProvider.bind(this);
|
|
let children = WebConsoleUtils.namesAndValuesOf(aObject, this._objectCache);
|
|
this._updateRemoteObject(children, 0);
|
|
return children;
|
|
},
|
|
|
|
/**
|
|
* An object provider for when the user inspects local objects (not remote
|
|
* ones).
|
|
*
|
|
* @private
|
|
* @param string aFromCacheId
|
|
* The cache ID from where to retrieve the desired object.
|
|
* @param string aObjectId
|
|
* The ID of the object you want.
|
|
* @param string aDestCacheId
|
|
* The ID of the cache where to store any objects referenced by the
|
|
* desired object.
|
|
* @param function aCallback
|
|
* The function you want to receive the object.
|
|
*/
|
|
_localObjectProvider:
|
|
function PTV__localObjectProvider(aFromCacheId, aObjectId, aDestCacheId,
|
|
aCallback)
|
|
{
|
|
let object = WebConsoleUtils.namesAndValuesOf(this._objectCache[aObjectId],
|
|
this._objectCache);
|
|
aCallback({cacheId: aFromCacheId,
|
|
objectId: aObjectId,
|
|
object: object,
|
|
childrenCacheId: aDestCacheId || aFromCacheId,
|
|
});
|
|
},
|
|
|
|
/** nsITreeView interface implementation **/
|
|
|
|
selection: null,
|
|
|
|
get rowCount() { return this._rows.length; },
|
|
setTree: function(treeBox) { this._treeBox = treeBox; },
|
|
getCellText: function(idx, column) {
|
|
let row = this._rows[idx];
|
|
return row.name + ": " + row.value;
|
|
},
|
|
getLevel: function(idx) {
|
|
return this._rows[idx].level;
|
|
},
|
|
isContainer: function(idx) {
|
|
return !!this._rows[idx].inspectable;
|
|
},
|
|
isContainerOpen: function(idx) {
|
|
return this._rows[idx].isOpened;
|
|
},
|
|
isContainerEmpty: function(idx) { return false; },
|
|
isSeparator: function(idx) { return false; },
|
|
isSorted: function() { return false; },
|
|
isEditable: function(idx, column) { return false; },
|
|
isSelectable: function(row, col) { return true; },
|
|
|
|
getParentIndex: function(idx)
|
|
{
|
|
if (this.getLevel(idx) == 0) {
|
|
return -1;
|
|
}
|
|
for (var t = idx - 1; t >= 0; t--) {
|
|
if (this.isContainer(t)) {
|
|
return t;
|
|
}
|
|
}
|
|
return -1;
|
|
},
|
|
|
|
hasNextSibling: function(idx, after)
|
|
{
|
|
var thisLevel = this.getLevel(idx);
|
|
return this._rows.slice(after + 1).some(function (r) r.level == thisLevel);
|
|
},
|
|
|
|
toggleOpenState: function(idx)
|
|
{
|
|
let item = this._rows[idx];
|
|
if (!item.inspectable) {
|
|
return;
|
|
}
|
|
|
|
if (item.isOpened) {
|
|
this._treeBox.beginUpdateBatch();
|
|
item.isOpened = false;
|
|
|
|
var thisLevel = item.level;
|
|
var t = idx + 1, deleteCount = 0;
|
|
while (t < this._rows.length && this.getLevel(t++) > thisLevel) {
|
|
deleteCount++;
|
|
}
|
|
|
|
if (deleteCount) {
|
|
this._rows.splice(idx + 1, deleteCount);
|
|
this._treeBox.rowCountChanged(idx + 1, -deleteCount);
|
|
}
|
|
this._treeBox.invalidateRow(idx);
|
|
this._treeBox.endUpdateBatch();
|
|
}
|
|
else {
|
|
let levelUpdate = true;
|
|
let callback = function _onRemoteResponse(aResponse) {
|
|
this._treeBox.beginUpdateBatch();
|
|
item.isOpened = true;
|
|
|
|
if (levelUpdate) {
|
|
this._updateRemoteObject(aResponse.object, item.level + 1);
|
|
item.children = aResponse.object;
|
|
}
|
|
|
|
this._rows.splice.apply(this._rows, [idx + 1, 0].concat(item.children));
|
|
|
|
this._treeBox.rowCountChanged(idx + 1, item.children.length);
|
|
this._treeBox.invalidateRow(idx);
|
|
this._treeBox.endUpdateBatch();
|
|
}.bind(this);
|
|
|
|
if (!item.children) {
|
|
let fromCacheId = item.level > 0 ? this._panelCacheId :
|
|
this._rootCacheId;
|
|
this._remoteObjectProvider(fromCacheId, item.objectId,
|
|
this._panelCacheId, callback);
|
|
}
|
|
else {
|
|
levelUpdate = false;
|
|
callback({object: item.children});
|
|
}
|
|
}
|
|
},
|
|
|
|
getImageSrc: function(idx, column) { },
|
|
getProgressMode : function(idx,column) { },
|
|
getCellValue: function(idx, column) { },
|
|
cycleHeader: function(col, elem) { },
|
|
selectionChanged: function() { },
|
|
cycleCell: function(idx, column) { },
|
|
performAction: function(action) { },
|
|
performActionOnCell: function(action, index, column) { },
|
|
performActionOnRow: function(action, row) { },
|
|
getRowProperties: function(idx, column, prop) { },
|
|
getCellProperties: function(idx, column, prop) { },
|
|
getColumnProperties: function(column, element, prop) { },
|
|
|
|
setCellValue: function(row, col, value) { },
|
|
setCellText: function(row, col, value) { },
|
|
drop: function(index, orientation, dataTransfer) { },
|
|
canDrop: function(index, orientation, dataTransfer) { return false; },
|
|
|
|
_cleanup: function PTV__cleanup()
|
|
{
|
|
if (this._rows.length) {
|
|
// Reset the existing _rows children to the initial state.
|
|
this._updateRemoteObject(this._rows, 0);
|
|
this._rows = [];
|
|
}
|
|
|
|
delete this._objectCache;
|
|
delete this._rootCacheId;
|
|
delete this._panelCacheId;
|
|
delete this._remoteObjectProvider;
|
|
},
|
|
};
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
//// Helper for creating the panel.
|
|
|
|
/**
|
|
* Creates a DOMNode and sets all the attributes of aAttributes on the created
|
|
* element.
|
|
*
|
|
* @param nsIDOMDocument aDocument
|
|
* Document to create the new DOMNode.
|
|
* @param string aTag
|
|
* Name of the tag for the DOMNode.
|
|
* @param object aAttributes
|
|
* Attributes set on the created DOMNode.
|
|
* @returns nsIDOMNode
|
|
*/
|
|
function createElement(aDocument, aTag, aAttributes)
|
|
{
|
|
let node = aDocument.createElement(aTag);
|
|
for (var attr in aAttributes) {
|
|
node.setAttribute(attr, aAttributes[attr]);
|
|
}
|
|
return node;
|
|
}
|
|
|
|
/**
|
|
* Creates a new DOMNode and appends it to aParent.
|
|
*
|
|
* @param nsIDOMDocument aDocument
|
|
* Document to create the new DOMNode.
|
|
* @param nsIDOMNode aParent
|
|
* A parent node to append the created element.
|
|
* @param string aTag
|
|
* Name of the tag for the DOMNode.
|
|
* @param object aAttributes
|
|
* Attributes set on the created DOMNode.
|
|
* @returns nsIDOMNode
|
|
*/
|
|
function appendChild(aDocument, aParent, aTag, aAttributes)
|
|
{
|
|
let node = createElement(aDocument, aTag, aAttributes);
|
|
aParent.appendChild(node);
|
|
return node;
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
//// PropertyPanel
|
|
|
|
/**
|
|
* Creates a new PropertyPanel.
|
|
*
|
|
* @see PropertyTreeView
|
|
* @param nsIDOMNode aParent
|
|
* Parent node to append the created panel to.
|
|
* @param string aTitle
|
|
* Title for the panel.
|
|
* @param string aObject
|
|
* Object to display in the tree. For details about this object please
|
|
* see the PropertyTreeView constructor in this file.
|
|
* @param array of objects aButtons
|
|
* Array with buttons to display at the bottom of the panel.
|
|
*/
|
|
function PropertyPanel(aParent, aTitle, aObject, aButtons)
|
|
{
|
|
let document = aParent.ownerDocument;
|
|
|
|
// Create the underlying panel
|
|
this.panel = createElement(document, "panel", {
|
|
label: aTitle,
|
|
titlebar: "normal",
|
|
noautofocus: "true",
|
|
noautohide: "true",
|
|
close: "true",
|
|
});
|
|
|
|
// Create the tree.
|
|
let tree = this.tree = createElement(document, "tree", {
|
|
flex: 1,
|
|
hidecolumnpicker: "true"
|
|
});
|
|
|
|
let treecols = document.createElement("treecols");
|
|
appendChild(document, treecols, "treecol", {
|
|
primary: "true",
|
|
flex: 1,
|
|
hideheader: "true",
|
|
ignoreincolumnpicker: "true"
|
|
});
|
|
tree.appendChild(treecols);
|
|
|
|
tree.appendChild(document.createElement("treechildren"));
|
|
this.panel.appendChild(tree);
|
|
|
|
// Create the footer.
|
|
let footer = createElement(document, "hbox", { align: "end" });
|
|
appendChild(document, footer, "spacer", { flex: 1 });
|
|
|
|
// The footer can have butttons.
|
|
let self = this;
|
|
if (aButtons) {
|
|
aButtons.forEach(function(button) {
|
|
let buttonNode = appendChild(document, footer, "button", {
|
|
label: button.label,
|
|
accesskey: button.accesskey || "",
|
|
class: button.class || "",
|
|
});
|
|
buttonNode.addEventListener("command", button.oncommand, false);
|
|
});
|
|
}
|
|
|
|
appendChild(document, footer, "resizer", { dir: "bottomend" });
|
|
this.panel.appendChild(footer);
|
|
|
|
aParent.appendChild(this.panel);
|
|
|
|
// Create the treeView object.
|
|
this.treeView = new PropertyTreeView();
|
|
this.treeView.data = aObject;
|
|
|
|
// Set the treeView object on the tree view. This has to be done *after* the
|
|
// panel is shown. This is because the tree binding must be attached first.
|
|
this.panel.addEventListener("popupshown", function onPopupShow()
|
|
{
|
|
self.panel.removeEventListener("popupshown", onPopupShow, false);
|
|
self.tree.view = self.treeView;
|
|
}, false);
|
|
|
|
this.panel.addEventListener("popuphidden", function onPopupHide()
|
|
{
|
|
self.panel.removeEventListener("popuphidden", onPopupHide, false);
|
|
self.destroy();
|
|
}, false);
|
|
}
|
|
|
|
/**
|
|
* Destroy the PropertyPanel. This closes the panel and removes it from the
|
|
* browser DOM.
|
|
*/
|
|
PropertyPanel.prototype.destroy = function PP_destroy()
|
|
{
|
|
this.treeView.data = null;
|
|
this.panel.parentNode.removeChild(this.panel);
|
|
this.treeView = null;
|
|
this.panel = null;
|
|
this.tree = null;
|
|
}
|
|
|