Revision control
Copy as Markdown
Other Tools
/* 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
const CONFIG_WINDOWTYPE = "irc:chatzilla:config";
/* Now we create and set up some required items from other Chatzilla JS files
* that we really have no reason to load, but the ones we do need won't work
* without these...
*/
var ASSERT = function (cond, msg) {
if (!cond) {
Services.prompt.alert(window, MSG_ALERT, msg);
}
return cond;
};
var client;
function CIRCNetwork() {}
function CIRCServer() {}
function CIRCChannel() {}
function CIRCChanUser() {}
function CIRCUser() {}
function CIRCDCC() {}
function CIRCDCCUser() {}
function CIRCDCCChat() {}
function CIRCDCCFileTransfer() {}
function CIRCSTS() {}
function getObjectDetails(obj) {
var rv = {};
rv.sourceObject = obj;
rv.TYPE = obj.TYPE;
rv.parent = "parent" in obj ? obj.parent : null;
rv.user = null;
rv.channel = null;
rv.server = null;
rv.network = null;
switch (obj.TYPE) {
case "PrefNetwork":
rv.network = obj;
if ("primServ" in rv.network) {
rv.server = rv.network.primServ;
} else {
rv.server = null;
}
break;
case "PrefChannel":
rv.channel = obj;
rv.server = rv.channel.parent;
rv.network = rv.server.parent;
break;
case "PrefUser":
rv.user = obj;
rv.server = rv.user.parent;
rv.network = rv.server.parent;
break;
}
return rv;
}
/* Global object for the prefs. The 'root' of all the objects to do with the
* prefs.
*/
function PrefGlobal() {
this.networks = {};
this.commandManager = {};
this.commandManager.defineCommand = function () {};
this.commandManager.removeCommand = function () {};
this.entities = {};
this.hostCompat = {};
}
PrefGlobal.prototype.TYPE = "PrefGlobal";
/* Represents a single network in the hierarchy.
*
* |force| - If true, sets a pref on this object. This makes sure the object
* is "known" next time we load up (since we look for any prefs).
*
* |show| - If true, the object still exists even if the magic pref is not set.
* Thus, allows an object to exist without any prefs set.
*/
function PrefNetwork(parent, name, force, show) {
if (":" + name in parent.networks) {
return parent.networks[":" + name];
}
this.parent = parent;
this.unicodeName = name;
this.viewName = name;
this.canonicalName = name;
this.collectionKey = ":" + name;
this.encodedName = name;
this.prettyName = getMsg(MSG_PREFS_FMT_DISPLAY_NETWORK, this.unicodeName);
this.servers = {};
this.primServ = new PrefServer(this, "dummy server");
this.channels = this.primServ.channels;
this.users = this.primServ.users;
this.prefManager = getNetworkPrefManager(this);
this.prefs = this.prefManager.prefs;
this.prefManager.onPrefChanged = function () {};
if (force) {
this.prefs.hasPrefs = true;
}
if (this.prefs.hasPrefs || show) {
this.parent.networks[this.collectionKey] = this;
}
return this;
}
PrefNetwork.prototype.TYPE = "PrefNetwork";
/* Cleans up the mess. */
PrefNetwork.prototype.clear = function () {
this.prefs.hasPrefs = false;
delete this.parent.networks[this.collectionKey];
};
/* A middle-management object.
*
* Exists only to satisfy the IRC library pref functions that expect this
* particular hierarchy.
*/
function PrefServer(parent, name) {
this.parent = parent;
this.unicodeName = name;
this.viewName = name;
this.canonicalName = name;
this.collectionKey = ":" + name;
this.encodedName = name;
this.prettyName = this.unicodeName; // Not used, thus not localised.
this.channels = {};
this.users = {};
this.parent.servers[this.collectionKey] = this;
return this;
}
PrefServer.prototype.TYPE = "PrefServer";
/* Represents a single channel in the hierarchy.
*
* |force| and |show| the same as PrefNetwork.
*/
function PrefChannel(parent, name, force, show) {
if (":" + name in parent.channels) {
return parent.channels[":" + name];
}
this.parent = parent;
this.unicodeName = name;
this.viewName = name;
this.canonicalName = name;
this.collectionKey = ":" + name;
this.encodedName = name;
this.prettyName = getMsg(MSG_PREFS_FMT_DISPLAY_CHANNEL, [
this.parent.parent.unicodeName,
this.unicodeName,
]);
this.prefManager = getChannelPrefManager(this);
this.prefs = this.prefManager.prefs;
this.prefManager.onPrefChanged = function () {};
if (force) {
this.prefs.hasPrefs = true;
}
if (this.prefs.hasPrefs || show) {
this.parent.channels[this.collectionKey] = this;
}
return this;
}
PrefChannel.prototype.TYPE = "PrefChannel";
/* Cleans up the mess. */
PrefChannel.prototype.clear = function () {
this.prefs.hasPrefs = false;
delete this.parent.channels[this.collectionKey];
};
/* Represents a single user in the hierarchy.
*
* |force| and |show| the same as PrefNetwork.
*/
function PrefUser(parent, name, force, show) {
if (":" + name in parent.users) {
return parent.users[":" + name];
}
this.parent = parent;
this.unicodeName = name;
this.viewName = name;
this.canonicalName = name;
this.collectionKey = ":" + name;
this.encodedName = name;
this.prettyName = getMsg(MSG_PREFS_FMT_DISPLAY_USER, [
this.parent.parent.unicodeName,
this.unicodeName,
]);
this.prefManager = getUserPrefManager(this);
this.prefs = this.prefManager.prefs;
this.prefManager.onPrefChanged = function () {};
if (force) {
this.prefs.hasPrefs = true;
}
if (this.prefs.hasPrefs || show) {
this.parent.users[this.collectionKey] = this;
}
return this;
}
PrefUser.prototype.TYPE = "PrefUser";
/* Cleans up the mess. */
PrefUser.prototype.clear = function () {
this.prefs.hasPrefs = false;
delete this.parent.users[this.collectionKey];
};
// Stores a list of |PrefObject|s.
function PrefObjectList() {
this.objects = [];
return this;
}
// Add an object, and init it's private data.
PrefObjectList.prototype.addObject = function (pObject) {
this.objects.push(pObject);
return (pObject.privateData = new ObjectPrivateData(
pObject,
this.objects.length - 1
));
};
/* Removes an object, without changing the index. */
PrefObjectList.prototype.deleteObject = function (index) {
this.objects[index].privateData.clear();
this.objects[index].clear();
this.objects[index] = { privateData: null };
};
// Get a specific object.
PrefObjectList.prototype.getObject = function (index) {
return this.objects[index].privateData;
};
// Gets the private data for an object.
PrefObjectList.getPrivateData = function (object) {
return object.privateData;
};
// Stores the pref object's private data.
function ObjectPrivateData(parent, index) {
this.parent = parent; // Real pref object.
this.prefs = {};
this.groups = {};
this.arrayIndex = index;
this.deckIndex = -1;
this.dataLoaded = false;
var treeObj = document.getElementById("pref-tree-object");
this.tree = document.getElementById("pref-tree");
this.treeContainer = document.createElement("treeitem");
this.treeNode = document.createElement("treerow");
this.treeCell = document.createElement("treecell");
this.treeContainer.setAttribute("prefobjectindex", this.arrayIndex);
this.treeCell.setAttribute("label", this.parent.unicodeName);
switch (this.parent.TYPE) {
case "PrefChannel":
case "PrefUser":
var p = this.parent.parent.parent; // Network object.
var pData = PrefObjectList.getPrivateData(p);
if (!("treeChildren" in pData) || !pData.treeChildren) {
pData.treeChildren = document.createElement("treechildren");
pData.treeContainer.appendChild(pData.treeChildren);
treeObj.view.toggleOpenState(treeObj.view.rowCount - 1);
}
pData.treeContainer.setAttribute("container", "true");
pData.treeChildren.appendChild(this.treeContainer);
break;
default:
this.tree.appendChild(this.treeContainer);
break;
}
this.treeContainer.appendChild(this.treeNode);
this.treeNode.appendChild(this.treeCell);
return this;
}
// Creates all the XUL elements needed to show this pref object.
ObjectPrivateData.prototype.loadXUL = function (tabOrder) {
var t = this;
/* Function that sorts the preferences by their label, else they look
* fairly random in order.
*
* Sort keys: not grouped, sub-group name, boolean, pref label.
*/
function sortByLabel(a, b) {
if (t.prefs[a].subGroup || t.prefs[b].subGroup) {
// Non-grouped go first.
if (!t.prefs[a].subGroup) {
return -1;
}
if (!t.prefs[b].subGroup) {
return 1;
}
// Sub-group names.
if (t.prefs[a].subGroup < t.prefs[b].subGroup) {
return -1;
}
if (t.prefs[a].subGroup > t.prefs[b].subGroup) {
return 1;
}
}
// Booleans go first.
if (t.prefs[a].type == "boolean" && t.prefs[b].type != "boolean") {
return -1;
}
if (t.prefs[a].type != "boolean" && t.prefs[b].type == "boolean") {
return 1;
}
// ...then label.
if (t.prefs[a].label < t.prefs[b].label) {
return -1;
}
if (t.prefs[a].label > t.prefs[b].label) {
return 1;
}
return 0;
}
if (this.deckIndex >= 0) {
return;
}
this.deck = document.getElementById("pref-object-deck");
this.tabbox = document.createElement("tabbox");
this.tabs = document.createElement("tabs");
this.tabPanels = document.createElement("tabpanels");
this.tabbox.setAttribute("flex", 1);
this.tabPanels.setAttribute("flex", 1);
this.tabbox.appendChild(this.tabs);
this.tabbox.appendChild(this.tabPanels);
this.deck.appendChild(this.tabbox);
this.deckIndex = this.deck.childNodes.length - 1;
this.loadData();
var prefList = Object.keys(this.prefs).sort(sortByLabel);
for (var i = 0; i < tabOrder.length; i++) {
var pto = tabOrder[i];
var needTab = pto.fixed;
if (!needTab) {
// Not a "always visible" tab, check we need it.
for (var j = 0; j < prefList.length; j++) {
if (this.prefs[prefList[j]].mainGroup == pto.name) {
needTab = true;
break;
}
}
}
if (needTab) {
this.addGroup(pto.name);
}
}
for (i = 0; i < prefList.length; i++) {
this.prefs[prefList[i]].loadXUL();
}
if (this.tabs.childNodes.length > 0) {
this.tabbox.selectedIndex = 0;
}
};
// Loads all the prefs.
ObjectPrivateData.prototype.loadData = function () {
if (this.dataLoaded) {
return;
}
this.dataLoaded = true;
// Now get the list of pref names, and add them...
var prefList = this.parent.prefManager.prefNames;
for (var i in prefList) {
this.addPref(prefList[i]);
}
};
// Clears up all the XUL objects and data.
ObjectPrivateData.prototype.clear = function () {
//dd("Removing prefs for " + this.parent.displayName + " {");
if (!this.dataLoaded) {
this.loadData();
}
for (var i in this.prefs) {
this.prefs[i].clear();
}
//dd("}");
if (this.deckIndex >= 0) {
this.tabbox.remove();
this.treeContainer.removeAttribute("container");
this.treeContainer.remove();
}
};
// Resets all the prefs to their original values.
ObjectPrivateData.prototype.reset = function () {
for (var i in this.prefs) {
if (this.prefs[i].type != "hidden") {
this.prefs[i].reset();
}
}
};
// Adds a pref to the internal data structures.
ObjectPrivateData.prototype.addPref = function (name) {
return (this.prefs[name] = new PrefData(this, name));
};
// Adds a group to a pref object's data.
ObjectPrivateData.prototype.addGroup = function (name) {
// Special group for prefs we don't want shown (nothing sinister here).
if (name == "hidden") {
return null;
}
if (!(name in this.groups)) {
this.groups[name] = new PrefMainGroup(this, name);
}
return this.groups[name];
};
// Represents a single pref on a single object within the pref window.
function PrefData(parent, name) {
// We want to keep all this "worked out" info, so make a hash of all
// the prefs on the pwData [Pref Window Data] property of the object.
// First, lets find out what kind of pref we've got:
this.parent = parent; // Private data for pref object.
this.name = name;
this.manager = this.parent.parent.prefManager; // PrefManager.
this.record = this.manager.prefRecords[name]; // PrefRecord.
this.def = this.record.defaultValue; // Default value.
this.type = typeof this.def; // Pref type.
this.val = this.manager.prefs[name]; // Current value.
this.startVal = this.val; // Start value.
this.label = this.record.label; // Display name.
this.help = this.record.help; // Help text.
this.group = this.record.group; // Group identifier.
this.labelFor = "none"; // Auto-grouped label.
// Handle defered prefs (call defer function, and use resulting
// value/type instead).
if (this.type == "function") {
this.def = this.def(this.name);
}
this.type = typeof this.def;
// And those arrays... this just makes our life easier later by having
// a particular name for array prefs.
if (isinstance(this.def, Array)) {
this.type = "array";
}
if (this.group == "hidden") {
this.type = "hidden";
}
// Convert "a.b" into sub-properties...
var m = this.group.match(/^([^.]*)(\.(.*))?$/);
ASSERT(m, "Failed group match!");
this.mainGroup = m[1];
this.subGroup = m[3];
return this;
}
/* Creates all the XUL elements to display this one pref. */
PrefData.prototype.loadXUL = function () {
if (this.type == "hidden") {
return;
}
// Create the base box for the pref.
this.box = document.createElement("box");
this.box.orient = "horizontal";
this.box.setAttribute("align", "center");
switch (this.type) {
case "string":
label = document.createElement("label");
label.setAttribute("value", this.label);
label.width = 100;
label.flex = 1;
this.box.appendChild(label);
this.edit = document.createElement("textbox");
// We choose the size based on the length of the default.
if (this.def.length < 8) {
this.edit.setAttribute("size", "10");
} else if (this.def.length < 20) {
this.edit.setAttribute("size", "25");
} else {
this.edit.flex = 1;
}
var editCont = document.createElement("hbox");
editCont.flex = 1000;
editCont.appendChild(this.edit);
this.box.appendChild(editCont);
// But if it's a file/URL...
if (this.def.match(/^([a-z]+:\/|[a-z]:\\)/i)) {
// ...we make it as big as possible.
this.edit.removeAttribute("size");
this.edit.flex = 1;
if (
!this.name.match(/path$/i) &&
(this.def.match(/^(file|chrome):\//i) ||
this.name.match(/filename$/i))
) {
// So long as the pref name doesn't end in "path", and
// it's chrome:, file: or a local file, we add the button.
var ext = "";
var m = this.def.match(/\.([a-z0-9]+)$/);
if (m) {
ext = "*." + m[1];
}
// We're cheating again here, if it ends "filename" it's
// a local file path.
var type = this.name.match(/filename$/i) ? "file" : "fileurl";
type = this.name.match(/folder$/i) ? "folder" : type;
appendButton(this.box, "onPrefBrowse", {
label: MSG_PREFS_BROWSE,
spec: ext,
kind: type,
});
}
}
break;
case "number":
label = document.createElement("label");
label.setAttribute("value", this.label);
label.width = 100;
label.flex = 1;
this.box.appendChild(label);
this.edit = document.createElement("textbox");
this.edit.setAttribute("size", "5");
this.edit.setAttribute("type", "number");
this.edit.setAttribute("min", "-1");
editCont = document.createElement("hbox");
editCont.flex = 1000;
editCont.appendChild(this.edit);
this.box.appendChild(editCont);
break;
case "boolean":
this.edit = document.createElement("checkbox");
this.edit.setAttribute("label", this.label);
this.box.appendChild(this.edit);
break;
case "array":
this.box.removeAttribute("align");
var oBox = document.createElement("box");
oBox.orient = "vertical";
oBox.flex = 1;
this.box.appendChild(oBox);
if (this.help) {
label = document.createElement("label");
label.appendChild(document.createTextNode(this.help));
oBox.appendChild(label);
}
this.edit = document.createElement("listbox");
this.edit.flex = 1;
this.edit.setAttribute("style", "height: 1em;");
this.edit.setAttribute("kind", "url");
if (this.def.length > 0 && this.def[0].match(/^file:\//)) {
this.edit.setAttribute("kind", "fileurl");
}
this.edit.setAttribute("onselect", "gPrefWindow.onPrefListSelect(this);");
this.edit.setAttribute("ondblclick", "gPrefWindow.onPrefListEdit(this);");
oBox.appendChild(this.edit);
var box = document.createElement("box");
box.orient = "vertical";
this.box.appendChild(box);
// NOTE: This order is important - getRelatedItem needs to be
// kept in sync with this order. Perhaps a better way is needed...
appendButton(box, "onPrefListUp", {
label: MSG_PREFS_MOVE_UP,
class: "up",
});
appendButton(box, "onPrefListDown", {
label: MSG_PREFS_MOVE_DOWN,
class: "down",
});
appendSeparator(box);
appendButton(box, "onPrefListAdd", { label: MSG_PREFS_ADD });
appendButton(box, "onPrefListEdit", { label: MSG_PREFS_EDIT });
appendButton(box, "onPrefListDelete", { label: MSG_PREFS_DELETE });
break;
default:
// This is really more of an error case, since we really should
// know about all the valid pref types.
var label = document.createElement("label");
label.setAttribute("value", "[not editable] " + this.type);
this.box.appendChild(label);
}
this.loadData();
if (this.edit) {
this.edit.setAttribute("prefobjectindex", this.parent.arrayIndex);
this.edit.setAttribute("prefname", this.name);
// Associate textbox with label for accessibility.
if (label) {
this.edit.id = this.manager.branchName + this.name;
label.setAttribute("control", this.edit.id);
}
}
if (
!ASSERT(
"groups" in this.parent,
"Must have called " +
"[ObjectPrivateData].loadXUL before trying to display prefs."
)
) {
return;
}
this.parent.addGroup(this.mainGroup);
if (this.subGroup) {
this.parent.groups[this.mainGroup].addGroup(this.subGroup);
}
if (!this.subGroup) {
this.parent.groups[this.mainGroup].box.appendChild(this.box);
} else {
this.parent.groups[this.mainGroup].groups[this.subGroup].box.appendChild(
this.box
);
}
// Setup tooltip stuff...
if (this.help && this.type != "array") {
this.box.setAttribute("tooltiptitle", this.label);
this.box.setAttribute("tooltipcontent", this.help);
this.box.setAttribute("onmouseover", "gPrefWindow.onPrefMouseOver(this);");
this.box.setAttribute("onmousemove", "gPrefWindow.onPrefMouseMove(this);");
this.box.setAttribute("onmouseout", "gPrefWindow.onPrefMouseOut(this);");
}
};
/* Loads the pref's data into the edit component. */
PrefData.prototype.loadData = function () {
/* Note about .value and .setAttribute as used here:
*
* XBL doesn't kick in until CSS is calculated on a node, so the code makes
* a compromise and uses these two methods as appropriate. Initally this
* is called is before the node has been placed in the document DOM tree,
* and thus hasn't been "magiced" by XBL and so .value is meaningless to
* it. After initally being set as an attribute, it's added to the DOM,
* XBL kicks in, and after that .value is the only way to change the value.
*/
switch (this.type) {
case "string":
if (this.edit.hasAttribute("value")) {
this.edit.value = this.val;
} else {
this.edit.setAttribute("value", this.val);
}
break;
case "number":
if (this.edit.hasAttribute("value")) {
this.edit.value = this.val;
} else {
this.edit.setAttribute("value", this.val);
}
break;
case "boolean":
if (this.edit.hasAttribute("checked")) {
this.edit.checked = this.val;
} else {
this.edit.setAttribute("checked", this.val);
}
break;
case "array":
// Remove old entires.
while (this.edit.hasChildNodes()) {
this.edit.firstChild.remove();
}
// Add new ones.
for (var i = 0; i < this.val.length; i++) {
var item = document.createElement("listitem");
item.value = this.val[i];
item.crop = "center";
item.setAttribute("label", this.val[i]);
this.edit.appendChild(item);
}
// Make sure buttons are up-to-date.
gPrefWindow.onPrefListSelect(this.edit);
break;
default:
}
};
/* Cleans up the mess. */
PrefData.prototype.clear = function () {
//dd("Clearing pref " + this.name);
if ("box" in this && this.box) {
this.box.remove();
delete this.box;
}
try {
this.manager.clearPref(this.name);
} catch (ex) {}
};
/* Resets the pref to it's default. */
PrefData.prototype.reset = function () {
//try {
// this.manager.clearPref(this.name);
//} catch(ex) {}
this.val = this.def;
this.loadData();
};
/* Saves the pref... or would do. */
PrefData.prototype.save = function () {
//FIXME//
};
// Represents a "main group", i.e. a tab for a single pref object.
function PrefMainGroup(parent, name) {
// Init this group's object.
this.parent = parent; // Private data for pref object.
this.name = name;
this.groups = {};
this.label = getMsg("pref.group." + this.name + ".label", null, this.name);
this.tab = document.createElement("tab");
this.tabPanel = document.createElement("tabpanel");
this.box = this.sb = document.createElement("scroller");
this.tab.setAttribute("label", this.label);
this.tabPanel.setAttribute("orient", "vertical");
this.sb.setAttribute("orient", "vertical");
this.sb.setAttribute("flex", 1);
this.parent.tabs.appendChild(this.tab);
this.parent.tabPanels.appendChild(this.tabPanel);
this.tabPanel.appendChild(this.sb);
return this;
}
// Adds a sub group to this main group.
PrefMainGroup.prototype.addGroup = function (name) {
// If the sub group doesn't exist, make it exist.
if (!(name in this.groups)) {
this.groups[name] = new PrefSubGroup(this, name);
}
return this.groups[name];
};
// Represents a "sub group", i.e. a groupbox on a tab, for a single main group.
function PrefSubGroup(parent, name) {
this.parent = parent; // Main group.
this.name = name;
this.fullGroup = this.parent.name + "." + this.name;
this.label = getMsg(
"pref.group." + this.fullGroup + ".label",
null,
this.name
);
this.help = getMsg("pref.group." + this.fullGroup + ".help", null, "");
this.gb = document.createElement("groupbox");
this.cap = document.createElement("caption");
this.box = document.createElement("box");
this.cap.setAttribute("label", this.label);
this.gb.appendChild(this.cap);
this.box.orient = "vertical";
// If there's some help text, we place it as the first thing inside
// the <groupbox>, as an explanation for the entire subGroup.
if (this.help) {
this.helpLabel = document.createElement("label");
this.helpLabel.appendChild(document.createTextNode(this.help));
this.gb.appendChild(this.helpLabel);
}
this.gb.appendChild(this.box);
this.parent.box.appendChild(this.gb);
return this;
}
// Actual pref window itself.
function PrefWindow() {
// Not loaded until the pref list and objects have been created in |onLoad|.
this.loaded = false;
/* PREF TAB ORDER: Determins the order, and fixed tabs, found on the UI.
*
* It is an array of mainGroup names, and a flag indicating if the tab
* should be created even when there's no prefs for it.
*
* This is for consistency, although none of the ChatZilla built-in pref
* objects leave fixed tabs empty currently.
*/
this.prefTabOrder = [
{ fixed: true, name: "general" },
{ fixed: true, name: "appearance" },
{ fixed: false, name: "lists" },
{ fixed: false, name: "dcc" },
{ fixed: false, name: "startup" },
{ fixed: false, name: "global" },
{ fixed: false, name: "munger" },
];
/* PREF OBJECTS: Stores all the objects we've using that have prefs.
*
* Each object gets a "privateData" object, which is then used by the pref
* window code for storing all of it's data on the object.
*/
this.prefObjects = null;
/* TOOLTIPS: Special tooltips for preference items.
*
* Timer: return value from |setTimeout| whenever used. There is only
* ever one timer going for the tooltips.
* Showing: stores visibility of the tooltip.
* ShowDelay: ms delay which them mouse must be still to show tooltips.
* HideDelay: ms delay before the tooltips hide themselves.
*/
this.tooltipTimer = 0;
this.tooltipShowing = false;
this.tooltipShowDelay = 1000;
this.tooltipHideDelay = 20000;
this.tooltipBug418798 = false;
}
PrefWindow.prototype.TYPE = "PrefWindow";
/* Updates the tooltip state, either showing or hiding it. */
PrefWindow.prototype.setTooltipState = function (visible) {
// Shortcut: if we're already in the right state, don't bother.
if (this.tooltipShowing == visible) {
return;
}
var tt = document.getElementById("czPrefTip");
// If we're trying to show it, and we have a reference object
// (this.tooltipObject), we are ok to go.
if (visible && this.tooltipObject) {
/* Get the boxObject for the reference object, as we're going to
* place to tooltip explicitly based on this.
*/
var tipobjBox = this.tooltipObject.boxObject;
// Adjust the width to that of the reference box.
tt.sizeTo(tipobjBox.width, tt.boxObject.height);
/* show tooltip using the reference object, and it's screen
* position. NOTE: Most of these parameters are supposed to affect
* things, and they do seem to matter, but don't work anything like
* the documentation. Be careful changing them.
*/
tt.showPopup(
this.tooltipObject,
-1,
-1,
"tooltip",
"bottomleft",
"topleft"
);
// Set the timer to hide the tooltip some time later...
// (note the fun inline function)
this.tooltipTimer = setTimeout(
setTooltipState,
this.tooltipHideDelay,
this,
false
);
this.tooltipShowing = true;
} else {
/* We're here because either we are meant to be hiding the tooltip,
* or we lacked the information needed to show one. So hide it.
*/
tt.hidePopup();
this.tooltipShowing = false;
}
};
/** Window event handlers **/
/* Initalises, and loads all the data/utilities and prefs. */
PrefWindow.prototype.onLoad = function () {
// Get ourselves a base object for the object hierarchy.
client = new PrefGlobal();
// Kick off the localisation load.
initMessages();
// Use localised name.
client.viewName = MSG_PREFS_GLOBAL;
client.unicodeName = client.viewName;
client.prettyName = client.viewName;
// Use the window mediator service to prevent mutliple instances.
var enumerator = Services.wm.getEnumerator(CONFIG_WINDOWTYPE);
// We only want one open at a time because don't (currently) cope with
// pref-change notifications. In fact, it's not easy to cope with.
// Save it for some time later. :)
enumerator.getNext();
if (enumerator.hasMoreElements()) {
Services.prompt.alert(window, MSG_ALERT, MSG_PREFS_ALREADYOPEN);
window.close();
return;
}
// Make sure we know what host we're on.
initApplicationCompatibility();
// Kick off the core pref initalisation code.
initPrefs();
// Turn off all notifications, or it'll get confused when any pref
// does change.
client.prefManager.onPrefChanged = function () {};
// The list of objects we're tacking the prefs of.
this.prefObjects = new PrefObjectList();
/* Oh, this is such an odd way to do this. But hey, it works. :)
* What we do is ask the pref branch for the client object to give us
* a list of all the preferences under it. This just gets us all the
* Chatzilla prefs. Then, we filter them so that we only deal with
* ones for networks, channels and users. This means, even if only
* one pref is set for a channel, we get it's network and channel
* object created here.
*/
var prefRE = new RegExp(
"^networks.([^.]+)" + "(?:\\.(users|channels)?\\.([^.]+))?\\."
);
var rv = {};
let branch = Services.prefs.getBranch("extensions.irc.");
let netList = branch.getChildList("networks.", rv);
for (let item of netList) {
let m = item.match(prefRE);
if (!m) {
continue;
}
var netName = unMungeNetworkName(m[1]);
/* We're forcing the network into existance (3rd param) if the
* pref is actually set, as opposed to just being known about (the
* pref branch will list all *known* prefs, not just those set).
*
* NOTE: |force| will, if |true|, set a property on the object so it
* will still exist next time we're here. If |false|, the
* the object will only come into existance if this magical
* [hidden] pref is set.
*/
let force = branch.prefHasUserValue(item);
// Don't bother with the new if it's already there (time saving).
if (!(":" + netName in client.networks)) {
new PrefNetwork(client, netName, force);
}
if (2 in m && 3 in m && ":" + netName in client.networks) {
let net = client.networks[":" + netName];
// Create a channel object if appropriate.
if (m[2] == "channels") {
new PrefChannel(net.primServ, unMungeNetworkName(m[3]), force);
}
// Create a user object if appropriate.
if (m[2] == "users") {
new PrefUser(net.primServ, unMungeNetworkName(m[3]), force);
}
}
}
// Store out object that represents the current view.
var currentView = null;
// Enumerate source window's tab list...
if (
"arguments" in window &&
0 in window.arguments &&
"client" in window.arguments[0]
) {
/* Make sure we survive this, external data could be bad. :) */
try {
var czWin = window.arguments[0];
var s;
var n, c, u;
this.ownerClient = czWin.client;
this.ownerClient.configWindow = window;
/* Go nick the source window's view list. We can then show items in
* the tree for the currently connected/shown networks, channels
* and users even if they don't have any known prefs yet.
*
* NOTE: the |false, true| params tell the objects to not force
* any prefs into existance, but still show in the tree.
*/
for (i = 0; i < czWin.client.viewsArray.length; i++) {
var view = czWin.client.viewsArray[i].source;
// Network view...
if (view.TYPE == "IRCNetwork") {
n = new PrefNetwork(client, view.unicodeName, false, true);
if (view == czWin.client.currentObject) {
currentView = n;
}
}
if (view.TYPE.match(/^IRC(Channel|User)$/)) {
n = new PrefNetwork(
client,
view.parent.parent.unicodeName,
false,
true
);
s = n.primServ;
}
// Channel view...
if (view.TYPE == "IRCChannel") {
c = new PrefChannel(s, view.unicodeName, false, true);
if (view == czWin.client.currentObject) {
currentView = c;
}
}
// User view...
if (view.TYPE == "IRCUser") {
u = new PrefUser(s, view.unicodeName, false, true);
if (view == czWin.client.currentObject) {
currentView = u;
}
}
}
} catch (ex) {}
}
// Add the client object...
this.prefObjects.addObject(client);
// ...and everyone else.
var i, j;
/* We sort the keys (property names, i.e. network names). This means the UI
* will show them in lexographical order, which is good.
*/
var sortedNets = Object.keys(client.networks).sort();
for (i = 0; i < sortedNets.length; i++) {
net = client.networks[sortedNets[i]];
this.prefObjects.addObject(net);
var sortedChans = Object.keys(net.channels).sort();
for (j = 0; j < sortedChans.length; j++) {
this.prefObjects.addObject(net.channels[sortedChans[j]]);
}
var sortedUsers = Object.keys(net.users).sort();
for (j = 0; j < sortedUsers.length; j++) {
this.prefObjects.addObject(net.users[sortedUsers[j]]);
}
}
// Select the first item in the list.
var prefTree = document.getElementById("pref-tree-object");
if ("selection" in prefTree.treeBoxObject) {
prefTree.treeBoxObject.selection.select(0);
} else {
prefTree.view.selection.select(0);
}
// Find and select the current view.
for (i = 0; i < this.prefObjects.objects.length; i++) {
if (this.prefObjects.objects[i] == currentView) {
if ("selection" in prefTree.treeBoxObject) {
prefTree.treeBoxObject.selection.select(i);
} else {
prefTree.view.selection.select(i);
}
}
}
this.onSelectObject();
// We're done, without error, so it's safe to show the stuff.
document.getElementById("loadDeck").selectedIndex = 1;
// This allows [OK] to actually save, without this it'll just close.
this.loaded = true;
// Force the window to be the right size now, not later.
window.sizeToContent();
// XXX: If we're on mac, make it wider because the default theme's
// tabs are huge:
if (client.platform == "Mac") {
window.resizeBy(140, 0);
}
// Center window.
if ("arguments" in window && 0 in window.arguments) {
var ow = window.arguments[0];
window.moveTo(
ow.screenX + Math.max((ow.outerWidth - window.outerWidth) / 2, 0),
ow.screenY + Math.max((ow.outerHeight - window.outerHeight) / 2, 0)
);
}
};
/* Closing the window. Clean up. */
PrefWindow.prototype.onClose = function () {
if (this.ownerClient) {
delete this.ownerClient.configWindow;
}
if (this.loaded) {
destroyPrefs();
}
};
/* OK button. */
PrefWindow.prototype.onOK = function () {
if (this.onApply()) {
window.close();
}
return true;
};
/* Apply button. */
PrefWindow.prototype.onApply = function () {
// If the load failed, better not to try to save.
if (!this.loaded) {
return false;
}
try {
// Get an array of all the (XUL) items we have to save.
var list = getPrefTags();
//if (!Services.prompt.confirm(window, MSG_CONFIRM, "There are " + list.length + " pref tags to save. OK?")) return false;
for (var i = 0; i < list.length; i++) {
// Save this one pref...
var index = list[i].getAttribute("prefobjectindex");
var name = list[i].getAttribute("prefname");
// Get the private data for the object out, since everything is
// based on this.
var po = this.prefObjects.getObject(index);
var pref = po.prefs[name];
var value;
// We just need to force the value from the DOMString form to
// the right form, so we don't get anything silly happening.
switch (pref.type) {
case "string":
value = list[i].value;
break;
case "number":
value = 1 * list[i].value;
break;
case "boolean":
value = list[i].checked;
break;
case "array":
var l = [];
for (var j = 0; j < list[i].childNodes.length; j++) {
l.push(list[i].childNodes[j].value);
}
value = l;
break;
default:
throw Components.Exception(
"Unknown pref type: " + pref.type + "!",
Cr.NS_ERROR_FAILURE
);
}
/* Fun stuff. We don't want to save if the user hasn't changed
* it. This is so that if the default is defered, and changed,
* but this hasn't, we keep the defered default. Which is a
* Good Thing. :)
*/
if (
(pref.type != "array" && value != pref.startVal) ||
(pref.type == "array" && value.join(";") != pref.startVal.join(";"))
) {
po.parent.prefs[pref.name] = value;
}
// Update our saved value, so the above check works the 2nd
// time the user clicks Apply.
pref.val = value;
pref.startVal = pref.val;
}
return true;
} catch (e) {
Services.prompt.alert(window, MSG_ALERT, getMsg(MSG_PREFS_ERR_SAVE, e));
return false;
}
};
/* Cancel button. */
PrefWindow.prototype.onCancel = function () {
window.close();
return true;
};
/** Tooltips' event handlers **/
PrefWindow.prototype.onPrefMouseOver = function (object) {
this.tooltipObject = object;
this.tooltipTitle = object.getAttribute("tooltiptitle");
this.tooltipText = object.getAttribute("tooltipcontent");
// Reset the timer now we're over a new pref.
clearTimeout(this.tooltipTimer);
this.tooltipTimer = setTimeout(
setTooltipState,
this.tooltipShowDelay,
this,
true
);
};
PrefWindow.prototype.onPrefMouseMove = function (object) {
// If the tooltip isn't showing, we need to reset the timer.
if (!this.tooltipShowing) {
clearTimeout(this.tooltipTimer);
this.tooltipTimer = setTimeout(
setTooltipState,
this.tooltipShowDelay,
this,
true
);
}
};
PrefWindow.prototype.onPrefMouseOut = function (object) {
// Left the pref! Hide tooltip, and clear timer.
this.setTooltipState(false);
clearTimeout(this.tooltipTimer);
};
PrefWindow.prototype.onTooltipPopupShowing = function (popup) {
if (!this.tooltipText) {
return false;
}
var fChild = popup.firstChild;
var diff = popup.boxObject.height - fChild.boxObject.height;
// Setup the labels...
var ttt = document.getElementById("czPrefTipTitle");
ttt.firstChild.nodeValue = this.tooltipTitle;
var ttl = document.getElementById("czPrefTipLabel");
ttl.firstChild.nodeValue = this.tooltipText;
/* In Gecko 1.9, the popup has done no layout at this point, unlike in
* earlier versions. As a result, the box object of all the elements
* within it are 0x0. It also means the height of the labels isn't
* updated. To deal with this, we avoid calling sizeTo with the box
* object (as it's 0) and instead just force the popup height to 0 -
* otherwise it will only ever get bigger each time, never smaller.
*/
if (popup.boxObject.width == 0) {
this.tooltipBug418798 = true;
}
if (this.tooltipBug418798) {
popup.height = 0;
} else {
popup.sizeTo(popup.boxObject.width, fChild.boxObject.height + diff);
}
return true;
};
/** Components' event handlers **/
// Selected an item in the tree.
PrefWindow.prototype.onSelectObject = function () {
var prefTree = document.getElementById("pref-tree-object");
var rv = {};
if ("selection" in prefTree.treeBoxObject) {
prefTree.treeBoxObject.selection.getRangeAt(0, rv, {});
} else {
prefTree.view.selection.getRangeAt(0, rv, {});
}
var prefTreeIndex = rv.value;
var delButton = document.getElementById("object-delete");
if (prefTreeIndex > 0) {
delButton.removeAttribute("disabled");
} else {
delButton.setAttribute("disabled", "true");
}
var prefItem = prefTree.contentView.getItemAtIndex(prefTreeIndex);
var pObjectIndex = prefItem.getAttribute("prefobjectindex");
var pObject = this.prefObjects.getObject(pObjectIndex);
if (
!ASSERT(
pObject,
"Object not found for index! " + prefItem.getAttribute("prefobjectindex")
)
) {
return;
}
pObject.loadXUL(this.prefTabOrder);
var header = document.getElementById("pref-header");
header.setAttribute(
"title",
getMsg(MSG_PREFS_FMT_HEADER, pObject.parent.prettyName)
);
var deck = document.getElementById("pref-object-deck");
deck.selectedIndex = pObject.deckIndex;
this.currentObject = pObject;
};
// Browse button for file prefs.
PrefWindow.prototype.onPrefBrowse = function (button) {
var type = button.getAttribute("kind");
var edit = button.previousSibling.lastChild;
var rv;
if (type == "folder") {
try {
// if the user set the pref to an invalid folder, this will throw:
var current = getFileFromURLSpec(edit.value);
} catch (ex) {
// Just setting it to null will work:
current = null;
}
rv = pickGetFolder(MSG_PREFS_BROWSE_TITLE, current);
} else {
let typeList = [];
if (button.hasAttribute("spec")) {
let spec = button.getAttribute("spec");
typeList.push([spec, spec]);
}
rv = pickOpen(MSG_PREFS_BROWSE_TITLE, typeList);
}
if (!rv.ok) {
return;
}
edit.value = type == "file" ? rv.file.path : rv.picker.fileURL.spec;
};
// Selection changed on listbox.
PrefWindow.prototype.onPrefListSelect = function (object) {
var list = getRelatedItem(object, "list");
var buttons = {};
buttons.up = getRelatedItem(object, "button-up");
buttons.down = getRelatedItem(object, "button-down");
buttons.add = getRelatedItem(object, "button-add");
buttons.edit = getRelatedItem(object, "button-edit");
buttons.del = getRelatedItem(object, "button-delete");
if (
"selectedItems" in list &&
list.selectedItems &&
list.selectedItems.length
) {
buttons.edit.removeAttribute("disabled");
buttons.del.removeAttribute("disabled");
} else {
buttons.edit.setAttribute("disabled", "true");
buttons.del.setAttribute("disabled", "true");
}
if (
!("selectedItems" in list) ||
!list.selectedItems ||
list.selectedItems.length == 0 ||
list.selectedIndex == 0
) {
buttons.up.setAttribute("disabled", "true");
} else {
buttons.up.removeAttribute("disabled");
}
if (
!("selectedItems" in list) ||
!list.selectedItems ||
list.selectedItems.length == 0 ||
list.selectedIndex == list.childNodes.length - 1
) {
buttons.down.setAttribute("disabled", "true");
} else {
buttons.down.removeAttribute("disabled");
}
};
// Up button for lists.
PrefWindow.prototype.onPrefListUp = function (object) {
var list = getRelatedItem(object, "list");
var selected = list.selectedItems[0];
var before = selected.previousSibling;
if (before) {
before.parentNode.insertBefore(selected, before);
list.selectItem(selected);
list.ensureElementIsVisible(selected);
}
};
// Down button for lists.
PrefWindow.prototype.onPrefListDown = function (object) {
var list = getRelatedItem(object, "list");
var selected = list.selectedItems[0];
if (selected.nextSibling) {
if (selected.nextSibling.nextSibling) {
list.insertBefore(selected, selected.nextSibling.nextSibling);
} else {
list.appendChild(selected);
}
list.selectItem(selected);
}
};
// Add button for lists.
PrefWindow.prototype.onPrefListAdd = function (object) {
var list = getRelatedItem(object, "list");
var newItem;
switch (list.getAttribute("kind")) {
case "url":
var item = prompt(MSG_PREFS_LIST_ADD);
if (item) {
newItem = document.createElement("listitem");
newItem.setAttribute("label", item);
newItem.value = item;
list.appendChild(newItem);
this.onPrefListSelect(object);
}
break;
case "file":
case "fileurl":
let rv = pickOpen(MSG_PREFS_BROWSE_TITLE);
if (rv.ok) {
var data = { file: rv.file.path, fileurl: rv.picker.fileURL.spec };
var kind = list.getAttribute("kind");
newItem = document.createElement("listitem");
newItem.setAttribute("label", data[kind]);
newItem.value = data[kind];
list.appendChild(newItem);
this.onPrefListSelect(object);
}
break;
}
};
// Edit button for lists.
PrefWindow.prototype.onPrefListEdit = function (object) {
var list = getRelatedItem(object, "list");
switch (list.getAttribute("kind")) {
case "url":
case "file":
case "fileurl":
// We're letting the user edit file types here, since it saves us
// a lot of work, and we can't let them pick a file OR a directory,
// so they pick a file and can edit it off to use a directory.
var listItem = list.selectedItems[0];
var newValue = prompt(MSG_PREFS_LIST_EDIT, listItem.value);
if (newValue) {
listItem.setAttribute("label", newValue);
listItem.value = newValue;
}
break;
}
};
// Delete button for lists.
PrefWindow.prototype.onPrefListDelete = function (object) {
var list = getRelatedItem(object, "list");
var listItem = list.selectedItems[0];
if (
Services.prompt.confirm(
window,
MSG_CONFIRM,
getMsg(MSG_PREFS_LIST_DELETE, listItem.value)
)
) {
listItem.remove();
}
};
/* Add... button. */
PrefWindow.prototype.onAddObject = function () {
var rv = {};
/* Try to nobble the current selection and pre-fill as needed. */
switch (this.currentObject.parent.TYPE) {
case "PrefNetwork":
rv.type = "net";
rv.net = this.currentObject.parent.unicodeName;
break;
case "PrefChannel":
rv.type = "chan";
rv.net = this.currentObject.parent.parent.parent.unicodeName;
rv.chan = this.currentObject.parent.unicodeName;
break;
case "PrefUser":
rv.type = "user";
rv.net = this.currentObject.parent.parent.parent.unicodeName;
rv.chan = this.currentObject.parent.unicodeName;
break;
}
// Show add dialog, passing the data object along.
window.openDialog(
"config-add.xul",
"cz-config-add",
"chrome,dialog,modal",
rv
);
if (!rv.ok) {
return;
}
/* Ok, so what type did they want again?
*
* NOTE: The param |true| in the object creation calls is for |force|. It
* causes the hidden pref to be set for the objects so they are shown
* every time this window opens, until the user deletes them.
*/
switch (rv.type) {
case "net":
this.prefObjects.addObject(new PrefNetwork(client, rv.net, true));
break;
case "chan":
if (!(":" + rv.net in client.networks)) {
this.prefObjects.addObject(new PrefNetwork(client, rv.net, true));
}
this.prefObjects.addObject(
new PrefChannel(client.networks[":" + rv.net].primServ, rv.chan, true)
);
break;
case "user":
if (!(":" + rv.net in client.networks)) {
this.prefObjects.addObject(new PrefNetwork(client, rv.net, true));
}
this.prefObjects.addObject(
new PrefUser(client.networks[":" + rv.net].primServ, rv.chan, true)
);
break;
default:
// Oops. Not good, if we got here.
Services.prompt.alert(window, MSG_ALERT, "Unknown pref type: " + rv.type);
}
};
/* Delete button. */
PrefWindow.prototype.onDeleteObject = function () {
// Save current node before we re-select.
var sel = this.currentObject;
// Check they want to go ahead.
if (
!Services.prompt.confirm(
window,
MSG_CONFIRM,
getMsg(MSG_PREFS_OBJECT_DELETE, sel.parent.unicodeName)
)
) {
return;
}
// Select a new item BEFORE removing the current item, so the <tree>
// doesn't freak out on us.
var prefTree = document.getElementById("pref-tree-object");
if ("selection" in prefTree.treeBoxObject) {
prefTree.treeBoxObject.selection.select(0);
} else {
prefTree.view.selection.select(0);
}
// If it's a network, nuke all the channels and users too.
if (sel.parent.TYPE == "PrefNetwork") {
var chans = sel.parent.channels;
for (k in chans) {
PrefObjectList.getPrivateData(chans[k]).clear();
}
var users = sel.parent.users;
for (k in users) {
PrefObjectList.getPrivateData(users[k]).clear();
}
}
sel.clear();
this.onSelectObject();
};
/* Reset button. */
PrefWindow.prototype.onResetObject = function () {
// Save current node before we re-select.
var sel = this.currentObject;
// Check they want to go ahead.
if (
!Services.prompt.confirm(
window,
MSG_CONFIRM,
getMsg(MSG_PREFS_OBJECT_RESET, sel.parent.unicodeName)
)
) {
return;
}
// Reset the prefs.
sel.reset();
};
// End of PrefWindow. //
/*** Base functions... ***/
/* Gets a "related" items, such as the buttons associated with a list. */
function getRelatedItem(object, thing) {
switch (object.nodeName) {
case "listbox":
switch (thing) {
case "list":
return object;
case "button-up":
return object.parentNode.nextSibling.childNodes[0];
case "button-down":
return object.parentNode.nextSibling.childNodes[1];
case "button-add":
return object.parentNode.nextSibling.childNodes[3];
case "button-edit":
return object.parentNode.nextSibling.childNodes[4];
case "button-delete":
return object.parentNode.nextSibling.childNodes[5];
}
break;
case "button":
var n = object.parentNode.previousSibling.lastChild;
if (n) {
return getRelatedItem(n, thing);
}
break;
}
return null;
}
// Wrap this call so we have the right |this|.
function setTooltipState(w, s) {
w.setTooltipState(s);
}
// Reverses the Pref Manager's munging of network names.
function unMungeNetworkName(name) {
name = ecmaUnescape(name);
return name.replace(/_/g, ":").replace(/-/g, ".");
}
// Adds a button to a container, setting up the command in a simple way.
function appendButton(cont, oncommand, attr) {
var btn = document.createElement("button");
if (attr) {
for (var a in attr) {
btn.setAttribute(a, attr[a]);
}
}
if (oncommand) {
btn.setAttribute("oncommand", "gPrefWindow." + oncommand + "(this);");
} else {
btn.setAttribute("disabled", "true");
}
cont.appendChild(btn);
}
// Like appendButton, but just drops in a separator.
function appendSeparator(cont, attr) {
var spacer = document.createElement("separator");
if (attr) {
for (var a in attr) {
spacer.setAttribute(a, attr[a]);
}
}
cont.appendChild(spacer);
}
/* This simply collects together all the <textbox>, <checkbox> and <listbox>
* elements that have the attribute "prefname". Thus, we generate a list of
* all elements that are for prefs.
*/
function getPrefTags() {
var rv = [];
var i, list;
list = document.getElementsByTagName("textbox");
for (i = 0; i < list.length; i++) {
if (list[i].hasAttribute("prefname")) {
rv.push(list[i]);
}
}
list = document.getElementsByTagName("checkbox");
for (i = 0; i < list.length; i++) {
if (list[i].hasAttribute("prefname")) {
rv.push(list[i]);
}
}
list = document.getElementsByTagName("listbox");
for (i = 0; i < list.length; i++) {
if (list[i].hasAttribute("prefname")) {
rv.push(list[i]);
}
}
return rv;
}
// Sets up the "extra1" button (Apply).
function setupButtons() {
// Hacky-hacky-hack. Looks like the new toolkit does provide a solution,
// but we need to support SeaMonkey too. :)
var dialog = document.documentElement;
dialog.getButton("extra1").label = dialog.getAttribute("extra1Label");
}
// And finally, we want one of these.
var gPrefWindow = new PrefWindow();